Discover Azul's high-performance Java platform providing faster speed, startup, & efficiency without code changes
Support
Blog chevron_right Java

Java 16 – JDK 16 Updates and New Features

How time flies!  Even in these extraordinary times, it seems hard to believe another six months have passed, and we now have a new release of the JDK.  As usual, I will summarise all the new features with some commentary on their potential impact on Java application development.

Not all releases are created equal and, depending on how feature development phases align; some will have more features than others.  JDK 16 contains quite a few new things, although several of them are continuations or finalization of incubator modules or preview features from earlier releases.  I think this is one of the biggest benefits of the faster release cadence. Providing new functionality without making it part of the standard, gathering feedback from developers and potentially making changes has delivered an improved process for the JDK development.

I’ll take my usual approach of dividing the post into language changes, library additions, JVM related updates and, finally, anything else.

Java 16 Language Updates

There are no new syntax constructs in JDK 16, but there are still several significant changes.

JEP 394: Pattern matching for instanceof

This was introduced as a preview feature in JDK 14 and continued in preview mode in JDK 15.  This feature has been finalized in JDK 16. It is included in the Java SE 16 Language Specification and does not require the --enable-preview command line flag for compilation and runtime.

There are two minor changes to how the feature worked in JDK 15.  The first is that pattern variables are no longer implicitly final.  This is a very logical change since local variables are not treated as implicitly final.

The second change is that is now a compile-time error for a pattern instanceof expression to compare an expression of type S against a pattern of type T, where S is a subtype of T. Here’s an example:

var a = new ArrayList<String>();

if (a instanceof List l)
    System.out.println(l.size());

This will result in a compiler error:

Error: pattern type java.util.List is a subtype of expression type java.util.ArrayList<java.lang.String>

This seems logical, since the if statement is redundant as the predicate would always evaluate to true.

JEP 395: Records

Another preview feature introduced in JDK 14 and now finalized in JDK 16.  For me, this is a beneficial addition to the Java language, as we finally have a simple way to deal with tuples.

The only change to Records from the JDK 15 implementation is to relax the longstanding restriction whereby an inner class cannot declare a member that is explicitly or implicitly static.  This is very useful for simplifying certain stream operations.  Often it is desirable to have a stream pass more than one object for each element.  Now, a local record can be defined reducing the type pollution that would otherwise result.

JEP 397: Sealed Classes

This was introduced as a preview feature in JDK 15 and continues as one in JDK 16.

There are three changes:

  1. Adding new language syntax often requires new keywords. Adding these as reserved words to the language (like assert in JDK 1.4) can seriously impact backwards compatibility since these words are no longer valid as identifiers.  We have had the concept of restricted types and restricted keywords (like var and sealed) to work around this.  This JEP introduces the concept of contextual keywords.  Specifically, for JDK 16, sealed, non-sealed and permits are contextual keywords.
  2. As with anonymous classes and lambda expressions, local classes may not be subclasses of sealed classes when determining the implicitly declared permitted subclasses of a sealed class or interface.
  3. The narrowing reference conversion definition in the JLS has been extended to navigate sealed hierarchies to determine which conversions are not possible at compile time.

Java 16 Class Libraries

There are four new libraries defined through individual JEPs:

JEP 338: Vector API

Not to be confused with the deprecated Vector class in the collections API, this is a library to enable developers to better use underlying CPU vector capabilities.  All modern processors have single instruction, multiple data (SIMD) capabilities.  Multiple elements of an array can be loaded into very wide registers, such as the 512-bit AVX-512 on specific Intel processors.  For example, a single operation, adding 10 to each value, can be performed in a single machine instruction cycle, significantly improving the performance of numerically intensive code.  The problem is that the compiler needs to recognize code that can be turned into vector operations, and this can be very hard, especially if there are conditional operations involved.

The vector API provides a mechanism for developers to make it explicit to the compiler that vector operations should be used.  This does, however, make the code more complicated.  First, it is necessary to obtain a vector species.  This is the form of the vector that is required, both in terms of the size of the register to be used and the type of the data to be loaded, such as a float.  Type-specific vectors are then created and loaded, as required, with values.  The final part is to manipulate the vectors using the appropriate mathematical functions.

All of this can be seen in the example lifted from the JEP.

static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256;

void vectorComputation(float[] a, float[] b, float[] c) {
    for (int i = 0; i < a.length; i += SPECIES.length()) {
        var m = SPECIES.indexInRange(i, a.length);

        // FloatVector va, vb, vc;
        var va = FloatVector.fromArray(SPECIES, a, i, m);
        var vb = FloatVector.fromArray(SPECIES, b, i, m);
        var vc = va.mul(va)
           .add(vb.mul(vb))
           .neg();

        vc.intoArray(c, i, m);
    }
}

This is provided as an incubator module, so subject to change before being included in the Java SE standard.  I hope that the developers will realize that using abbreviations is not necessary (or ideal).  I would much rather see multiply() than mul() and so on.

