Java

Java 20 and IntelliJ IDEA

Read this post in other languages:

I’ve been writing about the new Java releases for a while now (since Java 10) and I like how we developers get to know about and use the new Java features every six months.

Compared to some of its previous releases, Java 20 adds relatively fewer features. It introduces Scoped Values as an incubating API to support virtual threads by enabling sharing of immutable data within and across threads. In its second preview, Record Patterns improves support for generic Record Patterns and lets you use Record Patterns in enhanced for statements. In its fourth preview, Pattern Matching for switch improves its usage when working with exhaustive switch, simplified switch labels and inferring type arguments for generic Record Patterns.

In its second preview in Java 20, the Foreign Function and Memory API continues to improve its capabilities to enable Java code to talk with code and data outside of JVM. Virtual threads, lightweight threads that would revolutionize how you create multithreaded applications, supported by Structured Concurrency, are in another preview in the latest Java release. The Vector API, currently in its fifth preview, helps work with vector computations in your code.

In this blog post, I’ll cover IntelliJ IDEA’s support with the language features like Record Patterns and Pattern Matching for switch, specifically the most interesting changes like improved support with type inference for generic Record Patterns, exhaustive switch statements and expressions, and Record Patterns in enhanced for headers.

Let’s get started.

IntelliJ IDEA Configuration

Support for Java 20 is available in IntelliJ IDEA 2023.1. More support is on the way in future IntelliJ IDEA releases.

To use new language features like Record Patterns and Pattern Matching for switch from Java 20, go to ProjectSettings | Project, set the Project SDK to 20 and set Project language level to ’20 (Preview) – Record patterns (second preview), Pattern Matching for switch (fourth 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 – 20 (Preview) – Record patterns (second preview), Pattern Matching for switch (fourth 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.
Let’s start with a quick refresher on Record Patterns that was introduced in Java 19.

Record Patterns

An obvious action you’d like to perform with record instances is to extract the values of its components so that you can use those values in your applications. This is exactly what Record Patterns do.

Let’s start with a quick refresher on what Record Patterns are and why you need them.

Quick recap of Record Patterns

Records offer a simple and a concise way to create transparent carriers for your data. They enable you to aggregate multiple values (also referred to as components) together. On the other hand, Record Patterns disaggregates a record instance into its components, so that you could use the value of the components with ease.

For an example, for the following records:

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) { }

The following code demonstrates how you can use a Record Pattern with the instanceof operator and define concise and clear code to disaggregate the components of a record instance into a set of pattern variables:

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;
}

If you are completely new to Record Patterns, I’d recommend you to check out my blog post Java 19 and IntelliJ IDEA that covers Record Patterns in detail – what they are, why and how you should use them. In this post, I’ll cover the changes in Record Patterns from Java 19 to Java 20.

Java 20 dropped usage of the named Record Patterns initially added in Java 19. Java 20 also improves support for interfering the type arguments of generic records, and enables you to use Record Patterns in the header of an enhanced for loop.

Type inference of record components

Java 19 supported the inference of the record components-you could use var instead of using the explicit type of a record component. Revisit the example on Record Pattern from the preceding section and note the usage of 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 applicable to all the local variables defined using var, IntelliJ IDEA can display the explicit type of a variable defined using the reserved keyword var:

Generic Record Patterns

Java 20 supports inference of type arguments of generic Record Patterns. For example, let us understand this with an example of gifts, like a WristWatch, or, say, a Book, that you might want to send to your friends.

Assume the following definitions of non-generic classes Book, and WristWatch and a generic record Gift:

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

What happens when your friend receives your gift and tries to unwrap it? Let’s assume they call the unwrap method as defined below. In the following code, method unwrap uses a Record Pattern Gift<wristwatch> (var watch). Since this pattern has already specified the generic type WristWatch with the record name Gift, the pattern variable watch can be inferred as type WristWatch:

void unwrap(Gift<wristwatch> obj) {
   if (obj instanceof Gift<wristwatch> (var watch)) {
       watch.setAlarm(LocalTime.of(10, 25));
   }
}

The following examples wouldn’t have worked in Java 19, but they okay with Java 20:

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

void unwrapAndUseGift(Gift<WristWatch> obj) {
    if (obj instanceof Gift(var gift)) {
        gift.setAlarmTime(LocalTime.now());
    }
}

void birthdayGift(Gift<DiamondStudded<WristWatch>> gift) {
    if (gift instanceof Gift<DiamondStudded<WristWatch>>(DiamondStudded(var personalizedGift))) {
        System.out.println(personalizedGift);
    }
}

