Meet SoftTeco at a MEDICA 2024 11-14 Nov, Düsseldorf

Java 14 Version and Its Features

As an avid Java adept, SoftTeco constantly monitors all trends and changes that happen in the Java world. So obviously, we could not miss the release of Java 14 that happened on March 17, 2020. 

For now, some of the features of the new version remain in the preview mode. That means that they are under consideration and may be removed or changed in the future. However, developers are encouraged to test them and leave feedback but it’s not recommended to use these features in the actual development.

So what are the most interesting new features that Java 14 brought to developers? The SoftTeco team reviews them below.

Java 14 Version and Its Features

Pattern Matching for instanceof (preview mode)

Java 14 offers us an improved and shorter version of pattern comparison, which includes a predicate and related variables. The scope of various variables is limited to the block in which it is declared.

Object object = new User("Alex", 30, "male",
       new Address("Belarus", "Minsk"));
// Java 13 and previous
if (object instanceof User) {
   User user = (User) object;
   System.out.println(user.getName());
}
// Java 14. Pattern Matching for instanceof (Preview)
if (object instanceof User user) {
   System.out.println(user.getAddress().getCity());
}

Now we can define the variable in the predicate and when the object matches, we can immediately use the variable as a typed object.

Helpful NullPointerException

Another great feature of a new Java 14 version is a more informative NPE. 

An NPE can occur anywhere in the program and developers rely on the JVM to determine the line number of this error. But if the line involves calling several nested objects, it becomes impossible to determine which of them was not generating an error.

public class User {
    private String name;
    private int age;
    private String gender;
    private Address address;
}
public class Address {
    private String country;
>    private String city;
}
public class ExampleNPE {
   public static void main(String[] args) {
       User user = new User("Alex", 30, "male", null);
       System.out.println(user.getAddress().getCity());
   }
}

In order to find the intermediate variables before, developers had to resort to debugging or enter local variables. With Java 14, it is now possible to pinpoint the exact source. The NPE must use the command line parameter:

-XX:+ShowCodeDetailsInExceptionMessages
In this mode, the error message will look like this:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "com.softteco.project.Address.getCity()" because the return value of "com.softteco.project.User.getAddress()" is null
at com.softteco.project.ExampleNPE.main(ExampleNPE.java:8)

In the first part of the message, we see to which line this error belongs and in the second part of the message, we can see what exactly caused it.

Such messages are more readable and developers do not have to resort to debugging. However, it is important to note that in this form, the NPE poses a security risk since this approach reveals the code structure and the names of the local variables (if javac -g debugging information is included in our class). Therefore, it is advisable to use this mode in a test environment only and not to display this NPE format in the Production mode.  

How to Create Keystore From Certificate and Private Key

It is impossible to imagine a modern web application that is not using TLS connections. So, regardless of the …

Record (preview mode)

Records are a new type that was designed to store data. This type does not carry any functional load. 

During the development process, it often becomes necessary to create classes that essentially only declare the data. But in order to work with this data, one needs to create special methods (getters, setters), constructors, redefine hashCode(), equals(), toString() methods. 

Record classes eliminate this syntax load and make the classes less verbose without using any additional libraries (such as lombok). All you have to do is specify the necessary variables in the record definition. You won’t be able to add additional fields in the body of the record though as it is only possible if they are static. By declaring a record, you will explicitly indicate that this record is used for data storage only.

public record Person(String name, int age, String country) {
}

However, you need to understand that a record is an immutable type and is implicitly inherited from the Record class. Thus, the record above will cause an error «Classes cannot directly extend ‘java.lang.Record’». That means you cannot inherit from this record and cannot extend it, but you can implement interfaces. 

Records are not abstract so you will need to override the interface methods. Despite the fact that Records are created to store the data, you can also define your own methods.

If we look at our class after compilation, we get an immutable class with final variables, getters, a canonical constructor, and overridden methods hashCode (), equals (), toString ().

public final class Person extends java.lang.Record {
   private final java.lang.String name;
   private final int age;
   private final java.lang.String country;
   public Person(java.lang.String name, int age, java.lang.String country) { /* compiled code */ }
   public Person(java.lang.String country) { /* compiled code */ }
   public java.lang.String toString() { /* compiled code */ }
   public final int hashCode() { /* compiled code */ }
   public final boolean equals(java.lang.Object o) { /* compiled code */ }
   public java.lang.String name() { /* compiled code */ }
   public int age() { /* compiled code */ }
   public java.lang.String country() { /* compiled code */ }
}

