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.
▚Preparations
▚Version Requirements for Java 16
Here are the most common IDEs' and build tools' minimum version requirements for Java 16 (although I advise to always pick the newest available version just to be safe):
- 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: generally speaking 3.5.0, but e.g. this bug was only fixed in 3.6.1
- 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 -->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>16</release>
<compilerArgs>
--enable-preview
</compilerArgs>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>--enable-preview</argLine>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<argLine>--enable-preview</argLine>
</configuration>
</plugin>
// 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.
▚Records
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;
}
@Override
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;
}
@Override
public int hashCode() {
return Objects.hash(low, high);
}
@Override
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
, andtoString
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)
elephant.eatPlants();
else if (animal instanceof Tiger tiger)
tiger.eatMeat();
}
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
▚Stream
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 R
s 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))
.forEach(System.out::print);
// 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 flatMap
s 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)
.map(Integer::toString)
.toList();
So is this like collect(Collectors.toList())
?
Not quite:
- it can be more performant
- the returned list is shallowly immutable (official terminology is unmodifiable, but I don't like that)
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
▚HTTP API
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 BodyPublisher
s, 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.
▚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
.of(System.getProperty("user.home"))
.resolve("server.socket");
UnixDomainSocketAddress address =
UnixDomainSocketAddress.of(socketFile);
// server
ServerSocketChannel serverChannel = ServerSocketChannel
.open(StandardProtocolFamily.UNIX);
serverChannel.bind(address);
SocketChannel channel = serverChannel.accept();
// send/receive messages...
// client
SocketChannel channel = SocketChannel
.open(StandardProtocolFamily.UNIX);
channel.connect(address);
// 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
▚Vector
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
▚Tooling
▚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
andrpm
- macOS:
pkg
anddmg
- Windows:
msi
andexe
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
▚Performance
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.
▚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
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 Processing ⇝ ZGC | What's new in JDK 16 by Per Liden
▚Shenandoah
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)
▚Security
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
java.security.cert
APIs that represent X.500 distinguished names asString
orPrincipal
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
- several
- 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
▚Reflection
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
andStream::toList
HttpRequest::newBuilder
andBodyPublishers::concat
- Unix domain socket support for
ServerSocketChannel
andSocketChannel
- 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?