Java 15 and IntelliJ IDEA

Posted on by Mala Gupta

Java 15 introduces a new language feature – sealed classes and interfaces. The language syntax allows you to restrict the classes or interfaces that can extend or implement other classes or interfaces. The goal is to let you define the possible hierarchies in your business domain in a declarative manner, so that it is easier to process them. This language feature is introduced as a preview language feature.

Java 15 also modifies the preview language feature Records, introduced in Java 14, and enhances the interfaces and enums you are used to working with. Pattern Matching for instanceof, introduced as a preview language feature in Java 14, is in its second preview in Java 15, without any changes. Introduced in Java 13, Text Blocks are being added to Java 15 as a standard language feature. There are no changes to Text Blocks from Java 14.

In this article, I will cover all the new and updated language features in Java 15, why you need them, and I’ll show you how to use them in IntelliJ IDEA. Let’s get started.


Sealed classes and interfaces (a preview language feature)

By defining a class as a sealed class, you can explicitly define which other classes can extend it. On the one hand, it lets you reuse a class with inheritance, and on the other hand, it lets you restrict which classes can extend it. But why would you 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 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();
       } else if (plant instanceof Climber) {
           return sowClimber();
       } else if (plant instanceof Herb) {
           return sellHerb();
       } else if (plant instanceof Shrub) {
           return pruneShrub();
       } else {
           System.out.println("Unreachable CODE. Unknown Plant type");
           return 0;
       }
   }

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

The problem code is the assumption that a developer has to deal with in the else part. Though 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 modifier sealed, you can declare a class as a sealed class. A sealed class uses the reserved keyword permits to list the classes that can extend it directly. The subclasses can either be final, non-sealed, or sealed.

The following gif shows how to change the declaration of a regular class to a sealed class and modify the declaration of the classes that extend it:

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, but you can still control who can extend it. In the past, to prevent classes from being extended, developers created package-private classes. However, this also meant that these classes had limited accessibility. This is no longer the case if you use sealed classes.

You can get the permitted subclasses via reflection using the Class.permittedSubclasses() method. This makes it possible to enumerate the complete sealed hierarchy at runtime, which can be useful.

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

IntelliJ IDEA Configuration

Java 15 features are supported in IntelliJ IDEA 2020.2, which was released in July 2020. You can configure it to use Java 15 by selecting the Project SDK as 15 and choosing the ‘Project language level’ as ‘15 (Preview) – Sealed types, records, patterns, local enums and interfaces’ for your Project and Modules settings.

Java 15 will be GA (General Availability) on September 15, 2020, after which you will also be able to download it directly from IntelliJ IDEA. To do so, click on SDKs, under ‘Platform Settings’, then click the ‘+’ sign at the top, choose ‘Download JDK’, then select the Vendor and the version and the directory to download the JDK to.

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 manner and won’t need to deal with any ‘general’ 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 the exhaustive list of extended types. This lets you eliminate the definition of any ‘general code’ to execute for an unmatched Plant type passed to the method processInAFutureJavaVersion:

// This code doesn't work in Java 15.
// 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 modules. If a sealed base class is defined in a named module, all its implementations must be defined in the same module. However, they can appear in different packages.

For a sealed class defined in an unnamed 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.

A sealed class can be abstract too. The extended classes could be defined as abstract or concrete classes.

Let’s modify the set of classes I used in the preceding section and define the class Plant as an abstract class with an abstract method grow(). Since the derived class Herb is a final class, it must implement the method grow(). The non-sealed derived class Shrub is now an abstract class, and it may not implement the method grow(). The sealed derived class Climber implements the abstract method grow(), if it doesn’t need to be defined as an abstract class:

Here’s the modified code for reference:

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

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

non-sealed abstract class Shrub extends Plant {}

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

final class Cucumber extends Climber {}

If you define a sealed class and its derived classes in the same source file, you can omit the modifier 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

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

However, since you can’t declare an interface using 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. Since records, introduced in Java 14, are implicitly final, they don’t need any additional modifiers:

Here’s the code for reference:

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

final class Athlete implements Move {}
record Person(String name, int age) implements Move {}
non-sealed interface Jump extends Move {}
sealed interface Kick extends Move permits Karate {}

final class Karate implements Kick {}

Let’s move on to the next enhancement in Java 15 – the introduction of local records.

Records

A new type of class, Records introduced a compact form to model value objects. A preview language feature in Java 14, Records is in its second preview in Java 15, with a few changes.

If you are new to Records, or if you want to find out how Records are supported in IntelliJ IDEA, please refer to my Java 14 and IntelliJ IDEA blog post. IntelliJ IDEA has a lot of features to help you create and use Records and this blog post explains it using hands-on examples.

In this blog post, I’ll cover the changes to Records from Java 14 to Java 15.

Java 15 allows you to 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 Stocks that have 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, date)))
              .sorted((s1, s2) -> Double.compare(s1.stockValue(), s2.stockValue()))
              .limit(2)
              .map(s -> s.stock.getName())
              .collect(Collectors.toList());

}

Local interfaces and enums

Java 15 allows declaration of local enums and interfaces too – for the same reason. 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 by 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;
       }
   }
}

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-castToType.

In Java 14, the usage of the instanceof operator was simplified by the addition of Pattern Matching to the instanceof operator. It introduces a pattern variable, so you don’t need additional variables or explicit casting, making your code safer and more concise to write and read.

Pattern Matching for instanceof is in its second preview in Java 15 (without any changes from Java 14).

Please refer to my Java 14 and IntelliJ IDEA blog post, to find out how IntelliJ IDEA supports this new feature by adding new intentions. This blog post also has some excellent examples of how to refactor your code using this feature and other inspections from IntelliJ IDEA, like merging nested if and extracting or inlining variables.

Text Blocks

Multiline Strings, Text Blocks have been added as a standard language feature in Java 15 (without any changes from Java 14).

Please refer to my Java 14 and IntelliJ IDEA blog post, to find out how IntelliJ IDEA supports Text Blocks and you can use it in your applications.

Preview Language Features

Sealed classes and interfaces have been released as a preview language feature in Java 15. 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 a future Java release 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 a Switch Expression’s 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 2020.2 already supports all the new language features from Java 15. Try out Sealed classes and interfaces, Records, Pattern Matching for instanceof, and Text Blocks 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!

Subscribe

Subscribe for updates