At the same time, the writing mechanism overrides these methods to get the correct value. For example, the use of the toString() method for the regular User class and for writing Person will result in the following output to the console:

Person person = new Person("Alex", 30, "Belarus");
System.out.println(person.toString());
User user = new User("Alex", 30, "male",
       new Address("Belarus", "Minsk"));
System.out.println(user.toString());
Result: 
Person[name=Alex, age=30, country=Belarus]
com.softteco.project.User@277050dc

When creating a Record, we get a canonical constructor. You can also define constructors with a different number of fields if you have optional fields or if you use the default values for them by defining this moment in the constructor. You can also override the canonical constructor if you need to add your own logic to it. For the canonical constructor, you can use the simplified syntax without specifying the parameters:

public Person {
   //your logic for canonical constructor
}
public Person(String name) {
   this(name, 0, null);
}

It should also be said that two additional methods for writing were added to the java.lang.Class: isRecord(), which returns a Boolean value if our object is a record, and getRecordComponents (), which returns an array of fields with information such as the field name and the data type.

Person person = new Person("Alex", 30, "Belarus");
System.out.println(person.getClass().isRecord());
RecordComponent[] array = person.getClass().getRecordComponents();
for (RecordComponent component : array) {
   System.out.println(component);
}
Result:
true
java.lang.String name
int age
java.lang.String country

Switch Expressions (standard)

Below is a common example of a Switch structure:

public static int getDay(Day day) {
   int numberDay = 0;
   switch (day) {
       case MONDAY:
           numberDay = 1;
           break;
       case TUESDAY:
           numberDay = 2;
           break;
       case WEDNESDAY, THURSDAY:
           numberDay = 3;
           break;
       case FRIDAY:
           numberDay = 5;
           break;
       default:
           numberDay = 7;
   }
   return numberDay;
}

There is a potential risk here though. In case a developer forgets to specify the break statement in some Case, he will get wrong values.

The improvements in Switch construction started to appear in Java 12 when Switch became available for use as an expression and when the new  L -> syntax was introduced. This syntax eliminates the need for a break statement though it can be used to return a value, several commas could be specified constants, and the expression itself can be passed into variables. 

In Java 13, the Switch expression was improved by replacing the word «break» (used to return the value) with the word «yield», which was logically more understandable and correct. Thus, it became possible to use the Switch expression in its usual form or in lambda syntax.