void performanceBonus(Gift<DiamondStudded<WristWatch>> personalizedGift) {
    if (personalizedGift instanceof Gift(DiamondStudded(var actualGift))) {
        System.out.println("Wrist watch" + actualGift);
    }
}

Let us see how this changes the working of exhaustive switch constructs in the next section.

Exhaustive switch constructs with generic records

Before we start, let’s get the basics correct. The following image shows what a selector expression refers to in a switch statement or a switch expression (a variable or an expression you pass to the switch construct):

The syntax of the switch statements and switch expressions mandates that it must be exhaustive when you try to match the value of a selector expression with a Type Pattern or a Record Pattern in its case labels. In other words, the selector expression must match at least one of the values defined in the case labels.

For types that don’t have a definite count of its subtypes (a fixed count of how many other classes extend it), such as the type Object, you could either define a default case label or the Object type itself as one of the case labels to make it an exhaustive switch construct. The following are valid examples of exhaustive switch expression and exhaustive switch statement:

String exhaustiveSwitchExpression(Object obj) {
   return switch (obj) {
       case String s -> "String";
       case Apple apple -> "Apple";
       default -> "everything else";
   };
}

void exhaustiveSwitchStatement(Object obj) {
   switch (obj) {
       case String s -> System.out.println("String");
       case Apple apple -> System.out.println("Apple");
       case Object object -> System.out.println("everything else");
   };
}

Some types, like sealed classes, have definitive subtypes, so you might not need a default label to define exhaustive switch statements or switch expressions. Here is an example:

sealed interface HighProtein permits Egg, Cheese {}
final class Egg implements HighProtein {}
final class Cheese implements HighProtein {}
 
int processHighProtein(HighProtein protein) {
   return switch (protein) {
       case Egg egg -> 2;
       case Cheese cheese -> 10;
   };
}

However, if you define the interface HighProtein as a regular interface, I mean an interface that is not sealed, the preceding code won’t be okay to compile.

Now, let us talk about how to define exhaustive case labels if the selector expression is a generic record, which is being matched against Record Patterns. Following is an example of a generic class Apple that is extended by another class, HimalayanApple, a sealed interface HighProtein that is being implemented by classes Egg and Cheese, and a generic record Dish that accepts a type parameter and defines two components of that type:

public class Apple {}
public class HimalayanApple extends Apple{}

sealed public interface HighProtein permits Egg, Cheese {}
public final class Egg implements HighProtein {}
public final class Cheese implements HighProtein {}

public record Dish<T> (T x, T y){}

Let’s talk about the multiple combinations that will work or won’t work when you switch over the instances of this generic record class Dish.

Let’s start with a method, say, orderAppleDish, that accepts a method parameter of type Dish<Apple> appleDish and switches over it, matching it against Record Patterns. Do you think the following code is okay:

    int orderAppleDish(Dish<Apple> appleDish) {
        return switch (appleDish) {
            case Dish<Apple>(Apple apple, HimalayanApple himalayanApple) -> 1;
            case Dish<Apple>(HimalayanApple himalayanApple, Apple apple) -> 2;
        };
    }

Since class HimalayanApple extends class Apple, following two combinations are required to make the preceding switch expression an exhaustive switch:

Apple, Apple
HimalayanApple, HimalayanApple

The following gif shows how IntelliJ IDEA can detect that the preceding code is not okay and helps you navigate your code and correct errors:

Following is the final code from the preceding gif for your reference:

class Apple {}
class HimalayanApple extends Apple{}
record Dish<T> (T ingredient1, T ingredient2) {}

public class FoodOrder {

    int orderAppleDish(Dish<Apple> appleDish) {
        return switch (appleDish) {
            case Dish<Apple>(HimalayanApple apple1, HimalayanApple apple2) -> 4;
            case Dish<Apple>(Apple apple, HimalayanApple himalayanApple) -> 1;
            case Dish<Apple>(HimalayanApple himalayanApple, Apple apple) -> 2;
            case Dish<Apple>(Apple apple1, Apple apple2) -> 3;
        };
    }

}

Let us work with another example that uses the following sealed interface and its implementations with the generic record Dish:

sealed public interface HighProtein permits Egg, Cheese {}
public final class Egg implements HighProtein {}
public final class Cheese implements HighProtein {}

public record Dish<T> (T x, T y){}

