IntelliJ IDEA

Java 19 and IntelliJ IDEA

Read this post in other languages:

Java is more vibrant than ever before. Its shorter release cadence lets us all try out its new language or platform features, every six months. IntelliJ IDEA helps us to discover and use these new features, without making them overwhelming for us.

In this blog post, I will limit the coverage of Java 19 to its language features – Record Patterns and Pattern Matching for switch (third preview). I didn’t cover other Java 19 features like Virtual threads, a preview API, intentionally. IntelliJ IDEA supports basic syntax highlighting for Virtual Threads and the team is working on adding support for the Virtual Threads in its debugger and profiler.

Record Patterns simplify access to components of a record. Compare record patterns with deconstruction of a record – the ability to extract the values of components of a record to a set of variables, when an instance matches the structure of a record. Initially this might not seem a very big deal. However, as you’ll combine it with other language features like pattern matching for switch and sealed classes, you’ll be amazed by what you can achieve.

Pattern matching for switch adds patterns to the case labels in the switch statements and switch expressions. The type of the selector expression that can be used with a switch is expanded to any reference value. Also, case labels are no longer limited to constant values. It also helps replace if-else statement chains with switch, improving code readability. In this blog post, I’ll cover the changes introduced in the third preview of Pattern Matching for switch.

Let’s start with how to configure IntelliJ IDEA to use Java 19 features.

IntelliJ IDEA Configuration

Support for Java 19 is available in IntelliJ IDEA 2022.3. More support is on the way in future IntelliJ IDEA releases. To use pattern matching for switch with Java 19, go to Project Settings | Project, set the Project SDK to 19 and set Project language level to ‘19 (Preview) – Record patterns, pattern matching for switch (third preview)’:

You can use any version of the JDK that has already been downloaded on your system, or download another version by clicking on ‘Edit’ and then selecting ‘Add SDK >’, followed by ‘Download JDK…’. You can choose the JDK version to download from a list of vendors.

On the Modules tab, ensure the same language level is selected for the modules – 19 (Preview) – Record patterns, pattern matching for switch (third preview):

Once you select this, you might see the following pop-up which informs you that IntelliJ IDEA might discontinue the support for the Java preview language features in its next versions. Since a preview feature is not permanent (yet), and it is possible that it could change (or even be dropped) in a future Java release.
Now, let’s cover what are Record Patterns, its benefits and demonstrate it using hands-on examples.

Why do you need Record Patterns?

Data is at the heart of most applications. Often you use applications that find data for you, or process it in a way that helps you with decision making. Of course, this isn’t feasible without the application being able to store, retrieve or process its data.

In one of recent Java releases (version 16), Records was added to the Java language to make it easy for developers to work with data. Records greatly simplify how you model your immutable data. They act as transparent carriers or wrappers for your data. By using just a single line of code, you can define a record and its components.

For example, the following single line of code creates a new record Person that can store String value for its component name and an int value for age:

record Person (String name, int age) { }

A record saves you from writing boilerplate code. A record implicitly generates default implementation for its constructor, accessor methods for its components, utility methods like toString, equals and hashCode. When you use records as a wrapper for your data, you’ll most likely need to unwrap it to access its components. For example, when you have an instance of record Person, you might want to examine its age component to figure out if the person it represents is eligible for voting or not. Here’s a method, say, isEligibleToVote, that can accomplish this behavior:

boolean isEligibleToVote(Object obj) {
   if (obj instanceof Person person) {
       return person.age() >= 18;
   }
   return false;
}

The preceding example uses pattern matching for instanceof, which declares a pattern variable person so you don’t need to create a local variable to cast obj to Person.

Record patterns take it a step further. It not only compares the instance with the record type Person, it also declares variables for the components of a record, so that you don’t need to either define local variables or use pattern variables to access the components of a record. This is possible because the compiler knows the exact count and type of the components of a record.

Let’s rewrite the preceding method using a record pattern. When you use a record type with either the instanceof operator or in a switch case label, IntelliJ IDEA can detect it and suggests you use a Record Pattern:

Here’s the modified code for your reference:

boolean isEligibleToVote(Object obj) {
   if (obj instanceof Person(String name, int age)) {
       return age >= 18;
   }
   return false;
}

In the preceding code, the record pattern Person(String name, int age) only seems to enable using the variable age instead of person.age(). However, as you’ll read this blog post, you’ll realize that record patterns simplify the intent of the code and also help in creating concise data processing code.

If you haven’t worked with records before, or want to know more about what Records, or how IntelliJ IDEA supports them, please refer to my previous blog on Records.

Named Record Pattern

A record pattern can be followed by a record pattern variable. If so, the record pattern is referred to as a Named Record Pattern (Though not confirmed, support for Named Record Patterns might be dropped in the second preview of record patterns in Java 20).

