Features

Java 16 and IntelliJ IDEA

If you are still working with Java 8, you might have mixed feelings about the news of the release of Java 16. However, you’ll see these numbers are going to increment at a much faster and predictable rate with Java’s six-month release cadence.

I’m personally excited about Java 16! It adds Records and Pattern Matching for instanceof as standard language features with Sealed classes continuing to be a preview feature (in the second preview).

Fun fact – Records was voted the most popular Java 16 language feature by 1158 developers in this Twitter poll, with Pattern Matching for instanceof second.

In this blog post, I will limit coverage of Java 16 to its language features, why you need them, and how you can start using them in IntelliJ IDEA. You can use this link for a comprehensive list of the new Java 16 features. Let’s get started.


Records

Records introduce a new type declaration that simplifies the task of modeling your immutable data. Though it helps cut down on boilerplate code significantly, that isn’t the primary reason for its introduction. Here’s an example:

record Person(String name, int age) {}

With just one line of code, the preceding example defines a record Person with two components name and age. To create a record using IntelliJ IDEA 2021.1, select Record in the New Java Class dialog box. Fill in the name and you are good to go.

Let’s quickly check the configuration of IntelliJ IDEA on your system to ensure you can get the code to run it.

IntelliJ IDEA Configuration

Java 16 features are supported in IntelliJ IDEA 2021.1, which is scheduled to be released this March. The early access versions of 2021.1 are already available. You can configure it to use Java 16 by selecting 16 as the Project SDK and choosing 16 (Preview) – Sealed types in the Project language level for your Project and Modules settings.

You can also download Java 16 directly from IntelliJ IDEA. To do so, go to Platform Settings and click on SDKs, then click the ‘+’ sign at the top, choose Download JDK, then select the Vendor and version and the directory to download the JDK to.

Access sample code used in this blog post

All sample code used in this blog post is hosted at Github.

Implicit members added to a record

The compilation process creates a full-blown class – a record is defined as a final class, extending the java.lang.Record class from the core Java API. For each of the components of the record Person, the compiler defines a final instance variable (name and age). Interestingly, the name of the getter method is the same as that of the data variable (it doesn’t start with get or is). Since a record is supposed to be immutable, no setter methods are defined.
The methods toString(), hashCode(), and equals() are also generated automatically for records.

Why use records

Imagine you want to persist the details of, say, a person to a file. Before the introduction of records, you would need to define a regular class with instance variables, accessor methods, and implement methods from the Object class (toString(), hashcode(), and equals()). Though IntelliJ IDEA can generate all this code for you easily, there is no way to tag this class as a data class, so it would still be read as a regular class.

The following gif demonstrates how you can declare class Person as a regular class, generate code for it, implement the interface Serializable and run it to verify you can write it to a file and read from it:

Now, let’s declare Person as a record using just one line of code with components name (String) and age (int). It implements the interface Serializable since we need to persist its instances to a file. Note that you can still use the same methods to persist it to a text file. Also, there are no changes in how you instantiate it (by using the new operator):

Record Person is just one example on how you can use Records. You can use records to model your data, without additional overhead of defining additional methods.

What you can and can’t add to a record

Since the state of a record is defined using components in its declaration, it doesn’t make much sense to allow the addition of instance variables (or fields) to a record. However, you can add static fields, and instance or static methods to a record, if you need them. Here’s an example:

public record Person(String name, int age) {
   Person {
       instanceCtr++;
   }
   private static int instanceCtr;
   static int getInstanceCtr() {
       return instanceCtr;
   }
}

Modifying the default behavior of a constructor in a record

The default constructor of a record just initializes its state with the values you pass to it. You can change this default behavior if necessary, for example, to validate the parameter values before they are assigned.

IntelliJ IDEA lets you insert a compact, canonical, or custom constructor in a record. For your information, a compact constructor doesn’t have a parameter list, not even parentheses. A canonical constructor is one whose signature matches with the record’s state description. And a custom constructor lets you choose the record components you want to pass to the constructor of a record. With all these constructors, you can add validation code. A compact constructor enables you to add code without adding the full boilerplate code.

