Image of two illustrated people pointing at a browser screen displaying Java 16 logo
February 25, 2021

Java 16 Features and Previews

Java Updates

Java 16 is just around the corner, with the general availability version scheduled for release March 16. So what can we expect from Java 16? Let’s look at some of the JEPs and changes arriving with this new version of Java, the source code for which (thanks to JEP 357 and 369) is now available on GitHub.

Back to top

Java 16 JEPs

JEP 396: Strongly Encapsulate JDK Internals by Default

In Java 16, the strong encapsulating, introduced with the module system in Java 9, is now enforced by default. Up until now, the encapsulation was relaxed, meaning breaking though this encapsulation using reflection produced a warning. From Java 16 onwards, the default behavior is “deny”, throwing an IllegalAccessError exception instead. This potentially breaks applications that haven’t been updated. All is not lost if your applications aren't updated; you can still enable the relaxed mode, by using the --illegal-access

JEP 390: Warnings for Value-Based Classes

One warning gone, another one added! This JEP introduces warnings when using the primitive wrapper classes (Integer, Long, Double , etc.) in a way that won’t be supported in the future. For example, using the constructors for these wrapper types directly (like calling new Integer(1)) or using them as monitor objects for synchronization will raise this warning. 

JEP 395: Records

Records were added as a preview feature in Java 14, continued second preview phase in Java 15, and are now going to be part of the Java specifications for Java 16.

Record classes provide a clearer way to write immutable data aggregate classes in Java. They simplify this by eliminating much of the boilerplate code otherwise associated with these kinds of classes, such as having to explicitly write accessor methods, equals, hashCode and toString.

For readers familiar with Kotlin, a record is similar to a data class, or for users of Project Lombok, similar to @Value classes.

A simple example of a record for a coordinate in 3D, with an x, y and z value could look like:

 public record Coordinate(int x, int y, int z) {}

This will produce a class with three private final fields, and the necessary methods to construct an instance, access the fields and the other methods, as mentioned above.

Related reading: Learn more about how records work.

An interesting side benefit of this feature is that it also removes the restriction of non-static inner classes’ inability to declare static members.

JEP 394: Pattern Matching for instanceof

Another language feature that is now becoming a part of the standard is the instanceof pattern matching, which were also first previewed in Java 14.

This language addition simplifies instanceof checks with the ability to also declare a variable of the checked type, for immediate use in the truth-scope of the instanceof check.