JEP 380: UNIX domain socket channels

UNIX domain sockets have been around for a long time, dating all the way back to the BSD variant in 1983.  However, it wasn’t until 2018 that these were introduced in the Windows platform.  Since Java prides itself on being cross-platform, it didn’t make a lot of sense to have a library that couldn’t run anywhere.

Now we have the UNIX domain sockets API, which provides an easy way to handle interprocess communication on a single host.  These sockets are very similar to using a TCP/IP loopback except that they are addressed by a filesystem pathname rather than an IP address and port number.  The advantage of UNIX domain sockets is they are more efficient and secure than a loopback.

JEP 393: Foreign Memory Access API

This library was introduced as an incubator module in JDK 14 and is on its third iteration in JDK 16.

This API allows Java programs to safely and efficiently access foreign memory outside of the Java heap.  It is related to Project Panama, which is intended to replace the Java Native Interface as a way for Java applications to interact with native libraries.

There are four changes from the JDK 15 incubator version:

  1. A clearer separation of roles between the MemorySegment and MemoryAddress interfaces.
  2. A new interface, MemoryAccess, which provides common static memory accessors to minimize the need for the VarHandle API in simple cases.
  3. Support for shared segments.
  4. The ability to register segments with a Cleaner

JEP 389: Foreign Linker API

As an incubator module, this introduces an API that offers statically-typed, pure-Java access to native code.  This is also part of Project Panama and, combined with the Foreign Memory Access API, makes binding to a native library much simpler.  The initial implementation focuses on libraries written in C.

It is first necessary to locate the appropriate symbol within a library to interact with a foreign function.  The LibraryLookup interface is used to accomplish this, and symbols can be found from those loaded in the JVM through a library on the java.library.path or using an explicit file path.

Having located the foreign function to use, the Clinker interface provides two methods for interaction between the Java code and the function:

  1. The downcallHandler method returns a MethodHandle that can be used to call the foreign function in the same way as a Java method.
  2. The upCallStub method returns a MemorySegment (from the Foreign Memory Access API). It is used when the foreign function needs to call back to a Java method (typically as a callback method).

When you look at the examples of using the Foreign Linker API, the code seems quite complex.  Project Panama includes the jextract tool, which is not in the JDK yet.  This simplifies things considerably by removing all the boiler-plate code and enabling a simple static import to access the foreign function.  Hopefully, this will make it into JDK 17.

Other Java 16 API Changes

There are 58 new API elements included in the updated Java SE specification, including the UNIX domain sockets described above. Here are some others that I consider interesting to developers.

java.lang

In Class, permittedSubClasses() has been replaced with getPermittedSubClasses().

IndexOutOfBoundsException now has a new constructor that takes a long as an index to handle the situation where the index being referred to is bigger than an int.

java.nio

ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer and ShortBuffer all have a new put() method.  These methods copy a defined length section of a second buffer, starting at a given offset in that buffer to a specified index in the buffer that the method is being called on.  That description seems complicated, but it is a simple replacement for the loop:

for (int i = offset, j = index; i < offset + length; i++, j++)
    destinationBuffer.put(j, sourceBuffer.get(i));

java.time.format

The DateTimeFormatterBuilder class has a new method, appendDayPeriodText().  This provides a way of formatting the time of day as “in the morning”, “in the afternoon”, etc. rather than just AM or PM

javax.swing

It is good to see that Swing is still getting updates, even if they are pretty minor.

  • JPasswordField has a new method, setText(). This is useful if you want to pre-populate the password field with one that has been remembered (which is the only way I can deal with login credentials for websites).
  • AccessibleJSlider has a new listener, stateChanged()

java.util.logging

LogRecord has two new methods, getLongThreadID() and setLongThreadID().  These are replacements for the now deprecated get/setThreadID methods that are restricted to integer values.

There are a couple of exciting changes to the stream API in JDK 16.

The first is the inclusion of a new terminal operation provided as a default method on the Stream interface.  This is toList(), which accumulates the elements of the stream into an unmodifiable list.  Making the list unmodifiable has prompted some discussion on places like Reddit but seems to be a logical choice.

The second change is the introduction of mapMulti() as an intermediate operation, again provided as a set of default methods on stream (this includes type-specific ones for double, int and long).  When you first read the description of this operation, it looks identical to flatMap.  It returns a stream consisting of the results of replacing each element of this stream with multiple elements, specifically zero or more elements.  Unlike flatMap(), which effectively concatenates streams generated by each element on the input stream, mapMulti() applies a mapping that can result in multiple elements being produced.  If you were to use mapMulti() to map to zero or one element, it would be the same as a filter operation.  If you used it to map to only one element, it would be the same as a map().  The benefit is when you want to create multiple elements for each input element.  A simple example is shown below:

wordList.stream()
    .mapMulti((str, consumer) -> {
        for (int i = 0; I < str.length(); i++)
            consumer.accept(str.length());
        })
    .forEach(System.out::print);

For a stream of words, the result will be each word’s length printed that number of times.  The important thing here is that the accept() method on the consumer can be called as many times as required.

Only one previously deprecated API has been removed in JDK 16, the constructor of the javax.tools.ToolProvider class.

JEP 376 moves ZGC thread-stack processing from safepoints to a concurrent phase, allowing significantly reduced pauses inside GC safepoints, even on large heaps.  The initial phase of ZGC (as with many collectors) builds a root set of objects that are accessible by an application.  To do this requires a traversal of the stack looking for object references.  In the past, this has required all threads to reach a global safe point where they can all be paused safely and the work completed.  This is no longer necessary in ZGC and can be done concurrently with application threads.  Interestingly, the way this was designed did not bind it to ZGC, and so the Shenandoah project has also picked up the use of this feature.

Metaspaces were the replacement for the permanent generation, which used to be included in the heap.  JEP 387 introduces elastic metaspaces, where the memory used can be returned to the operating system more promptly.  The code has also been simplified, reducing the maintenance overhead.

Other JDK 16 Features

The most significant change to the OpenJDK project infrastructure for many years is the migration from Mercurial to Git for the OpenJDK source code management system (JEP 357) and hosting all the code on Github (JEP 369).  Git/Github is hugely popular with developers, so this is a sensible decision and will help get more people involved in the project.

Also related to the JVM source code specifically is the move to allowing the use of C++14 features (JEP 347).  It seems that the use of newer language features moves at a truly glacial pace as we are only now including features from a seven-year-old release.  Imagine the excitement this will cause for JVM developers, who can now use binary literals!

There are two new ports of the JDK, starting with JDK 16:

  1. Alpine Linux (JEP 386): This is targeted primarily at those looking to deploy Java-based microservices. Alpine Linux uses the musl implementation of the C library to deliver minimal Linux environments.  Combined with a jlink generated JDK, Docker images can be as small as 38Mb.  Admittedly, this only includes the java.base module, but it’s a great starting point.
  2. Windows/AArch64 (JEP 388): Arm 64 processors are becoming increasingly popular for both the server and client side. This port will help to increase Java’s popularity even further.  Having been involved with Java right from the very early days, I have to say it’s great to see Microsoft contributing to the OpenJDK project!

Project Valhalla, which will hopefully bring value types to Java requires some subtle changes to Java before it can be delivered.  The existing primitive wrapper classes (like Integer) are now designated as value-based classes, and the constructors have all been deprecated for removal.  Typically, these are not used anyway, but appropriate compiler warnings will be issued if you have used them.  Warning messages will also be generated if you attempt to synchronize on an instance of these classes.  This is all detailed in JEP 390.

JDK 9 introduced the Java Platform Module System, and one corollary part of that was JEP 260, which encapsulated most of the internal APIs of the JDK.  Because this had the potential to break a lot of existing code, a command-line flag was included: --illegal-access, which could have one of four values: permit, warn, debug and deny.  Since JDK 9, the default value for this has been permit, allowing code that uses critical internal APIs to continue to work as expected.  In JDK 16, thanks to JEP 396, JDK internal APIs are strongly encapsulated by default (changing the flag value to deny).  The JEP states that this will not encapsulate APIs for which standard replacements do not yet exist. This means that sun.misc.Unsafe will remain available.  Some reflection code that generated warnings in JDK 15 will now throw an exception and terminate, so be careful!

Although not confirmed yet, it looks likely that in JDK 17, it will no longer be possible to relax the encapsulation, effectively removing the –illegal-access flag.

Packaging Tool

Prior to JDK 11, Oracle included JavaFX in their builds of the JDK.  This provided a nice tool that could generate installable, platform-specific packages for Java applications.  When this was removed, developers expressed a desire to restore this functionality.  This was included in JDK 14 as an incubator module (although not a module exactly, incubators also cover tooling).  It is now included as a full feature in JDK 16.  The only change from JDK 14 is replacing the –bind-services command line flag with a more general –jlink-options one.

Two experimental features have been removed from the JDK, specifically the Graal JIT compiler and the ahead of time tool, jaot.  This is presumably because Oracle is investing and promoting GraalVM that includes this functionality.

Conclusions

JDK 16 continues the rapid pace of change in the Java platform. We’re really starting to see the power and benefit of preview features and incubator modules.

With JDK 17, we’ll see the next long-term support release.  Although this is not a concept of OpenJDK, it is one that’s delivered by all providers of OpenJDK binary distributions (including Zulu from Azul).  As such, you should definitely be testing your applications with JDK 16 to see if any of these changes have any impact.

Are you ready to use modern Java?

(Note: I counted 67 new features and APIs consisting of 17 JEPs and 50 new Java SE API elements.  The UNIX domain sockets account for 8 API elements).