What’s new in Java 15?11 min read

Java 15 will officially be released in September 2020, however its new and updated features are now available for us as early access for who wants to find out. In this article, we will go through some of the most prominent features of Java 15, such as text blocks, previews of sealed classes, local enums and interfaces, and records, etc.

You should keep in my that these are preview features, although they are fully implemented, they can be updated as standard features of Java language in the future, or even they can be removed.

Initial Setup for Java 15 experiment

If you feel excited and want to try along with me, you can install the IntelliJ IDEA 2020.2 Early Access Program here. This beta program has implemented a host of cool features, includes the support of Java 15.

After installing, open the IntelliJ IDEA IDE you’ve just installed, create a new Java project, you can choose whatever JDK version you have for your project (there is no need to install JDK 15 Early-Access Builds). Then once the project is created, click to File -> Project Structure -> Project Language Level -> 15 (Preview) – Sealed types, records, patterns, local enums and interfaces -> Apply -> OK:

Record

First, we take a look at one of the most interesting features, which is record, it already was a preview in Java 14 and now in its second preview in Java 15. Before Java 14, the common way we pass immutable data between objects is to create a class with a lot of boilerplate code. Typically, to create an immutable object, we have to repeatedly make private and final field for every piece of data, getters for each, a public constructor, override the toString(), equals() and hashCode() method. For example, we now create a class with immutable data:

class Person {

    private final String firstName;
    private final String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = 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 + '\'' +
                '}';
    }
}

It seems like a lot of work! Even an IDE can generate the code for us, but those mundane tasks just make the code looks really verbose, it is true boredom of having to do this over and over again. There is also a drawback of this approach, if later on a new field is added to the class, then we also need to manually modify the toString(), hashCode() and equals() methods.

Declaring a record

Records come to address the problems that we are facing, to create an immutable data class, everything we need is a record with the type and the name fields:

record Person(String firstName, String lastName) {}

The code with 40 lines before now we can truncate to just 1 line of code! Everything we did before like creating a public constructor, getter, equals, hashCode, and toString methods are automatically generated for us. We get rid of the redundancy and the code still accomplish the same thing as in the first example.

Here are few facts you need to remember about records:

  • Each component of the state description will be marked as private final.
  • Public accessor for each component of the state (but no getters).
  • A public constructor who takes the same parameters as the state description.
  • Implementations of equals()and hashCode()method that say 2 records are equal if they are as the same type and contain the same states.
  • An implementation of toString()method.

You can create a new instance of this record normally as we do with a class by providing appropriate number of arguments to the constructor:

Person person = new Person("Nam", "Do");
person.toString(); // Person[firstName=Nam, lastName=Do]

Notice that the generated toString() method has a little different representation, it contains the name of the record followed by square brackets holding the fields and their corresponding values.

You cannot add an instance field to a record, but it’s legitimate to add any static fields to the record if you want:

public record Person(String firstName, String lastName) {
     static int num;
     public void increaseNum() { 
         num++;
     }
}

A record acts as a final class, hence you cannot extend it, this code will throw a compile error:

record Keyboard(String keyboardName, float keyboardSize) {};

record MechanicalKeyboard(String switchType) extends Keyboard {}; // compile error 

If you want to create a new instance of a record, you need to pass the exact the same number of parameters as in the state description:

record Book(String title, String isbn, String author) {};

Book javaBook = new Book("Effective Java", "0134685997", "Joshua Bloch"); // ok

Book scalaBook = new Book("Programming in Scala", "0981531644", "Martin Odersky", 224); // it doesn't work, throws the compile error 

If you want to add some additional fields inside a record, they must be declared as static:

record Purchase(int invoiceNumber, double salesAmount, double salesTax) {
     static double taxRate = 0.075; // must be declared as static
}

Records inside a method

A record can also be used inside a local method, which might be used as a temporary holder for grouping objects in a readable way :

private Set<Person> aPrivateMethodWithALocalRecord(List<Person> people) {
      record PeopleWithOrders(Person person, List<Order> orders) {}
      
      // do some logic...
}

Returning multiple values with a record

Let’s consider two method, both scan to a collection to find a min and a max value respectively:

static<T> T min(Iterable<? extends T> elements,
                Comparator<? super T> comparator) { ... }

static<T> T max(Iterable<? extends T> elements,
                Comparator<? super T> comparator) { ... }

To get the min and max value of the collection, we need to scan the list twice instead of once, and the result might be corrupted if the collection can be concurrently modified.

If we try to obtain both min and max value in one pass, which means we need to return both boundary values at once, of course, we can do that by declaring a class. However, most of us try to avoid that purely because of the syntactic overhead of declaring a helper class.

Instead of declaring a helper class, we now have a better alternative solution for this by using a record:

record MinMax<T>(T min, T max) { }

static<T> MinMax<T> minMax(Iterable<? extends T> elements,
                           Comparator<? super T> comparator) { ... }

Local Enum and Interfaces

Java 15 now allows us to create an enum inside a local method. Creating a local enum might be useful for labeling some sort of data within the context of the method:

class Java15Demo {
   private void groupPeopleByGender(List<Person> people) {
      enum Gender {MALE, FEMALE};

      Map<Gender, List<Person>> peopleByGender = new HashMap<>();
      people.stream().filter(Person::isMale)
              .forEach(person -> peopleByGender.computeIfAbsent(Gender.MALE, gender -> new ArrayList<>())
              .add(person));

      // do some more logic...
   }
}

record Person(String firstName, String lastName, boolean isMale) {}

With Java 15, you now also can declare a local interface inside a method as well:

private void localInterfaces() {
      interface Person{
         String getName();
         int getAge();
         String getInfo();
      }

      interface Computer{
         String getType();
         String getCPUInfo();
      }

     // do some more code for logic and implementation...

}

You might also want to implement those interfaces right inside this method by creating anonymous classes.

Text block

Sometimes, we need to use a string literal to hold a snippet containing a SQL query, a simple HTML page, or some JSON data. For example:

String html = "<html>\n" +
           "    <body>\n" +
           "        <p>Hello World!</p>\n" +
           "    </body>\n" +
           "</html>";

If you want to have a multi-line string in Java code, you need to mangle it up into multiple strings with the explicit newline escape and a plus sign for concatenation at the end of each line. If you’re lucky, an IDE will do it for you, but in case you’re not, you need to manually and occasionally you will make mistakes and only discover these at run time.

To address this problem, the text block was introduced in Java 13 as a preview and now it’s a fully-fledged feature in Java 15. With the use of the text block, our string contains a snippet now look much more tasteful:

 String html = """
           <html>
               <body>
                   <p>Hello World</p>
               </body>
           </html>
           """;

As you can see, the text block uses a three quotes delimiter instead of one quote, it uses the same escape set inside so you still can use \n or \r in the body if you want.

Sealed Types

Another preview feature in Java 15 is the ability to declare sealed class and interfaces. According to the introduction of Java 15, a sealed class or interface can only be extended or implemented by classes and interfaces which permitted to do so. You might have seen this feature in other languages, and this missing feature now available in Java.

For example, we have an interface named Animal, and we only allow the implementation of this interface is either a Cat or a Dog and nothing more. We can use the sealed modifier to accomplish this:

sealed interface Animal permits Dog, Cat {}

record Dog(String name) implements Animal {}

record Cat(String name) implements Animal {}

So by creating a sealed interface, you keep the invariant by saying an animal is either a dog or a cat and nothing else. Notice that the new keyword permits is introduced for this purpose.

If we try to implement the Animal interface with an impermissible class or record, the code will not compile:

class Book implements Animal {}; // not permissible, code will not compile

If a class declared with a sealed modifier, the permitted subclasses can either be final, non-sealed, or sealed.

sealed class Person permits Student, Worker, Male {

}

sealed class Student extends Person {} // either marked as a sealed class

final class HighSchoolStudent extends Student {}; // or a final class if there is no subclass for this class. 

non-sealed Male extends Person {}; // or a non-sealed class

final class Worker extends Person {} 

By allowing a set of predefined class from extending your class, your sealed classes will be accessible to other modules and packages, while still preserving the right who can extend it. In the past, to prevent a class from being extended, you had to create private-package classes, which means you limit the accessibility of classes for their own package. Now there is no longer the case when using the sealed classes.

Pattern Matching for instanceof

As Java programmers, all of you might be familiar with the instance-of-and-cast idiom:

if (obj instanceof Student) {
    Student student = (Student) obj;
    student.goToSchool();
} else if (obj instance of Worker) {
    Worker worker = (Worker) worker;
    worker.goToWork();
}

At some point, we all have to do this, writing the code for testing if an object belongs to a particular type by using the instanceof operator, cast the object to this particular type, and store it to a local variable, then there can be further processes with this type. This is straightforward but it is suboptimal for several reasons:

  • First, it’s tedious, you have to perform casting all the time. If this obj is an instance of Student, and of course it cannot be something else but we still need to cast the object to the student.
  • Secondly, it obfuscates the more significant logic flow because there are too many appearances of the Student type.
  • Most importantly, because of the repetitive process, there are opportunities for errors to creep into your program, and you can only discover them at run time.

People might think pattern matching feature is only existed in functional programming languages such as Haskell. However, pattern matching for instance of is a preview feature that has been introduced in Java 14, now this is its second preview in Java 15.

pattern is a combination of a match predicate that determines if the pattern matches a target. With the pattern matching feature, now we can put the pattern on the RHS of the instanceof operator to check an object matches a specific type or not:

if (obj instanceof Student student) {
   student.goToSchool();
}

As you can see, we can write the type pattern as the type name (here Student is our type pattern) followed by a variable declaration (student). If the object has a type of Student by satisfying the matching pattern, then we can use the student variable to do some further work.

By using pattern matching, the code looks much readable and casting is nearly 100% avoidable.

Further reading

There are more lower-level features that have been introduced in Java 15 such as hidden classes, reimplement the legacy DatagramSocket API, disable and deprecate biased locking, and more. If you are interested in these features or want to find out more about the features I have introduced, feel free to check out these links for more comprehensive reading:

Previous Article
Next Article
Every support is much appreciated ❤️

Buy Me a Coffee