Let’s see how you can insert a compact constructor using the Alt+Insert shortcut in IntelliJ IDEA, and add validation code to it:

You can also add a canonical constructor to a record. This defines a parameter list – which must have the same names and order as those of the components of a record. A mismatch would result in a compilation error.

By invoking context actions in IntelliJ IDEA (with Alt+Enter), you can easily convert a canonical constructor to a compact constructor:

Truly immutable data

Records are truly immutable – you can’t change their field values using reflection ( if you haven’t tried it out yet, you can change the value of immutable strings using reflection).

Here is example code for a regular class, Notebook, which defines a private final field pageCount:

public class NoteBook {
   private final int pageCount;

   public NoteBook(int pageCount) {
       this.pageCount = pageCount;
   }

   public int getPageCount() {
       return pageCount;
   }

   @Override
   public String toString() {
       return "NoteBook{" + "pageCount=" + pageCount + '}';
   }
}

And here’s the code for a record Point:

public record Point(int x, int y) {
}

The following code confirms that the private and final fields of a (regular) class can be changed using reflection, but records are a harder nut to crack:

package com.jetbrains.java16.records;
import java.lang.reflect.Field;

public class UseReflection {
   public static void main(String[] args) throws Exception {
       changeFinalFieldValuesForNonRecords();
       changeFinalForRecords();
   }

   private static void changeFinalFieldValuesForNonRecords()
                throws NoSuchFieldException, IllegalAccessException {
       final var noteBook = new NoteBook(10);
       System.out.println(noteBook);

       Field pageField = noteBook.getClass().getDeclaredField("pageCount");
       pageField.setAccessible(true);
       int newCount = 1000;
       pageField.setInt(noteBook, newCount);

       System.out.println(noteBook);
   }

   private static void changeFinalForRecords()
                throws NoSuchFieldException, IllegalAccessException {
       final var point = new Point(12, 35);
       System.out.println(point);

       Field xField = point.getClass().getDeclaredField("x");
       xField.setAccessible(true);
       int newVal = 1000;
       xField.setInt(point, newVal);

       System.out.println(point);
   }
}

Defining a Generic record

You can define records with generics. Here’s an example of a record called Parcel, which can store any object as its contents, and capture the parcel’s dimensions and weight:

public record Parcel<T>(T contents,
   double length,
   double breadth,
   double height,
   double weight) {}

You can instantiate this record as follows:

class Table{ /* class code */ }
public class Java16 {
   public static void main(String[] args) {
       Parcel<Table> parcel = new Parcel<>(
new Table(), 200, 100, 55, 136.88);
       System.out.println(parcel);
   }
}

Converting Record to a regular class

If you are working with records but need to transition it to the codebase of an older Java version that doesn’t support records, you can quickly convert a record to a regular class by using the context action Convert record to class or vice-versa by using the context action Convert to a record:

Records as components of record

A record component can be another record. In the following example, record Automobile defines one of its components as Engine, another record:

Adding annotations to record components

You can add an appropriate annotation to the components of a record, say, @NotNull, as demonstrated in the following gif:

Developers often use third-party libraries like Jackson to persist value objects. Jackson supports records too. However, depending on the Jackson library version you are using (say, 2.11.3), you might need to annotate the components of your records using the annotation @JsonProperty, as follows:

import com.fasterxml.jackson.annotation.JsonProperty;

public record Rectangle(
       @JsonProperty("width") int width,
       @JsonProperty("length") int length) {
}

If you are using Jackson 2.12.2 or later versions, you don’t need to annotate your record components with @JsonProperty.

Here’s some sample code you can use to persist and read records using Jackson:

public class ReadWriteRecordUsingJackson {

   public static void main(String[] args) {
       Rectangle rectangle = new Rectangle(20, 60);
       writeToFileUsingJackson(rectangle);
       System.out.println(readFromFileUsingJackson());
   }

   static void writeToFileUsingJackson(Object obj) {
       try {
           new ObjectMapper()
                   .writeValue(new FileOutputStream(getFile()),
                               obj);
       } catch (IOException e) {
           e.printStackTrace();
       }
   }