public static int getDay(Day day) {
   int numberDay = switch (day) {
       case MONDAY: yield 1;
       case TUESDAY: yield 2;
       case WEDNESDAY, THURSDAY: yield 3;
       case FRIDAY: yield 5;
       default: yield 7;
   };
public static int getDay(Day day) {
   int numberDay = switch (day) {
       case MONDAY -> 1;
       case TUESDAY -> 2;
       case WEDNESDAY, THURSDAY -> 3;
       default -> {
           System.out.println(day);
           yield 7;
       }
   };

If any of the blocks is multi-line, we use the keyword «yield» to indicate the return value.

In both Java 12 and 13, Switch Expressions were included in the preview. In Java 14, the improved version of Switch Expressions is included in the standard JDK build.

It is important to highlight another Switch feature. If you use an enum for the case block and you did not specify all the values, you will receive an error saying «’switch’ expression does not cover all possible input values»:

public static int getDay(Day day) {
   int numberDay = switch (day) {
       case MONDAY -> 1;
       case TUESDAY -> 2;
       case WEDNESDAY, THURSDAY -> 3;
       case FRIDAY -> 5;
   };

To avoid this error, you must either use all the values from the enumeration or specify the default block, which will be used for all unused options:

public static int getDay(Day day) {
   int numberDay = switch (day) {
       case MONDAY -> 1;
       case TUESDAY -> 2;
       case WEDNESDAY, THURSDAY -> 3;
       case FRIDAY -> 5;
       case SATURDAY, SUNDAY -> 7;
   };
public static int getDay(Day day) {
   int numberDay = switch (day) {
       case MONDAY -> 1;
       case TUESDAY -> 2;
       case WEDNESDAY, THURSDAY -> 3;
       default -> 7;
   };

Text Blocks (preview mode)

Text blocks are a multi-line string literal that forms our string in an expected format and eliminates the need to use many escape sequences. Text blocks solve the issue of multiple escaping and concatenation by using sql, html, json and other injections into the code.

String sql = "SELECT * FROM users\n" +
" WHERE city = 'Minsk'\n" +
" ORDER BY last_name;";
The Text block syntax looks like this:
String sql = """
                   SELECT * FROM users
                          WHERE city = 'Minsk'
                          ORDER BY last_name;
                   """;

We include the necessary string literal in the three pairs of quotation marks. In this position, the text should be on the next line after the opening quotation marks and each transition to a new line is a line break. The trailing quotation marks determine text formatting, namely the indentation at the beginning of the line. Let’s try to display this block in the console:

SELECT * FROM users
       WHERE city = 'Minsk'
       ORDER BY last_name;

We can also use text blocks to concatenate with strings:

String query = "new query: " +
       """
             SELECT * FROM users
             WHERE city = 'Minsk'
             ORDER BY last_name;
             """;

This feature was already introduced in Java 13 and received several upgrades in Java 14. For example, two delimiters were added in Java 14: cancellation of a new line «\» and a single space «\s», for the case when we need to explicitly indicate a space since random problems at the end of the line are removed during the compilation.

//language=SQL
String sql = """
       SELECT * FROM users\
       WHERE city = 'Minsk'\s\
       ORDER BY last_name;
""";

The «\» character must be the last in the line, or an error will be displayed. Pay attention to the output of this block to the console:

Result:

SELECT * FROM usersWHERE city = ‘Minsk’ ORDER BY last_name;

The absence of a space results in merged strings in the sql-query, which, in turn, leads to an error in the query. We can see this by using the automatic syntax highlighting.

But we also need to monitor the spaces, since the implicit spaces at the end of the line are deleted after the compilation and result in an error. The explicit «\s» character helps in this case:

//language=SQL
String sql = """
       SELECT * FROM users
       WHERE city = 'Minsk'\s
       ORDER BY last_name;
""";

With text blocks, the code becomes more clear and readable, but in Java 14 the text blocks remain in a preview mode.

Result:

SELECT * FROM users

WHERE city = ‘Minsk’ 

ORDER BY last_name;

Packaging Tool (incubator)

The last feature that I’d like to discuss is the packaging tool based on javapackeger JavaFX. This tool allows you to package Java applications in platform-specific formats: Windows — msi and exe, Linux — deb and rpm, MacOS — pkg and dmg. In this way, developers can install and uninstall Java applications in a familiar way.

The resulting container includes the necessary runtime and application. The installer does not include a graphical interface; all work is done through the terminal.

For a non-modular application, the command looks like this:

$ jpackage --name app --input lib --main-jar app.jar

Here, app is the name of the received package, lib is the application location directory, app.jar is our jar file. The file will be created in the current directory in the standard format of the current OS.

By using the –type option you can specify the format of the output file.

$ jpackage --name app --input lib --main-jar app.jar --type msi

If the application does not have an attribute of the main class, it must be specified by using the –main-class option (Main is the class name):<

$ jpackage --name app --input lib --main-jar app.jar\--main-class app.Main

For a modular application, the command looks like this:

$ jpackage --name app --module-path lib -m app

where all the modules are in the lib folder.

If the main module app does not identify its main class, it must be specified:

$ jpackage --name app --module-path lib -m app/app.Main

When creating a package, we can specify various attributes, like metadata. You can also use details for a specific OS, such as the installation path when creating a shortcut. After starting the package, the application is installed, and then a standard launch for a specific OS is possible, for example, by double-clicking on the shortcut of the exe-file for Windows.

This functionality is presented in an incubator module, meaning that it can be modified or removed in the future.

Final word

It’s nice to see how Java keeps evolving and constantly receives new features that help create robust and reliable Java applications. In case you want to try out the new Java 14 features, you can download the open-source JDK 14 here (available for Windows, Linux, and macOS). Let us know what you think about the new Java version in comments – we want to hear your thoughts and opinions!

Want to stay updated on the latest tech news?

Sign up for our monthly blog newsletter in the form below.

Softteco Logo Footer