What's new in Java 14?

Java 14 new features

New features for developers in Java 14

Posted by Paweł Baczyński on March 11, 2020

Java 14 release is scheduled for general availability on March 17, 2020. Java 14 introduces three changes to the language and one useful feature for runtime:

  • Improve syntax for switch expressions
  • Pattern matching of instanceof (preview)
  • Introducing Records to reduce number of boilerplate code (preview)
  • Better debugging with more descriptive stack traces

In this article, we take a closer look at these new features to show you what to expect in Java 14. Two of them are preview features which means that they’re not permanent and may be changed or removed in future releases. You can enable preview features by using the following compiler flags:

javac --enable-preview --release 14
                        

What is new in Java 14?

Improve syntax for switch expressions

There are several problems with the design of the existing switch statements which were taken from C and C++ languages. A new form of switch called switch expression was created to mitigate them.

The first problem with the old switch statement is that it uses fall through semantics. This means that the break statement must be used in the code..

switch (month) {
    case JANUARY:
    case FEBRUARY:
    case MARCH:
        System.out.println("First quarter");
        break;
    case APRIL:
    case MAY:
    case JUNE:
        System.out.println("Second quarter");
        break;
    case JULY:
    case AUGUST:
    case SEPTEMBER:
        System.out.println("Third quarter");
    case OCTOBER:
    case NOVEMBER:
    case DECEMBER:
        System.out.println("Fourth quarter");
    break;
}
                    

By the way, did you notice a bug in the code? There is a missing break statement for the third quarter. When a programmer forgets the “break;” statement, they’re going to face a nasty bug. The switch expression uses an arrow notation (->) instead of the color (:) used by the older form. The above example can be rewritten to the new form like this:

switch (month) {
    case JANUARY, FEBRUARY, MARCH    -> System.out.println("First quarter");
    case APRIL, MAY, JUNE            -> System.out.println("Second quarter");
    case JULY, AUGUST, SEPTEMBER     -> System.out.println("Third quarter");
    case OCTOBER, NOVEMBER, DECEMBER -> System.out.println("Fourth quarter");                    
}                    
                    

Another issue is the scoping of variables. A variable declared in one case is in the scope of the entire switch statement.

switch (value) {
    case 0:
        String variable = ":)";
        System.out.println(variable);
    case 1:
        String variable = ":("; // compiler error variable already defined in scope
}                                          
                    

This problem doesn’t exist in switch expression.

switch (value) {
    case 0 -> {
        String variable = ":)";
        System.out.println(variable);
    }
    case 1 -> {
        String variable = ":D"; // no problem :)
    }
}    
                    

Also, until version 14 switch statements couldn't yield a value leading to constructs where a variable declaration is separated from the code that assigns a value.

String description;
switch (stars) {
    case 5:
        description = "excellent";
        break;
    case 4:
    case 3:
        description = "good";
        break;
    case 2:
    case 1:
        description = "could be better";
        break;
    default:
        throw new IllegalArgumentException();
}    
                    

This can be rewritten to the new form:

String description = switch (stars) {
    case 5    -> "excellent";
    case 4, 3 -> "good";
    case 2, 1 -> "could be better";
    default   -> throw new IllegalArgumentException();
};                                               
                    

In the case where more than one line is needed, all of them have to be included in curly braces. As all cases need to yield a value, we need to use the newly introduced yield statement. It must be the last statement in the block (similar to return in methods).

String description = switch (stars) {
    case 5    -> {
        System.out.println("Wow, an excellent score!");
        yield "excellent";
    }
    case 4, 3 -> "good";
    case 2, 1 -> "could be better";
    default   -> throw new IllegalArgumentException();
};                        
                    

Note that yield is not a new keyword. It’s rather what we call a restricted identifier (similar to var). It can only exist at the end of the case branch. If it was a new keyword, a lot of the existing code would be broken - including the JDK itself -> Thread. yield().

Pattern matching of instanceof (preview)

From time to time, a Java developer needs to use the intanceof operator. If this is the case, usually a cast to a given type follows right away:

if(number instanceof BigDecimal) {
    BigDecimal big = (BigDecimal) number;
    System.out.println(big.scale());
}                        
                    

This is too verbose (note that BigDecimal is repeated three times). Pattern matching allows declaring a binding variable (big) that relates to a predicate (number instanceof BigDecimal)

if(number instanceof BigDecimal big) {
    System.out.println(big.scale());
}                                              
                    

Isn't it cleaner? The binding variable can also be used in the same if condition:

if(number instanceof BigDecimal big && big.scale() > 1) {
    System.out.println(big.scale());    
}                                             
                    

In Java 14, the only predicate supported for pattern matching is instanceof check, but future releases are expected to extend the set of supported predicates.

Java Records to get rid of boilerplatecode

