Definitive Guide To Java 16

A detailed guide to Java 16: records, type patterns, sealed classes; Stream and HTTP/2 additions, Unix domain socket support; Project Panama previews, packaging tool, performance improvements, and more

Java 16 gets released today and here's everything you need to know about it, starting with version requirements before getting into the juicy bits: First and foremost the now-finalized records and type patterns, then sealed classes (in their second preview) and a lot of smaller additions as well as two important deprecations/limitations. Here's your overview with lots of links to more detailed articles.


Version Requirements for Java 16

Here are the most common IDEs' and build tools' minimum version requirements for Java 16:

  • IntelliJ IDEA: 2021.1 (currently in early access; release later this month)
  • Eclipse: 2021-03 (4.19) with Java 16 Support plugin (remember to remove support plugins that are no longer needed)
  • Maven: 3.5.0

    • compiler plugin: 3.8.0
    • surefire and failsafe: 2.22.0
  • Gradle: not yet 😔, but in 7.0

When it comes to compiling to Java 16 bytecode, keep in mind that you will likely have to update all tooling (e.g. Maven plugins) and dependencies (e.g. Spring, Hibernate, Mockito) that rely on bytecode manipulation. If they use ASM (e.g. the shade plugin) you may get away with simply updating that - ASM 9.0 is compatible with Java 16

Preview Features

I wrote a dedicated post on preview features, so I will stick to the very basics here. If you want to use sealed classes, include this in your build tool configuration:

<!-- Maven's pom.xml -->
// Gradle's build.gradle
compileJava {
	options.compilerArgs += ["--enable-preview"]
test {
	jvmArgs '--enable-preview'

In IntelliJ IDEA, set the language level for your module to 16 (Preview). In Eclipse, find the Java Compiler configuration and check Enable preview features.

Language Features

Here's the new syntax you get if you upgrade to Java 16.


Much has already been written about records (because they're so damn cool), so I don't want to bore you with all the details. Here's a straightforward example of a Range in a bean-like fashion:

public class Range {

	private final int low;
	private final int high;

	public Range(int low, int high) {
		this.low = low;
		this.high = high;

	public int getLow() {
		return low;

	public int getHigh() {
		return high;

	public boolean equals(Object o) {
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;
		Range range = (Range) o;
		return low == range.low &&
				high == range.high;

	public int hashCode() {
		return Objects.hash(low, high);

	public String toString() {
		return "[" + low + "; " + high + "]";


And here's almost the same type as a record:

//                 |-- components ---|
public record Range(int low, int high) { }

The compiler creates:

  • for each component, a field with the same name
  • a constructor with arguments matching the components (called the canonical constructor)
  • for each component, an accessor with the same name
  • equals, hashCode, and toString that use all components

That means the main difference to the long-form class above is that the accessors aren't getLow() and getHigh(), but low() and high(), respectively. (In my book, that's an improvement - I avoid get... naming for years.) And the generated toString() is of course not quite as compact as the hand-written one.

What's more important than the small differences and even than removing so much boilerplate is the semantic meaning of a record: A record is a named collection of data (technical term: nominal tuple) that has no need for encapsulation.

A record is a nominal tuple that has no need for encapsulation

It's that semantic meaning that lets the compiler generate the members of a record and that makes records work so well with serialization. And it should be your guiding principle when creating records: Not "can I save boilerplate" or "can this be a syntactically correct record", but "is this a collection of data that doesn't need encapsulation", i.e. "can this be a semantically correct record".

More on records:

JEP 395: Records
Java Feature Spotlight: Records by Brian Goetz
Simpler Serialization with Records by Julia Boes and Chris Hegarty

Type Pattern Matching

This feature got a smaller spotlight than records because its benefit is not as glaringly obvious. With type patterns, Java takes its first step towards pattern matching a rich set of features that can be used to easily test whether an instance has a desired property and then extract the part that is needed. Here's what that looks like in Java 16:

public void feed(Animal animal) {
	if (animal instanceof Elephant elephant)
	else if (animal instanceof Tiger tiger)

In general, the expression variable instanceof Type typedVar checks whether variable is an instance of Type and if it is, declares a new variable typedVar of that type. In the example above that means that you can use elephant and tiger as Elephant and Tiger, respectively. That's really all you need to know for basic syntax:

  • pick any regular instanceof check
  • append a variable name after the type
  • use that variable as that type

If you're interested in more details on (type) pattern matching, check out these links:

JEP 394: Pattern Matching for instanceof
Java Feature Spotlight: Pattern Matching by Brian Goetz
Java 16 Pattern Matching Fun by Benji Weber
Pattern Matching in Java by me
Type Pattern Matching with instanceof by me

Sealed Classes

In Java 16, this feature takes its second round of reviews, so it would be amazing if you could take some time to put it into practice in your code base and give your feedback on the Project Amber mailing list. To give you a leg up, here's a quick intro to what sealed classes are all about.

In short, imagine them as a middle ground between "everybody can extend this type" (default in Java) and "nobody can" (final classes). A sealed class/interface can't be extended/implemented except by the types it lists:

sealed interface Staff permits Employee, Freelancer { }

final class Employee implements Staff { }

final class Freelancer implements Staff { }

// compile error because `Staff` doesn't permit `Consultant`
final class Consultant implements Staff { }

With sealed classes, you can express that a class or interface can't just be extended by anybody, but that you have a specific list of subtypes in mind that you want to control. Not only does that express your intent to your colleagues, the compiler gets it, too.

Once we get pattern matching for switch expressions, the compiler can check exhaustiveness:

public double cost(Staff staff) {
	return switch(staff) {
		// strawman syntax!
		case Employee employee ->
			employee.salary() * 2;
		case Freelancer freelancer ->
			freelancer.averageInvoice() * 1.1;
		// no default branch, yet no error
		// ~> compiler checked exhaustiveness

More on sealed classes:

JEP 397: Sealed Classes (Second Preview)
Java Feature Spotlight: Sealed Classes by Brian Goetz

API Improvements

These are the three main APIs that saw improvements in Java 16. Of course there are a few more, even smaller changes, so if you want to dig deeper:

JDK 15 to 16 API Diff
Auto-generated release notes


The stream API got two new methods: mapMulti and toList. Let's start with the former:

// plus wildcards
<R> Stream<R> mapMulti​(BiConsumer<T, Consumer<R>> mapper)

You call mapMulti on a Stream<T> to map a single element of type T to multiple elements of type R. So far, so flatMap, but in contrast to that method, you don't pass a function that turns T into Stream<R>. Instead you pass a "function" that receives a T and can emit arbitrary many Rs by passing them to a Consumer<R> (that it also receives). I say "function" because it's actually a bi-consumer that doesn't return anything. Here's an example where we don't actually do anything:

Stream.of(1, 2, 3, 4)
	.mapMulti((number, downstream) -> downstream.accept(number))
// prints "1234"

Our BiConsumer<Integer, Consumer<T>> is called for each element in the stream [1, 2, 3, 4] and each time it simply passes the given number to the downstream consumer. Hence, each number is mapped to itself and so the resulting stream is also [1, 2, 3, 4].

For the motivation behind introducing a weird imitation of flatMap, more meaningful examples, and a bit of fun with the new method, check out these posts:

Faster flatMaps with Stream::mapMulti in Java 16 by me
Broken Stream::group with Java 16's mapMulti by me

An addition with much larger application is Stream::toList:

 List<T> toList()

That's simple, right? And it's simple to use as well:

List<String> numberStrings = Stream.of(1, 2, 3, 4)

So is this like collect(Collectors.toList())? Not quite:

For more on Stream::toList:

JDK-8180352: Add Stream.toList() method
JDK 16: Stream to List In One Easy Call by Dustin Marx
Stream.toList() and other converter methods I’ve wanted since Java 2 by Donald Raab


Not the biggest of deals, but the HTTP/2 API (introduced in Java 11) got two new methods:

Thanks to a new overload of HttpRequest::newBuilder that accepts an HttpRequest and a BiPredicate<String,String> you can take an existing HttpRequest and create a builder with the same initial configuration. The provided BiPredicate can remove headers and the builder API lets you add new ones or change other configuration details.

JDK-8252304: Seed an HttpRequest.Builder from an existing HttpRequest

The other new method deals with BodyPublishers, which you use to create the request body. Thanks to BodyPublishers::concat you can now easily concatenate the output of several publishers into one body.

JDK-8252382: Add a new factory method to concatenate a sequence of BodyPublisher instances into a single publisher

Unix Domain Sockets

Java's SocketChannel / ServerSocketChannel API provides blocking and multiplexed non-blocking access to sockets. Since Java 16, this is no longer limited to TCP/IP sockets: Unix domain sockets can now be used as well on Linux, MacOS, and - despite their name - Windows 10 and Windows Server 2019.

Unix domain sockets are addressed by filesystem path names and are thus limited to inter-process communication on the same host. This is how you can create a simple server and client that communicate with one another (the code ignores lots of real-life complexities; don't copy it):

// server & client
Path socketFile = Path
UnixDomainSocketAddress address =

// server
ServerSocketChannel serverChannel = ServerSocketChannel
SocketChannel channel = serverChannel.accept();
// send/receive messages...

// client
SocketChannel channel = SocketChannel
// send/receive messages...

Compared to TCP/IP loopback connections, Unix domain sockets have a few advantages:

  • Because they can only be used for communication on the same host, opening them instead of a TCP/IP socket has no risk of accepting remote connections.
  • Access control is applied with file-based mechanisms, which are detailed, well understood, and enforced by the operating system.
  • Unix domain sockets have faster setup times and higher data throughput than TCP/IP loopback connections.

Note that you can even use Unix domain sockets for communication between containers on the same system as long as you create the sockets on a shared volume.

JEP 380: Unix-Domain Socket Channels
Talking to Postgres Through Java 16 Unix-Domain Socket Channels by Gunnar Morling
Code-First Unix Domain Socket Tutorial by me

Incubating Panama

There are three really interesting incubating APIs out of Project Panama, which aims to improve and enrich the connections between Java and foreign (i.e. non-Java) APIs. It's good to have them on your radar and, if you're interested in their application, to kick their tires. Please keep in mind that there might be performance potholes and missing features and that nothing is set in stone yet.

Feedback is very welcome! Please report your experiences to the Project Panama mailing list. If you write a blog post, let them know there or ping me, so I can forward it.

Foreign Linker and Foreign-Memory Access

An integral part of Project Panama is the foreign linker API that allows statically-typed, pure-Java access to native code. The goals:

  • Ease of use: Replace JNI with a superior pure-Java development model.
  • C support: The initial scope of this effort aims at providing high quality, fully optimized interoperability with C libraries, on x64 and AArch64 platforms.
  • Generality: The Foreign Linker API and implementation should be flexible enough to, over time, accommodate support for other platforms (e.g., 32-bit x86) and foreign functions written in languages other than C (e.g. C++, Fortran).
  • Performance: The Foreign Linker API should provide performance that is comparable to, or better than, JNI.

This API will go through at least one more round of incubation in Java 17. One of the bigger features still on the roadmap is better support for loading libraries, such as automatically handling libraries with version suffixes, as well as linker scripts.

JEP 389: Foreign Linker API (Incubator)
Project Panama - The Foreign Linker API, podcast with Maurizio Cimadamore and Jorn Vernee
Foreign Linker API: Java native access without C by Markus Karg
A practical look at JEP-389 in JDK16 with libsodium by Brice Dutheil

The foreign linker API builds on the foundations laid by the foreign-memory access API, which offers a safe and efficient way to access memory outside of the Java heap. It's goals:

  • Generality: A single API should be able to operate on various kinds of foreign memory (e.g., native memory, persistent memory, managed heap memory, etc.).
  • Safety: It should not be possible for the API to undermine the safety of the JVM, regardless of the kind of memory being operated upon.
  • Control: Clients should have options as to how memory segments are to be deallocated: either explicitly (via a method call) or implicitly (when the segment is no longer in use).
  • Usability: For programs that need to access foreign memory, the API should be a compelling alternative to legacy Java APIs such as sun.misc.Unsafe.

JEP 393: Foreign-Memory Access API (Third Incubator)
Project Panama - The Foreign Memory Access API, podcast with Maurizio Cimadamore and Jorn Vernee
Foreign Memory Access - Pulling all the threads by Maurizio Cimadamore


The goal of this API:

[E]xpress vector computations that reliably compile at runtime to optimal vector hardware instructions on supported CPU architectures and thus achieve superior performance to equivalent scalar computations

The JEP is very informative, as are the Inside Java podcast and Gunnar's experiments:

JEP 338: Vector API (Incubator)
The Vector API podcast with John Rose and Paul Sandoz
FizzBuzz – SIMD Style! by Gunnar Morling


Remote JFR Streaming

This is a treat for everybody who's remotely monitoring their application: It is now possible to stream JFR events over JMX!

With a remote streaming connection from the server (running the app) to the client (running the JFR tool), "[t]he event data will be continuously written to a disk located on the client, in a similar manner to how it is continuously written to disk on the server". That means existing JFR tools, which already read from such a file, require very little change.

JDK-8253898: JFR: Remote Recording Stream
Monitoring REST APIs with Custom JDK Flight Recorder Events by Gunnar Morling

Packaging Tool

Java 14 and 15 incubated and 16 now officially releases jpackage, a tool that takes an app's JARs and turns them into a platform-specific package that can then be installed as is common for that operating system (e.g. with a Linux package manager). Supported formats:

  • Linux: deb and rpm
  • macOS: pkg and dmg
  • Windows: msi and exe

Since my package manager can't handle any of these formats, I didn't give this a try. 🤷🏾‍♂️ Heard good things, though.

JEP 392: Packaging Tool
Building Self-Contained, Installable Java Applications with JEP 343: Packaging Tool by Diogo Carleto
jpackage podcast with Kevin Rushfort


A lot of performance-related work goes into every Java release and 16 is no exception.

Hotspot's Metaspace

Hotspot's handling of class-metadata (called the metaspace - what a word 🚀) improved considerably. The footprint was reduced and it's now quicker to return unused memory to the operating system.

JEP 387: Elastic Metaspace

G1 and Parallel GC

For improvements in G1 and Parallel GC, I recommend this article by Thomas Schatzl:

JDK 16 G1/Parallel GC changes


ZGC implements concurrent thread-stack processing, which reduces the amount of work done in GC safepoints to "essentially nothing of significance".

JEP 376: ZGC: Concurrent Thread-Stack ProcessingZGC | What's new in JDK 16 by Per Liden


Shenandoah saw a number of pacer improvements (JDK-8247593, JDK-8247358, JDK-8247367), SoftMaxHeapSize support (JDK-8252660, concurrent weak reference processing (JDK-8254315) and reworked default heuristics to absorb more allocation spikes (JDK-8255984)


Just like with performance, security is also always being worked. Improvements in Java 16:

  • Crypto

    • SHA-3 signature algorithm support
    • the SunPKCS11 provider now supports SHA-3 algorithms
    • the default PKCS12 algorithms have been strengthened
    • the native elliptic curve implementations have been removed
  • PKI

    • several APIs that represent X.500 distinguished names as String or Principal objects have been deprecated
    • root CA certificates with 1024-bit keys have been removed
    • new Entrust Root CA certificate added
    • new SSL Root CA certificates added
  • TLS

    • TLS support for the EdDSA signature algorithm
    • TLS 1.0 and 1.1 are now disabled by default
  • Signed JAR support for RSASSA-PSS and EdDSA

I'm about as far from a security expert as one could be, so I'm unable to go into details on any of these changes. In fact, I even stole the list 😊 - from Sean Mullan's blog post, so you should probably that one:

JDK 16 Security Enhancements by Sean Mullan

Deprecations & Limitations

There are two very important changes in Java 16 that you need to have on your radar.

Primitive Wrapper Warnings

All eight primitive wrapper classes (Integer, Long, etc.) are now considered value-based classes, their constructors are deprecated for removal (use static valueOf methods instead) and synchronizing on instances triggers a warning. This is done in preparation for Project Valhalla's primitive objects, which I anticipate so much, this deprecation already makes me giddy.

JEP 390: Warnings for Value-Based Classes

Strong Encapsulation By Default

The other one is that the module system finally strongly encapsulates JDK-internal APIs by default. Wait, it didn't before?

No. Until Java 15, code on the class path could access pre-Java-9 packages by default and all you got was a warning if the access used reflection - I'm sure you've seen one of those:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by j9ms.internal.Nimbus
	(file:...) to constructor NimbusLookAndFeel()
WARNING: Please consider reporting this
	to the maintainers of j9ms.internal.Nimbus
WARNING: Use --illegal-access=warn to enable warnings
	of further illegal reflective access operations
WARNING: All illegal access operations will be denied
	in a future release

Funny that it should mention "a future release" - that would be 16. On the newest version, that access would result in an error instead.

This behavior could and can still be configured with --illegal-access:

  • permit:

    • permits static access without warning
    • warning on first reflective access to package
  • warn:

    • permits static access without warning
    • warning on each reflective access
  • debug:

    • permits static access without warning
    • warning plus stack trace on each reflective access
  • deny: illegal access denied (static + reflective)

On Java 9 to 15, permit was the default - on Java 16 it's deny. In a to-be-determined future version of Java , --illegal-access will be removed entirely.

JEP 396: Strongly Encapsulate JDK Internals by Default


That was a lot - Java 16 is one of the larger small releases. In summary, here's what you get for your update:

  • language features:

    • records (finalized)
    • type pattern matching (finalized)
    • sealed classes (preview)
  • APIs:

    • Stream::mapMulti and Stream::toList
    • HttpRequest::newBuilder and BodyPublishers::concat
    • Unix domain socket support for ServerSocketChannel and SocketChannel
    • foreign linker and foreign-memory access APIs (incubating)
    • vector API (incubating)
  • JVM & Tooling:

    • performance improvements in Hotspot, G1, parallel GC, ZGC, Shenandoah
    • remote JFR streaming
    • packaging tool

Not bad, ey?