JDK 15 is coming! Sealing classes is great!

JDK 15 is coming! Sealing classes is great!

JDK 15 is going to be released in September 2020. Amongst the changes expected in JDK 15 - Build 24, there is 1 direct code related JEP. We'll talk ab

We are going to look at a JEP 360 included in JDK 15 - Build 27. With this blog I just want to share what I've learned with regard to using sealed classes.

JDK 15 is going to be released in September 2020. Amongst the changes expected after JDK 15 - Build 27, there is 1 direct code related JEP. We'll talk about it here.

For this post, we only have one change to look at:

JEP 360: Sealed classes


1. The other JEP's

1.1. Proposals

In the pipeline, there is one proposal still left on for Java 15:

JEPs proposed to target JDK 15review ends
383: Foreign-Memory Access API (Second Incubator)2020/05/21

1.2. Targets

JEPs targeted to JDK 15, so far
360: Sealed Classes (Preview)
381: Remove the Solaris and SPARC Ports
339: Edwards-Curve Digital Signature Algorithm (EdDSA)
371: Hidden Classes
372: Remove the Nashorn JavaScript Engine
373: Reimplement the Legacy DatagramSocket API
374: Disable and Deprecate Biased Locking
375: Pattern Matching for instanceof (Second Preview)
377: ZGC: A Scalable Low-Latency Garbage Collector
378: Text Blocks
379: Shenandoah: A Low-Pause-Time Garbage Collector
384: Records (Second Preview)

2. Environment Setup

The project we will be looking at is developed in Java 15.

In the environment setup it is important to highlight the fact that my environment consists mainly of a MacBook Pro and IntelliJ. However what you need for this blog is the following:

  1. A machine with a system in it

  2. Java 15 EAB(Early Access Build) installed.

  3. An IDE

  4. Maven and GIT

In this post, I am assuming that you can install Java manually and that you can configure IntelliJ to run it. In order to help you though, I'd like to share with you how I created the alias for Java 15 on a MAC:

In terms of getting the packaged JDK 15 - Build 27 version for Mac-OS, this is an example of what you could do:

Example:

curl https://download.java.net/java/early_access/jdk15/27/GPL/openjdk-15-ea+27_osx-x64_bin.tar.gz --output openjdk-15-ea+27_osx-x64_bin.tar.gz

or

wget https://download.java.net/java/early_access/jdk15/27/GPL/openjdk-15-ea+27_osx-x64_bin.tar.gz

Install jenv:

brew install jenv

Add this to your .bash_profile

export PATH="$HOME/.jenv/bin:$PATH"
  eval "$(jenv init -)"
jenv add /Users/jofisaes/Downloads/jdk-15.jdk/Contents/Home
jenv global 15-ea
alias java15="jenv global 15-ea"

Note that in order to get back to previous java versions, you may have to use the same process to identify your previous version, install it and give it an alias.

All my examples are located in my GitHub repo. Make sure you have it locally if you want to see these examples in action.

To make sure you java Java 15 - Build 27, please check it via your command line:

java --version
openjdk 15-ea 2020-09-15
OpenJDK Runtime Environment (build 15-ea+27-1372)
OpenJDK 64-Bit Server VM (build 15-ea+27-1372, mixed mode, sharing)

Finally, bear in mind that I was able to run this example without the help of the compilers provided by IntellJ on design time. My version of IntellJ recognizes Java code up to version 14. Configuring properly, you can still run it on version 15. With other words, this just means that you will see underlined code (mostly in a red color) indicating that the code isn't compilable. If JDK 15 is configured correctly, you will be able to run it regardless.

This is how I have it configured. In the JDK configurations I have added Java 15 with the path of where I downloaded it and unpacked it to:

Then I added this version to the project

Note that the compiler in design time, the project compiler, is set to JDK 14 and this is why IntellJ will give you misleading warnings and error notices.

Finally this is how the overview of my desktop looks like:


3. JEP in detail

Please note that it's important to read the JEP in order to understand the examples I'm presenting further on.

3.1. JEP 360: Sealed classes

Sealed classes are basically what the wrapper images shows above. In our example, we can get the shrimps out of the package if we are allowed to. Also, we can make baked shrimps and cooked shrimps. We can even make a pie out of it or a cake. The point is, no matter how we transform the shrimps, they are still shrimps. They only take many forms no matter how we cook them. Now let's say we want to make a stewed chicken. Can we derive the shrimps into a stewed chicken 😱 !?!?!. Probably not. So if we have something like a Shrimps class, it wouldn't really make sense to make a StewedChicken sub-class right? That sounds obvious. At the moment we have these shrimps, and you know one of the things that I love? Tiger prawns! When they are grilled they are just fantastic. Are we allowed to make GrilledTigerPrawns out of Shrimps? Well, we don't know the species of these shrimps, but in order to make GrilledTigerPrawns we do need Prawns. We only have Shrimps in our example. On the other hand, shrimps are, in many cases also called gambas, and many times also called prawns. This seems ambiguous right?

However, there is a distinguishable difference between shrimps and prawns, and many articles try to explain the difference. Here is an example: Healthy Shrimp and Grits with Rosé

Albeit the confusion about it, we stay true to these definitions in this article.

We also need to take into consideration that this is on a very high level what sealed classes are all about.

Let's look into the code that exemplifies this in action.

In the project, we find one Shrimps class, a StewedChicken class, a GrilledTigerPrawns class and finally a CookedShrimps and a GrilledShrimps class.

Let's first have a look at the right way to do this. We will then break the code in order to durther understand what sealed classes actually mean:

We first create our interface class and with it define which classes will be allowed to implement the Shrimps interface. Keep in mind that our idea is not to transform the Shrimps. We are just defining an interface that defines the structure of a shrimp set. To make our example more evident we will make every Shrimps set implement a wrapInABox method.

  • ✅ Shrimps
public sealed interface Shrimps permits CookedShrimps, GrilledShrimps {
    String wrapInABox();
}

In this interface, we are specifying in the code that only CookedPrawns and GrilledShrimps will be able to implement this interface. Also not that by not specifying packages, we are also inplicitly telling the compiler that these classes exist within the same package.

Let's now look at the implementation of the examples that are not allowed to implement the Shrimps interface.

  • 🔴 StewedChicken
public record StewedChicken() {
    @Override
    public String toString() {
        return "I'm a chicken. Why am I even involved in this?";
    }
}

Our record class is not implementing the method wrapInABox. In our code we defined that all Shrimps implementations must implement this method. Since a StewedChicken is not a Shrimps set, it doesn't have to implement this.

Another example is the GrilledTigerShrimps. This would be a similar type, but not quite a Shrimps set.

  • 🔴 GrilledTigerPrawns
public record GrilledTigerPrawns() {
    @Override
    public String toString() {
        return "I'm definitely not a shrimp. I'm a tiger";
    }
}

Finally, we can implement the classes we are not only allowed to, but also have to implement:

  • ✅ CookedShrimps
public record CookedShrimps() implements Shrimps {

    @Override
    public String toString() {
        return "I'm a prawn and I'm cooked!";
    }

    @Override
    public String wrapInABox() {
        return "All the cooked prawns are wrapped!";
    }
}

The messages and strings we receive out of calling these methods, toString and wrapInABox, will be used in a bit, to make it clear how this works. The last implementation we miss is the GrilledShrimps class:

  • ✅ GrilledShrimps
public record GrilledShrimps() implements Shrimps {
    @Override
    public String toString() {
        return "I'm a prawn and I'm grilled!";
    }

    @Override
    public String wrapInABox() {
        return "All the grilled prawns are wrapped!";
    }
}

Now that we have the complete package let's now go the root of our module:

There we need to run a few commands.

Let's now compile our code:

javac  --enable-preview --release 15 src/main/java/org/jesperancinha/jtd/shrimps/*.java

We will be running the following main class:

public class ShrimpsLauncher {

    private static Logger logger = Logger.getLogger(ShrimpsLauncher.class.getName());

    public static void main(String[] args) {
        final var cookedShrimps = new CookedShrimps();
        final var grilledShrimps = new GrilledShrimps();
        final var grilledTigerPrawns = new GrilledTigerPrawns();
        final var stewedChicken = new StewedChicken();

        logger.info(cookedShrimps.toString());
        logger.info(grilledShrimps.toString());
        logger.info(grilledTigerPrawns.toString());
        logger.info(stewedChicken.toString());

        logger.info(cookedShrimps.wrapInABox());
        logger.info(grilledShrimps.wrapInABox());
    }
}

Finally let's run it and check our results:

java  --enable-preview -cp src/main/java org.jesperancinha.jtd.shrimps.ShrimpsLauncher

This is the output:

java  --enable-preview -cp src/main/java org.jesperancinha.jtd.shrimps.ShrimpsLauncher 
Jun 16, 2020 9:08:12 AM org.jesperancinha.jtd.shrimps.ShrimpsLauncher main
INFO: I'm a prawn and I'm cooked!
Jun 16, 2020 9:08:12 AM org.jesperancinha.jtd.shrimps.ShrimpsLauncher main
INFO: I'm a prawn and I'm grilled!
Jun 16, 2020 9:08:12 AM org.jesperancinha.jtd.shrimps.ShrimpsLauncher main
INFO: I'm definitely not a shrimp. I'm a tiger
Jun 16, 2020 9:08:12 AM org.jesperancinha.jtd.shrimps.ShrimpsLauncher main
INFO: I'm a chicken. Why am I even involved in this?
Jun 16, 2020 9:08:12 AM org.jesperancinha.jtd.shrimps.ShrimpsLauncher main
INFO: All the cooked prawns are wrapped!
Jun 16, 2020 9:08:12 AM org.jesperancinha.jtd.shrimps.ShrimpsLauncher main
INFO: All the grilled prawns are wrapped!

3.2. JEP 360: Sealed classes - What can fail

As we have seen above our implementation works! Let's now have a very quick look at how this can fail if we for example try to implement our interface Shrimp with a class that is not allowed to do so. I've included failed packages in the resources packagess. There I've changed our Shrimps interface to not allow the implementation of the GrilledShrimps class:

public sealed interface Shrimps permits CookedShrimps {

    String wrapInABox();
}

Let's now compile the code:

javac \
  --enable-preview --release 15 \
  src/main/resources/shrimps/*.java

src/main/resources/shrimps/GrilledShrimps.java:3: error: class is not allowed to extend sealed class: Shrimps
public record GrilledShrimps() implements Shrimps {
       ^
Note: Some input files use preview language features.
Note: Recompile with -Xlint:preview for details.
1 error

And this is the purpose of sealed classes. It gives you clarity on how to limit the implementation of certain classes.


4. Conclusion

From the JEP we take that the advantages of using sealed classes are these:

Allow the author of a class or interface to control which code is responsible for implementing it.

Provide a more declarative way than access modifiers to restrict the use of a superclass.

Support future directions in pattern matching by underpinning the exhaustive analysis of patterns.

in JEP 360 JEP 360: Sealed Classes (Preview)

As we have seen above this is true, we do indeed get more control of our implementation, we have made it more declarative than normal and finally we have made the analysis of this code much easier to do. From the latter, we can clearly see that we don't need to move around in the code to find out who implements Shrimps.


5. Resources


Thank you!

I hope you enjoyed this article as much as I did making it!
Please leave a review, comments or any feedback you want to give on any of the socials in the links bellow.
I’m very grateful if you want to help me make this article better.
I have placed all the source code of this application on GitHub.
Thank for reading!