Did I mention verbose code? Imagine that you need a class that groups just two fields (first and last name). To make this class complete, you’d have to generate an enormous amount of code.

public final class Person {
    private final String firstName;
    private final String lastName;
                        
    public Person(String firstName, String lastName) {
        this.firstName = Objects.requireNonNull(firstName);
        this.lastName = Objects.requireNonNull(lastName);
    }
                        
    public String getFirstName() {
        return firstName;   
    }
                        
    public String getLastName() {
        return lastName;
    }
                        
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        
        return Objects.equals(firstName, person.firstName) &&
            Objects.equals(lastName, person.lastName);
    }
                        
    @Override
    public int hashCode() {
        return Objects.hash(firstName, lastName);
    }
                        
    @Override
    public String toString() {
        return "Person[" +
                "firstName=" + firstName +
                ", lastName=" + lastName +
            ']';
    }
}                    
                    

Records to the rescue! With a simple declaration like this:

public record Person(String firstName, String lastName){
    //Implementation not needed.
}
                    

We can omit all of the required boilerplate code. The Java compiler will generate a constructor, private final fields, accessors, equals/hashCode and toString methods automatically. We could also solve this issue by using Lombok’s @Value annotation. It will be great when it will be possible to do it in the JDK itself without resorting to third party libraries.

The generated class can be examined with javap:

$ javap Person
Compiled from "Person.java"
public final class Person extends java.lang.Record {
  public Person(java.lang.String, java.lang.String);
  public java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public java.lang.String firstName();
  public java.lang.String lastName();
}
                    

There are two differences between the class and the record from the example above.

First, the accessors are named exactly as the fields, so it's firstName() instead of getFirstName(). Not much can be done about this. To accommodate it, a number of libraries need to be updated.

Second, the record implementation does no checks in the generated constructor. Here some things can be changed. It’s possible to modify the generated constructor like in this example:

public record Person (String firstName, String lastName) {
    public Person {
        Objects.requireNonNull(firstName);
        Objects.requireNonNull(lastName);
        // this.firstName = firstName; -- automatically generated
        // this.lastName = lastName;   -- automatically generated
    }
}                        
                    

Note that the construction declaration doesn’t declare any parameter list, not even empty parentheses.

You can also add other methods like constructors as well as initialization blocks or static fields. However, you can use no member fields other than in the record declaration. Also, records must not extend any class. They implicitly extend java.lang.Record.

A perceptive reader may have noticed that the Person record wasn’t declared as final while the class was final. There’s no difference here. In the case of records, they are final by default.

The purpose of records is to serve as value classes. Please don’t abuse them by declaring a service class as record. ;)

It’s interesting that this time, Java architects didn’t introduce a new keyword (like they did when enums were added). So, no existing code using record as identifier will be broken.

Better debugging with more descriptive stack traces

This one isn’t a change to the Java language. Rather, it’s a change to the runtime environment. It allows for easier spotting of a cause to a NullPointerException. It's disabled by default (for performance reasons, among other things). The feature can be useful in debugging. To enable it, the Java process has to be started with option

-XX:+ShowCodeDetailsInExceptionMessages 
                        
Let’s see it in action. There is a line of code that causes NullPointerException.
object.a().b().c().d().foo(); // this line throws NullPointerException
                        
The stack trace:

Exception in thread "main" java.lang.NullPointerException
    at com.example.Main.go(Main.java:27)
    at com.example.Main.main(Main.java:15)
                    

It shows which line caused the exception, but which method call? Not that helpful. Using the new feature we can get this stack trace:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "com.example.Example.d()" because the return value of "com.example.Example.c()" is null
    at com.example.Main.go(Main.java:27)
    at com.example.Main.main(Main.java:15)
                    

Now everything is clear. The value of object.a().b().c() is null and this is the reason for error. This also works for arrays of arrays.

String s = array[1][1][5]; // this line throws NullPointerException
                    

Currently stacktrace would look like this:

Exception in thread "main" java.lang.NullPointerException
    at com.example.Main.go(Main.java:30)
    at com.example.Main.main(Main.java:14)
                    

And with descriptive stack traces enabled:

Exception in thread "main" java.lang.NullPointerException: Cannot load from object array because "array[1][1]" is null
    at com.example.Main.go(Main.java:30)
    at com.example.Main.main(Main.java:14)                
                    

Again, this is a lot more useful as it clearly points to the part of the code that causes the problem.

The features added respond to programmers' needs. They’ll make working with Java easier as they allow to reduce the need for boilerplate code. Pattern matching for instanceof doesn’t seem like an important change but it’s a start to adding pattern matching to other places. Records look like a little revolution that should have been added a long time ago. I hope these features will soon move out of the preview stage and into the stable set of features.

The release of Java 14 brings some other changes as well. Some of them affect garbage collection, removing deprecated tools, etc. For a full list of features, visit this page

Let`s work together!

+48 792 014 952