Imagine a person orders food, passing an instance of Dish with the interface HighProtein as its type parameter. In this case the following switch expression is exhaustive:

Here’s the code for your reference:

public class FoodOrder {

    int orderHighProteinDish(Dish<HighProtein> proteinDish) {
        return switch (proteinDish) {
            case Dish<HighProtein>(HighProtein protein, Egg egg) -> 1;
            case Dish<HighProtein>(HighProtein protein, Cheese cheese) -> 2;
        };
    }

}

Since HighProtein is a sealed interface, the first case label in the following switch construct covers the possibility of being passed either Egg or Cheese as the second value to the record Dish. So, even though it defines just three case labels, it is an exhaustive switch expression:

public class FoodOrder {

    int orderHighProteinDish(Dish<HighProtein> proteinDish) {
        return switch (proteinDish) {
            case Dish<HighProtein>(Egg protein, HighProtein highProtein) -> 1;
            case Dish<HighProtein>(Cheese cheese, Egg egg) -> 2;
            case Dish<HighProtein>(Cheese cheese1, Cheese cheese2) -> 4;
        };
    }

}

In this last example on exhaustive switch and generic records, the following initial set of case labels in the switch expression is not exhaustive because it doesn’t include the possibility of handling a high protein dish with the first ingredient as an instance of HighProtein and the second value as an instance of Egg:

    int orderHighProteinDish(Dish<HighProtein> proteinDish) {
        return switch (proteinDish) {
            case Dish<HighProtein>(Egg protein, Cheese cheese) -> 1;
            case Dish<HighProtein>(Cheese cheese, Egg egg) -> 2;
            case Dish<HighProtein>(HighProtein highProtein, Cheese cheese) -> 3;
        };
    }

Let’s add another case label to the preceding code to make it an exhaustive switch expression:

            case Dish<HighProtein>(HighProtein highProtein, Egg egg) -> 10;

Using Record Patterns with enhanced for statements

With Java 20, you can use Record Patterns in the header of the enhanced for loops. However, a switch statement or an expression, in which you could assign multiple case labels, the single Record Patterns that you use in an enhanced for loop header must match all the values that you iterate over in the for loop. Otherwise, your code will throw runtime exceptions.

Here is an example of records Point and Triangle and an enhanced for loop that uses Record Patterns in its header to iterate through the list of Triangle instances:

record Point (int x, int y) { }
record Triangle(Point pointA, Point pointB, Point PointC) { }

long addLowerRightCoordinates(List<Triangle> triangles) {
    long sum = 0;
    for (Triangle(Point a, Point b, Point (int x, int y)) : triangles) {
        sum += x + y;
    }
    return sum;
}

Here are some of the incorrect examples:

public class Test {

    sealed interface UpperWithPermit permits PermittedRecord {}
    record PermittedRecord(int x) implements UpperWithPermit {}
    interface Upper {}
    record Record(List<String> x) implements Upper {}

    void test1(List<Upper> lists) {
        for (Record(List<String> x) : lists) {} 
    }

    void test2(List<? super UpperWithPermit> lists) {
        for (PermittedRecord(var x) : lists) {} 
    }

    void test3(List<? super PermittedRecord> lists) {
        for (PermittedRecord(var x) : lists) {} 
    }

    void test4(List lists) {
        for (PermittedRecord(var x) : lists) {} 
    }

    void test5(List<?> lists) {
        for (PermittedRecord(var x) : lists) {} 
    }
}

Pattern Matching for switch

With Java 20, Pattern Matching for switch is in its fourth preview. If you are completely new to Pattern Matching, I’d recommend you to check out this link to start with pattern matching with instanceof. If you are new to pattern matching with switch, please check out this link.

A few changes have been added to this feature in Java 20. When used with an enum class, pattern matching for switch now throws MatchException rather than throwing ImcompatibleClassChangeError, if the exhaustive switch statement or switch expression, can’t find a matching label at runtime. Another change with this feature in Java 20 is related to the inference of type parameters for generic Record Patterns in case labels. I’ve already covered this feature in this blog post under the section Exhaustive switch constructs with generic records.

Summary

IntelliJ IDEA continues to reduce the cognitive load for developers to use the latest Java features. IntelliJ IDEA 2023.1 supports the changes added in Java 20 to the language features like ‘Pattern Matching for switch’ and ‘Record Patterns’. The most interesting changes in these features are the ability to use Record Patterns in enhanced for headers and improvements to the type inference of type arguments for generic Record Patterns.

Happy Coding.

image description