Early Access Program Features News Releases

Java 14 and IntelliJ IDEA

Java 14 packs a lot of Java language features for you. It includes Records and Pattern Matching for instanceof as preview language features, and Text Blocks in the second preview. It also adds Switch Expressions as a Standard language Feature.

With Records, you get a compact syntax for declaring data classes. By using just a single line of code, you can model your data with ease. Don’t worry – the compiler does the heavy lifting to add the methods required to make the class work for you.

The usage of the instanceof operator is simplified with Pattern Matching for instanceof. After introducing a binding variable, you don’t need additional variables or explicit casting, which also makes your code safe and concise to write and read.

Released as a preview language feature in Java 13, Text Blocks add new escape sequences to improve the processing of whitespaces in multi-line string values. Switch Expressions have been added as a standard language feature in Java 14, with no changes from its previous version (it was introduced in Java 12 as a preview language feature).

In this article, I will cover what records are, talk about pattern matching for instanceof, new features in Text Blocks, and explain how to use them all in IntelliJ IDEA. Let’s get started!

Records

Records introduce a new type declaration in Java which simplifies the task of modeling your data as (shallowly immutable) data. Though it helps cut down significantly on the boilerplate code, this 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 2020.1, select ‘Record (Preview Feature)’ in the ‘New Java Class’ dialog box. Fill in the name and you are good to go.

IntelliJ IDEA configuration

Java 14 features are supported in IntelliJ IDEA 2020.1, which will be released in April 2020. The exciting news is that you can participate in IntelliJ IDEA’s EAP (Early Access Program) to download its preview builds for free and try out the Java 14 features today.

Configure IntelliJ IDEA 2020.1 to use JDK 14 for Project SDK and choose the Project language level as ‘14 (Preview) – Records, patterns, text blocks’ for your Project and Modules settings.

Starting with IntelliJ IDEA 2020.1, you can also download the JDK from within IntelliJ IDEA and configure it. All JDK versions from various providers might not be visible in this dialog box. This is due to the various rules that apply to them – you need to explicitly accept certain conditions before using them. In this case, please download the JDK from the provider’s website and then configure IntelliJ IDEA to use it.

Implicit members added to a record

The compilation process creates a full-blown class, adding instance variables and methods to a record. We can open the Person.class file in IntelliJ IDEA to view its decompiled version (in the project window, scroll to folder ‘out’ and click on Person.class to open it in the editor window):

As you can see in the decompiled code for the record Person, the compiler (re)defines it 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.

Using records

You can instantiate a record by using the operator new. The default constructor of a record accepts values for its components in the order of their definition. You can also call the implicit methods that are generated automatically for a record. Here’s an example:

public class Java14 {
   public static void main(String[] args) {
       var shreya = new Person("Shreya", 20);
       var harry = new Person("Harry", 45);

       System.out.println(shreya);
       System.out.println(shreya.equals(harry));
       System.out.println(shreya.name());
       System.out.println(shreya.age());
       System.out.println(shreya.hashCode());
   }
}

Don’t miss the parameter hints when you are passing values to the constructor in IntelliJ IDEA:

As you can notice from the output, the toString() displays the name of the record, its components, and their values. Also, no setter methods are defined by default for the components of a record (since records are supposed to be immutable). If you try to do that yourself, you’ll get a compilation error (because final variables can’t be reassigned).

What you can and can’t add to a record

You can add static fields, and instance or static methods to a record, if you need them:

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

However, since a record is a final class, you can’t define it as an abstract class. Additionally, it can’t extend another class (since it implicitly extends the java.lang.Record class). But there are no restrictions on its implementing interfaces.

Even though you can add static fields to a record, you can’t add instance variables to it. This is because a record is supposed to limit the instance members to the components it defines in the record definition.

When you invoke inspection ‘Generate’ (Alt + Insert for Win and Linux/ ^N for macOS) to generate code for insertion in a record, you’ll notice that you don’t get an option to generate setters. This is intentional because a record is supposed to be (shallowly) immutable.

A record has many similarities to the enums in Java – they could be considered siblings. On compilation, an enum gets multiple methods. It implicitly extends java.lang.Enum, and can’t be extended externally. Like a record, an enum can also implement interfaces.

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 to, say, validate the parameter values before moving forward with their assignment.

IntelliJ IDEA lets you insert a compact, canonical, or custom constructor in a record. For your quick information, a compact constructor doesn’t have a parameter list, not even parentheses. A canonical constructor is the one whose signature matches with the record’s state description. 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 the full boilerplate code.

Let’s see how you can insert a compact constructor using the Alt + Insert shortcut in IntelliJ IDEA, adding validation code to it. To verify this code, you can run the application, Java14, in several ways: the menu option ‘Run’, ‘Find Action’, or my preferred option – pressing Ctrl + Ctrl (works on all OS) and selecting the configuration to run:

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, and vice versa:

While trying out a new feature, you might edit code multiple times, resulting in redundant code in your codebase. For example, after multiple edits to your compact or canonical constructor in a record, you might end up with a constructor that doesn’t include any additional code, such as validation of parameters.

If this happens, IntelliJ IDEA can detect such constructors and offer to delete them for you:

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 Java14 {
    public static void main(String[] args) {
    	Parcel<Table> parcel = new Parcel<>(new Table(), 200, 100, 55, 136.88);
    	System.out.println(parcel);
    }
}

Adding annotations to record components

You can add an appropriate annotation to the components of a record. For example, for the record Person defined earlier in this post, you can add a @NotNull annotation to its component name. This also adds the annotation to its (canonical) constructor:

Reading and Writing Records to a File

Writing a record to a file and reading it using the Java File I/O API is simple. Let your record implement the Serializable interface and you are good to go. Here’s the modified version of the record Person, which you can write to and read from a file:

package com.jetbrains;
public record Person(String name, int age) implements Serializable {}
public class Java14 {
    public static void main(String[] args) {
    	Person javaDev = new Person("Java Dev", 25);
    	writeToFile(javaDev, "Java14-records");
        System.out.println(readFromFile("Java14-records"));
    }
    static void writeToFile(Person obj, String path) {
    	try (ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream(path))){
        	oos.writeObject(obj);
    	} catch (IOException e) {
        	e.printStackTrace();
        }
    }
    static Person readFromFile(String path) {
    	Person result = null;
    	try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path))){
        	result = (Person) 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 2020.1 introduces a simplified approach to 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‘) and 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 becomes confusing as developers start using records.

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 this redundant code 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 2020.1, you can invoke context-sensitive actions on the variable s (by using Alt + Enter or by clicking the yellow light bulb) and select ‘Replace ‘s’ with pattern variable’ to use pattern matching in Java 14:

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.

Pattern variables are final local variables that are declared and defined at the same place. With other local variables, it is possible to declare them and defer their assignment. You can’t assign another value to a pattern variable since it is implicitly final.

IntelliJ IDEA 2020.1 supports all the new features of Java 14, including Pattern Matching. Right now, you can participate in its Early Access Program (EAP) and download the Ultimate Edition to preview all the new features – for free.

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 would 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 instance variables – 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 this 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

Pattern Matching with instanceof can be used at multiple places to simplify your code. Various existing intentions work out of the box with Pattern Matching in IntelliJ IDEA, such as inverting, merging, splitting, simplifying if statements, removing redundant else blocks, simplifying conditional expressions, and many more.

The following code removes redundant casting by using Pattern Matching for instanceof and then simplifies its if statements:

The real strength of Pattern Matching for instanceof lies 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 check before, then instead of creating a new local variable it just converts the instanceof to the pattern.

Text Blocks

The Text Blocks feature is in its second preview in Java 14. It introduces two new escape sequences, \ and \s, to exercise finer control of how new-line characters and end-of-line spaces are handled in text blocks (multiline strings).

I’m only going to cover how Text Blocks have changed in Java 14 in this blog. For the complete coverage on what Text Blocks are and how IntelliJ IDEA supports them, please refer to our previous blog post on Java 13 and IntelliJ IDEA.

Multi-line string values in text blocks include newline characters, which can be suppressed by using \ at the end of a line. Also, the spaces at the end of a line in text blocks are removed by default. To insert them, you can use the escape sequence \s:

Switch Expressions

Switch Expressions were first released as a preview language feature with Java 12. After being released in the second preview in Java 13, Switch expressions are being added as a standard feature in Java 14.

There have been no changes in Switch Expressions since its last release in Java 13. For details on how it is supported in IntelliJ IDEA, please see our previous blog post.

Preview Language Features

Records and Pattern Matching with instanceof have been released as a preview language feature in Java 14. With Java’s new release cadence of six months, new language features are released as preview features. They are complete but not permanent. A preview language feature essentially means that this feature is ready to be used by developers, although its finer details could change in a future Java release depending on the developers’ feedback. Unlike an API, language features can’t be deprecated in the future. So, if you have any feedback on text blocks, feel free to share it on the JDK mailing list (free registration required).

IntelliJ IDEA is committed to only supporting preview features for the current JDK. This is due to how these features work. Preview language features can change across Java versions, until they are dropped or added as a standard language feature. Code which 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 was released with usage of break to return value from its branch, which was later changed to yield. Support for using break to return a value from Switch Expressions is already dropped in IntelliJ IDEA.

Looking ahead

Work is in progress to derive deconstruction patterns for records. It should present interesting use cases with ‘Sealed Types’ and ‘Pattern Matching’. A future version of Java should also see the addition of methods to the Object class to enable reflections to work with Records. In a future version of Java, you might soon be able to use Pattern Matching with Switch Expression.

Java is moving forward fast, and IntelliJ IDEA 2020.1 supports all the new language features in Java 14. Try out Records, Pattern Matching for instanceof, Text Blocks, and Switch Expressions today.

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

Happy Developing!

image description