A record pattern can also define pattern variables for its components. If you use a named record pattern and try to access one of its components using the record pattern variable, IntelliJ IDEA can nudge you to use the pattern variable for its component. You’ll see such code highlighted with a yellow background. You can use Alt+Enter to view this suggestion, and accept the suggestion to modify the code:

Record Patterns and nulls

Let’s revisit the method isEligibleToVote example from the preceding section. What happens if a null value is passed to following method:

boolean isEligibleToVote(Object obj) {
   if (obj instanceof Person(String name, int age)) {
       return age >= 18;
   }
   return false;
}

Since null isn’t an instance of the Record Pattern Person(String name, int age), the instanceof operator returns false, and the pattern variables name and age aren’t initialized. This is convenient since a Record Pattern handles nulls and you don’t need to define a not null check.

But, if the value for the component name is null, then the pattern will be matched.

Nested Record Patterns – concise code and clear intent

A record that defines another record as its component is common. Here’s an example:

record Name       (String fName, String lName) { }
record PhoneNumber(String areaCode, String number) { }
record Country    (String countryCode, String countryName) { }
record Passenger  (Name name, 
                   PhoneNumber phoneNumber, 
                   Country from, 
                   Country destination) { }

Without a record pattern, which can check for null component values, you’ll need a couple of null check operations to process the component values of, say, fName and countryCode, of a record Passenger, as follows:

boolean checkFirstNameAndCountryCode (Object obj) {
   if (obj != null) {
       if (obj instanceof Passenger passenger) {
           Name name = null;
           Country destination = null;
 
           if (passenger.name() != null) {
               name = passenger.name();
 
               if (passenger.destination() != null) {
                   destination = passenger.destination();
 
                   String fName = name.fName();
                   String countryCode = destination.countryCode();
 
                   if (fName != null && countryCode != null) {
                       return fName.startsWith("Simo") &&
                              countryCode.equals("PRG");
                   }
               }
           }
       }
   }
   return false;
}

The same behavior can be achieved by nesting Record Patterns, which also makes the intent of the code much clearer. The instanceof check will fail if the record components name and destination are null:

boolean checkFirstNameAndCountryCodeAgain (Object obj) {
   if (obj instanceof Passenger(Name (String fName, String lName),
                                PhoneNumber phoneNumber,
                                Country from,
                                Country (String countryCode, String countryName) )) {
 
       if (fName != null && countryCode != null) {
           return fName.startsWith("Simo") && countryCode.equals("PRG");
       }
   }
   return false;
}

As shown in the preceding example code, you can selectively include the Record Patterns for the components of your main record. For example, the preceding example doesn’t use a Record Pattern for the component from. But it uses a Record Pattern for the record component destination of the main record Passenger. In short, you can control the details you want to be extracted to pattern variables when you define a Record Pattern. This feature can be very useful for data processing intensive applications.

Using var with Record patterns

Let’s revisit the method checkFirstNameAndCountryCodeAgain from the preceding example and define the type of some of the pattern variables as var:

boolean checkFirstNameAndCountryCodeAgain (Object obj) {
   if (obj instanceof Passenger(Name (String fName, var lName),
                                var phoneNumber,
                                Country from,
                                Country (var countryCode, String countryName) )) {
 
       if (fName != null && countryCode != null) {
           return fName.startsWith("Simo") && countryCode.equals("PRG");
       }
   }
   return false;
}

As you can see, you can define the type of some or all of the pattern variables as var. Just in case you are curious what their type is, IntelliJ IDEA can show it to you:

Record patterns and generics

If your record is generic, its record pattern must use a generic type. For example, assume the following definitions of class WristWatch and a generic record Gift:

class WristWatch {}
record Gift<T>(T t) {}

You can use the following method to unwrap an instance of record Gift. You can use either var or WristWatch as a type for the pattern variable watch:

void unwrap(Gift<WristWatch> obj) {
   if (obj instanceof Gift<WristWatch> (var watch)) {
       System.out.println(watch);
   }
}

However, the following code won’t work:

static void cannotUnwap(Gift<object> obj) {
   if (obj instanceof Gift(var s)) {   // won’t compile              
   	//..
   }
}

The next section used record patterns and switch expressions to create a powerful recursive method.

Record patterns, switch expressions and sealed classes

You can create powerful, yet concise and expressive code to process your data by using a combination of record patterns, switch expressions and sealed classes. Here’s an example of a sealed interface TwoDimensional, which is implemented by records Point, Line, Triangle and a Square:

sealed interface TwoDimensional {}
record Point (int x, int y) implements TwoDimensional { }
record Line    ( Point start, 
                 Point end) implements TwoDimensional { }
record Triangle( Point pointA, 
                 Point pointB, 
                 Point PointC) implements TwoDimensional { }
record Square  ( Point pointA, 
                 Point pointB, 
                 Point PointC, 
                 Point pointD) implements TwoDimensional { }

The following method defines a recursive method process that uses a switch construct to return the sum of x and y coordinates of all the points in a two dimensional figure like a Line, Triangle or a Square:

static int process(TwoDimensional twoDim) {
   return switch (twoDim) {
       case Point(int x, int y) -> x + y;
       case Line(Point a, Point b) -> process(a) + process(b);
       case Triangle(Point a, Point b, Point c) -> 
                                 process(a) + process(b) + process(c);
       case Square(Point a, Point b, Point c, Point d) -> 
                                 process(a) + process(b) + process(c) + process(d);
   };
}

IntelliJ IDEA also displays the recursive call icon in the gutter for this method:

Record patterns in Java 20

With the current status ‘Proposed to Target’, the second preview of Record Patterns targeted for Java 20 plans to enhance it. Apart from supporting type inference for arguments of generic record patterns, it also mentions supporting record patterns in enhanced for statements. Pattern matching is definitely changing your everyday coding.

Pattern Matching for switch – third preview

Introduced in Java 17 as a preview language feature, Pattern Matching for switch is in its third preview with Java 19. If you are new to this topic or would like to like to know more about what it is or how IntelliJ IDEA supports it, please refer to this blog post in which I’ve covered it in great detail – starting from what is pattern matching, pattern matching for instanceof and then pattern matching for switch.

In this post, I’ll cover the changes to Pattern Matching for switch from its second preview in Java 18. Let’s start with the first change of using when clause in switch blocks.

Replacing guarded patterns with when in switch blocks

Imagine a set of classes – Pollution, AirPollution, and Deforestation, defined as follows:

class Pollution { }
class AirPollution extends Pollution {
   public int getAQI() {
       return 100;
   }
}
class Deforestation {
   public int getTreeDamage() {
       return 300;
   }
}

With the previous previews of Pattern matching for switch, you could add a condition to a case label by using a guarded pattern, that is, using && to define a condition. The following code uses pattern matching for switch and a guarded pattern && airPol.getAQI() > 200, to further refine the instances of AirPollution for which this switch will return a value of 500:

public class MyEarth {
   int getDamage(Object obj) {
       return switch (obj) {
           case AirPollution airPol && airPol.getAQI() > 200 -> 500;
           case Deforestation def -> def.getTreeDamage();
           case null, default -> -1;
       };
   }
}

In the third preview of Pattern Matching for switch, the guarded pattern has been replaced with when (if you have worked with SQL queries, you’ll be able to relate to it easily). Let’s rewrite the preceding example:

public class MyEarth {
   int getDamage(Object obj) {
       return switch (obj) {
           case AirPollution airPol when airPol.getAQI() > 200 -> 500;
           case Deforestation def -> def.getTreeDamage();
           case null, default -> -1;
       };
   }
}

New inspections – Push down for ‘switch’ expressions

IntelliJ IDEA has added a new inspection – Push down for ‘switch’ expressions, which can help you modify the switch expressions in a way that can be used to further separate computation and its side effects. For example, consider the following code:

void printObject(Object obj) {
   if (obj instanceof String s) {
       System.out.println("String: \"" + s + "\"");
   } else if (obj instanceof Collection<?> c) {
       System.out.println("Collection (size = " + c.size() + ")");
   } else {
       System.out.println("Other object: " + obj);
   }
}

As the first step, apply IntelliJ IDEA’s inspection Replace ‘if’ with ‘switch’, resulting in the following code:

void printObject(Object obj) {
   switch (obj) {
       case String s -> System.out.println("String: \"" + s + "\"");
       case Collection<?> c -> 
            System.out.println("Collection (size = " + c.size() + ")");
       case null, default -> System.out.println("Other object: " + obj);
   }
}

Notice that each switch label includes a call to System.out.println(). Now, you can apply IntelliJ IDEA’s inspection ‘Push down for ‘switch’ expression’, resulting in the following code:

void printObject(Object obj) {
   System.out.println(switch (obj) {
       case String s -> "String: \"" + s + "\"";
       case Collection<?> c -> "Collection (size = " + c.size() + ")";
       case null, default -> "Other object: " + obj;
   });
}

Finally, you might want to extract a variable to separate the computation and side effect, generating the following code:

void printObject(Object obj) {
   final var representation = switch (obj) {
       case String s -> "String: \"" + s + "\"";
       case Collection<?> c -> "Collection (size = " + c.size() + ")";
       case null, default -> "Other object: " + obj;
   };
   System.out.println(representation);
}

The following gif captures all the preceding steps:

Pattern Matching for switch in Java 20

With the current status ‘Proposed to Target’, the fourth preview of Pattern Matching for switch targeted for Java 20 plans to add more changes. Apart from simplifying the grammar for switch labels, it includes multiple other changes.

Preview Features

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 2022.3 supports Record Patterns and enhances its support for Pattern Matching for switch. More support is in the works. IntelliJ IDEA has full support for recent additions to Java like sealed classes and interfaces, records, pattern matching for instanceof, and text blocks.

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