if (obj instanceof Type) {

   Type t = (Type)obj;



Can now be simplified as:

if (obj instanceof Type t) {



Its strength really shines when you also have checks on the type-checked object, like:

if (obj instanceof Type t && t.value() == 123) { … }
Back to top

Stream API additions

The Stream API has been expanded with a few convenience methods in Java 16, with the introduction of toList() and the mapMulti family of methods.

.toList() provides a convenient and potentially optimized way to return an unmodifiable List, similar to .collect(Collectors.toUnmodifiableList()). The documentation also stresses that this method may return a value-based List, which could have implications in the future when primitive classes are introduced.

The mapMulti family of methods is similar to flatMap, but instead of you producing a Stream that is flattened, you receive a Consumer, that you can push elements to.

To avoid the overhead of constructing a Stream when only expanding to a few elements, or for situations where an imperative approach for generating the resulting elements is easier, use this method instead of flatMap. It comes in primitive mapping flavors as well, like mapMultiToInt, which will result in an IntStream. Similarly, for Long and Double.

Back to top

Incubator and Preview features

As with the previous many releases, Java 16 also contains multiple incubator and preview features. These include new ones and new iterations of existing ones, perfectly showcasing the ability to push new features—one of the main points of the new Java release cadence.

JEP 397: Sealed Classes (Preview, 2nd Round)

Among these is the second iterations of sealed classes, which we looked at last year in our article. This feature looks to be on track to be included into the spec with Java 17.

Sealed classes offer a way to restrict which other classes or interface may extend or implement them, thus providing a way to declare all possible permitted subtypes. This, for instance, allows developers to expose an API and still control all possible implementations of it.

The second preview of sealed classes adds more compile-time checks with regards to narrowing reference conversion. This means that the javac compiler, knowing all possible subtypes, can identify that Type T can never implement sealed interface I, and would then throw a compile-time error.

Take the following example code, in which the entire subtype tree for Shape is well known, since all permitted types are final:

public class Test {

  static sealed interface Shape permits Circle {}

  static final class Circle implements Shape {}

  public static void foo(Shape s) {

    Circle c = (Circle) s;

    List<Circle> l = (List<Circle>) s;



The above will compile with Java 15. But with Java 16 and the improvements to the narrowing reference conversion, you will see the following compile time error: error: incompatible types: Shape cannot be converted to List<Circle>

List<Circle> l = (List<Circle>) s;

JEP 393: Foreign-Memory Access API (Incubator, 3rd Round)

The Foreign-Memory Access incubator API (JEP 393) is going through its third iteration in this release. We looked at the first iteration, when it was introduced in Java 14. Since then, improvements have been made to the API, including support for shared regions, better cleanup management, the addition of more convenient utility methods, and a better separation between the MemorySegment and MemoryAddress interfaces.

JEP 389: Foreign Linker API (Incubator, New)

A new incubator module added in this release, related to the Foreign-Memory Access API, is the Foreign Linker API. Both the Foreign-Memory Access API and the Foreign Linker API are part of Project Panama, which aims to improve and enrich the connection between Java and foreign (non-Java) APIs.

The Foreign Linker API introduces a statically-typed way to access native code. The initial focus is access to C libraries and the API, but should be flexible enough to be expanded in the future as well. This is not meant as a re-implementation of Java Native Interface (JNI), but the goal is to provide an API that performs as well as JNI, or better!

This API allows Java code to call native methods (downcalls), but also provides the reverse (upcalls).

The API utilizes the MethodHandle API to achieve the interfacing between Java and the native methods, as well as lookup classes for loading and accessing native symbols.

Say we want to call the POSIX library function strlen, whose definition looks like:

size_t strlen(const char *s);

In order to call that function, we need to lookup the symbol, create a downcall using the CLinker class, convert the arguments to the Java equivalent, and establish how to convert between the two. In C, a string is a pointer to a memory segment with the characters, terminated with the null-char. This is where the Foreign-Memory Access API comes into play since char* can be modeled as a MemoryAddress.

The call required to create a MethodHandle for invoking the native strlen function looks like this:

MethodHandle strlen = CLinker.getInstance().downcallHandle(


    MethodType.methodType(long.class, MemoryAddress.class),

    FunctionDescriptor.of(CLinker.C_LONG, CLinker.C_POINTER)


Now, in order to invoke it, we would simply call long len = strlen.invokeExact(memAddress). Should we want to use this method to calculate the length of a Java String, we would need to convert it to the C equivalent first. Luckily, a convenient utility method exists for that, CLinker.toCString(str, charset). This method copies the string content as returned by str.getBytes(charset) . It defaults to system platform default charset if not specified, to an off-heap memory location, and returns a MemorySegment that we can then get the address from. Afterwards, it will release using the try-with-resources construct:

try (MemorySegment cstr = CLinker.toCString("JEP 393 and JEP 389")) {

long len = strlen.invokeExact(cstr.address()); // len would be 19


JEP 338: Vector API (Incubator, New)

The final preview feature included with Java 16 is the Vector API. The purpose of this module is to add support for expressing vector computation operations in a platform agnostic way. The JIT compiler should then translate this to the appropriate hardware instructions on supported CPU architectures, such as the ones supported by Streaming SIMD Extensions (SSE) and Advanced Vector Extensions (AVX) extensions. On hardware where those extensions are not supported, the API should still work correctly. 

New Official Ports

In Java 9, OpenJDK got an official port for Linux running on the aarch64 (arm64) architecture, meaning proper JIT compilation for the platform. Lately, the aarch64 architecture has become more and more popular, most recently with the new generation of Mac machines using the Apple M1 chip, which is an aarch64 architecture. The port for macOS on aarch64 isn’t quite there yet (JEP 391 is still in candidate phase, but multiple vendors already provide binaries for it), but from Java 16, the port for OpenJDK running on Windows on aarch64 (JEP 388) is now official. This adds support for newer devices like Microsoft’s Surface Pro X series of laptops.

Java 16 also adds official support for Alpine Linux (JEP 386), which is a widely adopted Linux distribution for Docker containers.

Back to top

Other improvements and changes

Java 16 also adds support for using Unix-Domain socket channels (JEP 380) for inter-process communication. New improvements are added to the ZGC garbage collector (JEP 376), along with an improvement to speed the release of OS memory from the metaspace (JEP 387).

Java 16 also sees the addition of the jpackage tool (JEP 392), which is based on the JavaFX jpackager tool, and allows for creating native packages and installers.

The experimental AOT compiler and the Graal JIT, originally introduced with JEP 295 in Java 9, has been removed, or at least from the Oracle built binaries.

A lot of changes have gone into the crypto libraries, such as added support for SHA-3 related algorithms, added support for EdDSA, removal of some legacy elliptic curves, and disabling of TLS 1.0 and 1.1.

Back to top

Final Thoughts

New features and implementations in Java 16 will make development more streamlined. JEPs such as Records and Pattern Matching will save developers time, while preview features surrounding Foreign APIs will help developers using non-Java APIs. 

However, because Java 16 is not a long-term support release, many developers may choose to wait for the Java 17 releasewhere you may see some of the Java 16 preview features appear as fully delivered features.

Additional Resources

Wondering what the future of Java looks like? Download our 2021 Java Developer Productivity Report to see insights on the Java tools and technologies developers are using this year. 

Get Started With JRebel

See what JRebel can do for your Java project with a free 14-day trial.


Back to top