   static Object readFromFileUsingJackson() {
       Object retValue = null;
       try {
           retValue = new ObjectMapper()
                        .readValue( new FileInputStream(getFile()),
                                    Rectangle.class);
       } catch (IOException e) {
           e.printStackTrace();
       }
       return retValue;
   }

   private static File getFile() {
       return new File("mydata.json");
   }
}

If you are working with a Maven project, you can add the following dependencies to your pom.xml:

<dependency>
   <groupId>com.fasterxml.jackson.core</groupId>
   <artifactId>jackson-core</artifactId>
   <version>2.12.2</version>
</dependency>
<dependency>
   <groupId>com.fasterxml.jackson.core</groupId>
   <artifactId>jackson-annotations</artifactId>
   <version>2.12.2</version>
</dependency>
<dependency>
   <groupId>com.fasterxml.jackson.core</groupId>
   <artifactId>jackson-databind</artifactId>
   <version>2.12.2</version>
</dependency>

Reading and Writing Records to a File

You can write records to streams and read them, like other class instances. Let your record implement a relevant interface like the Serializable interface. Here’s example code, which will write to and read from a file:

package com.jetbrains.java16.records;
import java.io.Serializable;
public record Person(String name, int age) implements Serializable {
}

package com.jetbrains.java16.records;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.*;
public class ReadWriteObj {

   public static void main(String[] args) throws Exception {
       Person person = new Person("Java", 25);
       writeToFile(person, "../temp.txt");
       System.out.println(readFromFile("../temp.txt"));
   }

   static void writeToFile(Object obj, String path) {
       try (ObjectOutputStream oos = 
                    new ObjectOutputStream(new FileOutputStream(path))){
           oos.writeObject(obj);
       } catch (IOException e) {
           e.printStackTrace();
       }
   }
   static Object readFromFile(String path) {
       Object result = null;
       try (ObjectInputStream ois = 
                    new ObjectInputStream(new FileInputStream(path))){
           result = ois.readObject();
       } catch (ClassNotFoundException | IOException e) {
           e.printStackTrace();
       }
       return result;
   }
}

Refactoring the signature of a Record

You can refactor a Record and modify the order of its components or types, modify their names, and add new or remove existing ones. IntelliJ IDEA has simplified how you apply Rename or Change Signature Refactorings. The changes would reflect in a record’s canonical constructor and its instance creation:

A restricted identifier

record is a restricted identifier (like var), but it isn’t a regular keyword (yet). So, the following code is valid:

int record = 10;
void record() {}

However, you may want to refrain from using record as an identifier because such code will become confusing as more developers start using records.

Local records

You can define local records to model a domain object while you are processing values in a method. In the following example, the method getTopPerformingStocks finds and returns the names of the Stock with the highest value on a specified date.

List<String> getTopPerformingStocks(List<Stock> allStocks, LocalDate date) {
   // TopStock is a local record
   record TopStock(Stock stock, double stockValue) {}

return allStocks.stream()
               .map(s -> new TopStock(s, getStockValue(s, dateTime)))               .sorted(Comparator.comparingDouble(TopStock::value).reversed())
               .limit(2)
               .map(s -> s.stock().name())
               .collect(Collectors.toList());
}

Declaring implicit or explicit static members in an inner class

Starting from Java 16, an inner class can explicitly or implicitly define static members, including records.

Local interfaces and enums

You can declare local enums and interfaces. You can encapsulate your data or business logic, which is local to a method, within the method.

public void createLocalInterface() {
   interface LocalInterface {
       void aMethod();
   }
   // Code to use LocalInterface
}

public void createLocalEnum() {
   enum Color {RED, YELLOW, BLUE}
   // Code to use enum Color
}

However, they cannot capture any context variable. For example, for the local enum Data, the enum constants FOO and BAR can’t be created using the method parameter input in the method test():

void test(int input) {
   enum Data {
       FOO(input), BAR(input*2); // Error. Can’t refer to input

       private final int i;

       Data(int i) {
           this.i = i;
       }
   }
}

Sealed classes and interfaces (a preview feature)

The language syntax of Sealed types enables you to restrict the classes or interfaces that can extend or implement them. The goal of this language feature is to let you define the possible hierarchies in your business domain in a declarative manner. But why would you ever need to create restricted hierarchies?

Need for creating restricted hierarchies

Imagine you are creating an application that helps its users with gardening activities. Depending on the type of plant, a gardener might need to do different activities. Let’s model the plant hierarchy as follows (I’m not detailing the classes on purpose):

class Plant {}

class Herb extends Plant {}
class Shrub extends Plant {}
class Climber extends Plant {}

class Cucumber extends Climber {}

The following code is an example of how the Gardener class might use this hierarchy:

public class Gardner {
   int process(Plant plant) {
       if (plant instanceof Cucumber) {
           return harvestCucumber(plant);
       } else if (plant instanceof Climber) {
           return sowClimber(plant);
       } else if (plant instanceof Herb) {
           return sellHerb(plant);
       } else if (plant instanceof Shrub) {
           return pruneShrub(plant);
       } else {
           System.out.println("Unreachable CODE. Unknown Plant type");
           return 0;
       }
   }

   private int pruneShrub(Plant plant) {...}
   private int sellHerb(Plant plant) {...}
   private int sowClimber(Plant plant) {...}
   private int harvestCucumber(Plant plant) {...}
}

The problem code is the assumption that a developer has to deal with in the last else construct – defining actions even though the developer knows that all possible types of the method parameters plant have been addressed. Though it might look unreachable now, what happens if another developer adds a class to this hierarchy? Sealed classes can impose this restriction on the hierarchies at the language level.

Define secure hierarchies with sealed classes

With the contextual keyword sealed, you can declare a class as a sealed class. A sealed class uses the contextual keyword permits to list the classes that can extend it directly. Its subclasses can either be final, non-sealed, or sealed.

The following gif shows how you can use IntelliJ IDEA to change the declaration of a regular class to a sealed class (by using the context action ‘Seal class’). By default IntelliJ IDEA declares all derived classes as non-sealed. You can modify it to be final or sealed:

Here’s the modified code for reference:

sealed public class Plant permits Herb, Shrub, Climber {
}

final class Herb extends Plant {}
non-sealed class Shrub extends Plant {}
sealed class Climber extends Plant permits Cucumber{}

final class Cucumber extends Climber {}

By allowing a predefined set of classes to extend your class, you can decouple accessibility from extensibility. You can make your sealed class accessible to other packages and modules, and you can still control who can extend it.

In the past, to prevent classes from being extended, developers created package-private classes. But, this also meant that these classes had limited accessibility. Another approach to prevent extension was to create public classes with private or package-private constructors. Though it enabled a class to be visible, it gave limited control on the exact types that could extend your class.

This is no longer the case if you use sealed classes. The goal of the Sealed types is to model your business domain, so that you process it in a definitive manner.

You can’t create another class, say, AquaticPlant, that tries to extend the sealed class Plant, without adding it to the permits clause of the class Plant. As shown in the following gif, this is a compilation error:

Revisiting processing of Plant types in class Gardner

After creating a sealed hierarchy, you will be able to process an instance from the hierarchy in a precise way, and won’t need to deal with any unknown implementations. The process method in class Gardner will work with no chance of running the else clause. However, the syntax of the ifelse construct will still need you to define the else part (this may change in a future Java version).

Type-test-patterns, introduced with Pattern Matching for instanceof in Java 14, might be added to the switch expressions in future Java versions. With the enhanced switch expressions, you are able to work with an exhaustive list of extended types. This could let you eliminate the definition of code to execute for an unmatched Plant type passed to the method processInAFutureJavaVersion():

// This code doesn't work in Java 16.
// It would work in a future Java version after the addition of 
// type-test-pattern to the switch expressions
int processInAFutureJavaVersion(Plant plant) {
   return switch (plant) {
       case Cucumber c -> c.harvestCucumber();
       case Climber cl -> cl.sowClimber();
       case Herb h -> h.sellHerb();
       case Shrub s -> s.pruneShrub();
   }
}

Package and module restrictions

Sealed classes and their implementations can’t span across multiple Java modules.

If a sealed base class is declared in a named Java module, all its implementations must be defined in the same module. However, they can appear in different packages.

For a sealed class declared in an unnamed Java module, all its implementations must be defined in the same package.

Rules for base and extended classes

The classes that extend a sealed class must either be final, non-sealed, or sealed. A final class prohibits further extension. A non-sealed class allows other classes to extend it. And a sealed subclass must follow the same set of rules as the parent base class – it could be extended by an explicit list of other classes.

Abstract sealed base class

A sealed class can be abstract too. The extended classes could be defined as abstract or concrete classes. Here’s the modified code which adds an abstract method grow() to the class Plant:

sealed abstract public class Plant permits Herb, Shrub, Climber {
   abstract void grow();
}

public final class Shrub extends Plant {
   @Override
   void grow() {
   }
}

public non-sealed abstract class Herb extends Plant {}

public sealed class Climber extends Plant permits Cucumber{
   @Override
   void grow() {
   }
}

final class Cucumber extends Climber {}

Implicit subclasses

If you define a sealed class and its derived classes in the same source file, you can omit the contextual keyword permits and the name of the derived classes that are included in the declaration of a sealed class. In this case, the compiler can infer the hierarchy.

Sealed interfaces

Unlike classes, interfaces can not define constructors. Before the introduction of sealed classes, a public class could define a private or package-private constructor to limit its extensibility, but interfaces couldn’t do that.

A sealed interface allows you to explicitly specify the interfaces that can extend it and the classes that can implement it. It follows rules similar to sealed classes.

However, since you can’t declare an interface using the modifier final – because doing so would clash with its purpose, as interfaces are meant to be implemented – an inheriting interface can be declared using either sealed or non-sealed modifiers. The permits clause of an interface declaration lists the classes that can directly implement a sealed interface and interfaces that can extend it. An implementing class can be either final, sealed, or non-sealed:

Here’s the code for reference:

sealed public interface Move permits Athlete, Jump, Kick {
}

final class Athlete implements Move {}
non-sealed interface Jump extends Move {}
sealed interface Kick extends Move permits Karate {}

final class Karate implements Kick {}

Sealed classes and Java API

Soon, you might see the addition of sealed classes and interfaces to the core Java API too. Check out the API Note for the interface ConstantDesc from the package java.lang.constant:

Also, class java.lang.Class adds two methods getPermittedSubclasses()
and isSealed() for working with the sealed types. You can also use them to enumerate the complete sealed hierarchy at runtime, which can be useful.

Stronger code analysis with closed list of subclasses

With sealed classes and interfaces, you can have an explicit list of inheritors that is known to the compiler, IDE and the runtime (via reflection). This closed list of subclasses makes the code analysis more powerful.

For example, consider the following completely sealed hierarchy of WritingDevice (which doesn’t have non-sealed subtypes):

interface Erasable {}

sealed class WritingDevice permits Pen, Pencil {}
final class Pencil extends WritingDevice {}
sealed class Pen extends WritingDevice permits Marker {}
final class Marker extends Pen {}

Now, instanceof and casts can check the complete hierarchy statically. Code on line1 and line2 are compilation errors. The compiler checks all the inheritors from the permits list and finds that no one of them implements the Erasable or the CharSequence interface:

class UseWritingDevice {
   static void write(WritingDevice pen) {
       if (pen instanceof Erasable) {                   // line1
       }
       CharSequence charSequence = ((CharSequence) pen);// line2
   }
}

The following gif demonstrates it in IntelliJ IDEA:

Let’s now move on and look at another next language feature in Java 16 – Pattern Matching for instanceof.

Pattern Matching for instanceof

Many Java developers use the instanceof operator to check whether a given reference variable is of a certain type. They compare a reference variable to a type by using the instanceof operator. If the result is true, the next obvious step is to explicitly cast it to the type they compared it with to access its members. These steps have an obvious repetition here, like compare-ifResultTrue-cast.

Here’s an example of code that can be commonly found in code bases:

void outputValueInUppercase(Object obj) {
   if (obj instanceof String) {
       String s = (String) obj;
       System.out.println(s.toUpperCase());
   }
}

Pattern Matching for instanceof removes the repetition by introducing a pattern variable with the instanceof operator. If the instanceof condition is true, the pattern variable binds to the variable being compared, which prevents the need to define an additional variable or for explicit casting to use its members.
In IntelliJ IDEA, you can invoke context-sensitive actions on the variable s (by using Alt+Enter or by clicking the light bulb icon) and select Replace s with pattern variable to use pattern matching:

This modification introduces the pattern variable s (on line 5), which appears right after type String. This saves you from either defining a new variable or explicitly casting it to String before you could call the method toUpperCase() on it.

Until Java 15, pattern variables were final local variables, declared and defined at the same place. However, with Java 16, pattern variables are no longer final.

Scope of pattern variable

The scope of the pattern variable is limited. If you try to access it in the else block, you’ll receive an error.

Though it might be confusing, if the class PatternMatching defines an instance or static variable with the same name as the pattern variable (s), the preceding code will compile. In this case, s in the else block will not refer to the pattern variable introduced in the if block:

Simplifying conditional expressions

The simplicity of pattern matching might be deceptive. Here’s an example of how developers usually override the equals() method for a class, in this case, Monitor, with two fields – model (String value) and price (double value):

public class Monitor {
  String model;
  double price;
  @Override
  public boolean equals(Object o) {
      if (o instanceof Monitor) {
          Monitor other = (Monitor) o;
          if (model.equals(other.model) && price == other.price) {
              return true;
          }
      }
      return false;
  }
}

This is how the equals() method could be simplified by using pattern matching for instanceof and the further simplification of if statements:

Pattern matching and out-of-the-box support for existing intentions in IntelliJ IDEA

The real strength of Pattern Matching for instanceof is in how it can be used with various other intentions. For example, the following code merges if statements, introduces a pattern variable, and inlines a variable:

Simplifying usages of multiple instanceof in a code block

To look for places where you can use Pattern Matching for instanceof, spot usages of the instanceof operator, and explicit casting of variables. For instance, the following example has multiple occurrences of the instanceof operator with explicit casting. Let’s simplify it:

Inlining of pattern variables

If you don’t need the pattern variables, you can inline them with Inline pattern variable refactoring by using the shortcut Ctrl+Alt+N on Win and Linux/ ⌥⌘N on macOS. After you do so, all its usages would be replaced with explicit casting.

Automatic pattern introduction on ‘extract variable’ refactoring

If you extract a variable from a cast expression, and IntelliJ IDEA detects that there were instanceof checks made before, then instead of creating a new local variable it just converts the instanceof to the pattern.

Preview Features

In their second preview, sealed classes continue to be a preview feature in Java 16.

With Java’s new release cadence of six months, new language features are released as preview features. They may be reintroduced in later Java versions in the second or third preview, with or without changes. Once they are stable enough, they may be added to Java as a standard language feature.

Preview language features are complete but not permanent, which essentially means that these features are ready to be used by developers, although their finer details could change in future Java releases depending on developer feedback. Unlike an API, language features can’t be deprecated in the future. So, if you have feedback about any of the preview language features, feel free to share it on the JDK mailing list (free registration required).

Because of how these features work, IntelliJ IDEA is committed to only supporting preview features for the current JDK. Preview language features can change across Java versions, until they are dropped or added as a standard language feature. Code that uses a preview language feature from an older release of the Java SE Platform might not compile or run on a newer release. For example, Switch Expressions in Java 12 were released with the usage of break to return a value from its branch, which was later changed to yield. Support for using break to return a value from Switch Expressions has already been dropped in IntelliJ IDEA.

Summary

IntelliJ IDEA is not only committed to supporting new Java features, but also to ensuring that our existing intentions and inspections work with them.

IntelliJ IDEA 2021.1 supports all the new language features from Java 16. Try out Sealed classes and interfaces, Records, and Pattern Matching for instanceof today.

We love to hear from our users. Don’t forget to submit your feedback regarding the support for these features in IntelliJ IDEA.

Happy Developing!

image description