Definitive Guide To Java 12

A detailed guide to Java 12: migration, version requirements, new features (switch expressions, teeing collectors, indenting/transforming Strings, and more), and JVM changes (default CDS, Shenandoah, G1).

Java 12 will be released in a few days and here's everything you need to know about it. Be it switch expressions, the teeing collector, improved start time thanks to the default CDS archive, or better memory usage due to garbage collection improvements - I'll present each feature in turn. Before we get to that we need to discuss migration, though. Most importantly whether you even want to do that. Why wouldn't you, you ask? Well, read on.

Migrating to Java 12

I assume that you already migrated your code base to Java 11 before going to Java 12. If not, you should do that first (check out this guide for help) - you definitely want to hit that milestone before going further. If you're on 11, all that remains is to answer the question whether you want to bump to 12.

Migrate to Java 11 first

Want to migrate?

Because here's the thing, and it pains me to say that, but you put your project at some risk when you move from the widely long-term-supported Java 11 to Java 12. Stephen Colebourne wrote an entire post on that, but the gist is:

  1. You're unlikely to get free long-term support (LTS) for Java 12 to 16 and even paid LTS is not easy to come by - as far as I know, Azul is the only company offering support for any such version (namely, 13 and 15).
  2. Without LTS, a release gets its last security update about 5 months after its release.
  3. If you can't upgrade to the next major release, you're stuck with an unsupported runtime.
  4. Then, the only remaining option to get security fixes is to downgrade to the newest major release you can get support for (currently, that would be Java 11).

In gaming terms: Java 11 is the last save point before you make it to 17. And it's gotta be a flawless run because if you die on the way, you'll have to start over. (Or retroactively buy save points for 13 and 15 from Azul).

Java 11 is the last save point before you make it to 17

What may keep you from upgrading to the next major release, though? Mostly changed/removed APIs and lacking support by cloud providers:

  • Starting with Java 9, the JDK occasionally sheds deprecated classes and methods (e.g. some finalize implementations in Java 12) and more aggressively reworks implementation details.

If your code uses such APIs, you need to change it. If your dependencies use such APIs, you need to update them. Either way, that can be anywhere between a breeze and a project breaker.

  • If you're running your project in the cloud or even if you're just considering doing that some time in the next two to three years, be careful with an upgrade.

Chances are, you can't freely pick the Java runtime.

And that's just for running on a new release (which suffices to address security concerns) - a lot more things pop up of you actually want to compile against the new version. I'm not going to go into that here, though. Read Stephen's post if you want to hear more grueling explorations of what may go wrong.

I don't want to discourage you, though. Moving with each Java release is rewarding in many respects. You get to benefit from higher productivity, better performance, and avoid the steep cliff of going from 11 to 17. Last but not least, working with Java will simply be more fun if you can constantly explore a few new things here and there. All I'm saying is, consider the update carefully. Take a close, holistic look at your project and ask yourself whether you can update all the things on a regular basis (which has its own benefits of course). If you can, go for it! It will be worth it.

Version Requirements

So you're ready to move to Java 12? Here are the minimum version requirements for the most common IDEs and build tools (although I advise to always pick the newest available version just to be safe):

When compiling to Java 12, update dependencies like Spring, Hibernate, Mockito, etc.

When it comes to compiling to Java 12 bytecode, keep in mind that you will likely have to update all dependencies that rely on bytecode manipulation, e.g. Spring, Hibernate, Mockito, etc.

Preview Features

In the recent past, the JDK gained two mechanisms to expose new functionality before it is set in stone:

Both mechanisms work in a similar way:

  • They allow easy experimentation with new and possibly unstable features.
  • They prevent accidental dependencies on them by requiring command line flags during compilation and execution.

The only incubator module was the reactive HTTP/2 client in Java 9 and 10 (got finalized in 11) and the only preview feature are switch expressions in Java 12 (see below), which means we can stick to the latter. To activate preview features, you need to use the flag --enable-preview.

This is how to do it in Maven:

<plugin>
	<artifactId>maven-compiler-plugin</artifactId>
	<configuration>
		<compilerArgs>
			--enable-preview
		</compilerArgs>
	</configuration>
</plugin>
<plugin>
	<artifactId>maven-surefire-plugin</artifactId>
	<configuration>
		<argLine>--enable-preview</argLine>
	</configuration>
</plugin>

And in Gradle:

compileJava {
	options.compilerArgs += ["--enable-preview"]
}
test {
	jvmArgs '--enable-preview'
}

In IntelliJ, set the language level to 12 (Preview) - Switch expressions. For me, this only works if I set for the module, i.e. setting it just for the project doesn't cut it.

Language Features and API Updates

You've decided to move to Java 12, updated your tools and dependencies, and bumped the compiler's source and target version. 🎉 Here's what you get in exchange.

Switch Expressions

So much has already been written about switch expressions that I don't want to keep you for long. The numerous details aside, it comes down to switch no longer just being a statement (which directs where computation goes, like if), but an expression (which is itself computed to a result, like the conditional/ternary operator ... ? ... : ...). The main use case will be to assign the computed value to a variable:

// https://thedailywtf.com/articles/What_Is_Truth_0x3f_
boolean bool = switch (ternaryBool) {
	case TRUE -> true;
	case FALSE -> false;
	case FILE_NOT_FOUND -> throw new UncheckedIOException(
			"This is ridiculous!",
			new FileNotFoundException());
};

Some of the details I just put aside are greatly anticipated improvements:

  • multiple case labels (e.g. case TRUE, FALSE)
  • no fall-through from one case to the next
  • compiler checks exhaustiveness (that's why there's no default branch in the example)

Definitive Guide To Switch Expressions In Java 12
First Contact with Switch Expressions in Java 12 (video)
JEP 325: Switch Expressions

Remember that this is a preview feature, so be aware that it may change in future releases. Until it's stabilized, don't bet too much of your internal code on it and never publish code that uses it. To use it in experiments, add --enable-preview to compiler and JVM commands.

Don't publish code that uses switch expressions!

Teeing Collectors

Sometimes you need to collect two pieces of information from a stream pipeline, but doing that before Java 12 wasn't exactly comfortable. See this example, where I want to determine a stream's smallest and greatest element, so I can use them to create a range:

Range<Integer> range = Stream
	.of(1, 8, 2, 5)
	.reduce(
		// the initial range - parameters are `min` and `max`
		// in that order, so this range is empty
		Range.of(Integer.MAX_VALUE, Integer.MIN_VALUE),
		// combining an existing range with the next number from the stream
		(_range, number) -> {
			int newMin = Math.min(number, _range.min());
			int newMax = Math.max(number, _range.max());
			return Range.of(newMin, newMax);
		},
		// combining two ranges (needed at the end of a parallel stream)
		(_range1, _range2) -> {
			int newMin = Math.min(_range1.min(), _range2.min());
			int newMax = Math.max(_range1.max(), _range2.max());
			return Range.of(newMin, newMax);
	});

The annoying thing is that there are collectors minBy and maxBy, which could do most of the work for me if only I could use both of them.

From Java 12 on, we can do just that by passing them to the teeing collector (static method Collectors::teeing). Just like Unix' tee command, it forwards each element the stream passes to it to the two specified collectors. Once the stream is exhausted, it combines the two results into a single instance with the third argument you specify, a function. With the teeing collector I can solve the problem as follows:

Range<String> range = Stream
	.of(1, 8, 2, 5)
	.collect(Collectors.teeing(
		// the collectors produce Optional<Integer>
		Collectors.minBy(Integer::compareTo),
		Collectors.maxBy(Integer::compareTo),
		// I wrote a static factory method that creates
		// a range from two Optional<Integer>
		Range::ofOptional))
	.orElseThrow(() -> new IllegalStateException(
		"Non-empty stream was empty."));

Much better, right?

Teeing Collector in Java 12
JDK-8209685: Create Collector which merges results of two other collectors

More Versatile Error Recovery With CompletableFuture

The API of CompletableFuture is already immense (here's a thorough introduction) and in Java 12 it gets a little larger. The reason why it's so big in the first place is the combinatorial explosion of mostly orthogonal requirements:

  • various actions (e.g. thenApply)
  • for result-bearing methods (akin to Stream::map) or CompletableFuture-bearing methods (akin to Stream::flatMap)
  • after one, one of two, or two of two actions complete (e.g. thenApply, applyToEither, and thenCombine)
  • in an unspecified thread, explicitly as a new task (with ...Async suffix), or as a new task with a specific Executor (with ...Async suffix and Executor argument)

Java 12 ticks a few boxes on the feature matrix that were previously empty. They relate to error recovery and are add-ons to the pre-existing method exceptionally(Function<Throwable, T>), which recovers from a failed computation by turning the exception into a normal result. There are five new methods:

  • exceptionallyCompose(Function<Throwable, CompletionStage<T>>) is to exceptionally like Stream::flatMap is to Stream::map: you can pass a function that produces a CompletionStage (supertype of CompletableFuture)
  • new ...Async overloads for exceptionally and exceptionallyCompose, once with the same arguments, once with an additional Executor

This gives us more tools to recover from all the things that can break out there.

JDK-8210971

Indenting and Transforming Strings

Imagine we had raw string literals in Java 12 (alas, we don't). Wouldn't it be handy to quickly fix their indentation?

public String createHtml() {
	// assume four-space indentation
	return html = ```
		<body>
			<h1>Header</h1>
		</body>```
		// two levels of indentation imply
		// eight spaces to get rid of
		.indent(-8);
}

What if, after discovering the new line character for new lines, you decided to go all-in on not abusing spaces and use an indentation character for indentation? (Crazy, I know!) Or what if you prefer to specify the target indentation instead of the change of indentation (which would be stable under refactoring). Given a method that does what you want, you could of course simply call it with the string:

public String createHtml() {
	// assume tab indentation; we want no
	// indentation, so we call our method
	// `String setIndentationToDepth(String, int)`
	// with the raw string and 0
	return html = setIndentationToDepth(```
		<body>
			<h1>Header</h1>
		</body>```,
		0);
	// ugh, I know
}

Compared to indent, that's clumsy. Thankfully, there's a way out:

public String createHtml() {
	// same as before...
	return html = ```
		<body>
			<h1>Header</h1>
		</body>```
		// ... but here we call `transform`
		// which expects a `Function<String, T>`
		.transform(s -> setIndentationToDepth(s, 0));
}

The method transform simply passes the instance that it's called on to the specified function. Neat!

Now, as you may have noticed, we didn't get raw string literals in Java 12, so why are these methods still in there? Good question. For once, I didn't scour the mailing lists, but Dustin Marx did and I highly recommend you read his article on the topic if you want to know more about String::transform:

The Brief but Complicated History of JDK 12's String::transform Method

An interesting tidbit is that transform may show up in other places. For example on Stream or on Optional, which means you could apply your methods that modify a Stream or Optional in a fluent call chain. Inspired by JDK-8140283:

// given this method...
public Stream<T> maybeAddFilter(Stream<T> s) {
	if (condition)
		return s.filter(...);
	else
		return s;
}

// ... and this pipeline ...
source.stream()
	.map(...)
	.collect(toList());

// ... this is how to call `maybeAddFilter`:
maybeAddFilter(
	source.stream()
		.map(...))
	.collect(toList());

// once again: ugh!
// what about `transform`?
source.stream()
	.map()
	.transform(this::maybeAddFilter)
	.collect(toList());

That would be pretty awesome!

Compact Number Format

Need to format upvotes, subscribers, or followers in a social-media-compliant manner, where 5412 becomes 5.4k? The new CompactNumberFormat is there for you:

// `CompactNumberFormat` has a constructor, but getting an instance
// from `NumberFormat::getCompactNumberInstance` is easier
NumberFormat followers = NumberFormat
	.getCompactNumberInstance(new Locale("en", "US"), Style.SHORT);
followers.setMaximumFractionDigits(1);
// prints "5.4k followers"
System.out.println(followers.format(5412) + " followers");

Compact Number Formatting Comes to JDK 12

Mismatching Files

The utility class Files got a new utility. The method mismatch(Path, Path) compares the two specified files and returns the index of the first byte where they differ or -1 if they don't:

long mismatchIndex = Files.mismatch(path1, path2);
boolean match = mismatchIndex == -1;
if (match)
	System.out.println("Files match");
else
	System.out.println(
		"Files first difference is at index "
			+ mismatchIndex);

JVM Improvements

Besides the language and APIs, the JVM has of course also seen improvements.

Default CDS Archives

What does the JVM do when it needs to load a class?

  • looks the class up in a JAR
  • loads the bytes
  • verifies the bytecode
  • pulls it into an internal data structure

For each used class, this process is repeated every time the JVM is relaunched, even though, as long as the class is unchanged, it always leads to the same result. Class-data sharing (CDS) removes the redundancy by storing the internal data structure, the so-called class-data archive, in a file and then memory-mapping it on future launches, so the classes don't have to be loaded again. There are two kinds of CDS:

  • "regular" CDS archives JDK classes
  • AppCDS archives JDK and application classes

CDS has been in the JDK for a while and Java 10 unlocked application class-data sharing as a free feature. Now Java 12 ships with an archive for the JDK classes and uses it by default.

Java 12 uses a CDS archive for JDK classes

You can easily observe the effect by launching an application with Java 12, once without additional command line options (CDS is on by default) and once with -Xshare:off (turning CDS off). On my laptop, launching a simple "Hello, World" takes ~50 ms with and ~100ms without CDS. If I launch it as a single source-file...

java HelloWorld.java
java -Xshare:off HelloWorld.java

... it's ~120ms and ~170ms, so apparently the compiler doesn't need a ton of classes.

There's nothing you need to do to benefit from default CDS archives, but you may notice a related message when launching a Java program from your IDE:

OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended

The archive is essentially a cache and the JVM needs to invalidate classes that may be different if loaded by a class loader than if taken from the archive. If the bootstrap class path is tampered with, only archived classes that were originally loaded by the boot class loader are guaranteed to be correct and so the JVM sticks to them. That's what the message tells you.

Improve Launch Times On Java 10 With Application Class-Data Sharing
JEP 341: Default CDS Archives

Garbage Collection

Shenandoah

Red Hat is working on a new low-pause-time garbage collector, dubbed Shenandoah:

Shenandoah [...] reduces GC pause times by doing evacuation work concurrently with the running Java threads.

Pause times with Shenandoah are independent of heap size, meaning you will have the same consistent pause times whether your heap is 200 MB or 200 GB. [...] Shenandoah is an appropriate algorithm for applications which value responsiveness and predictable short pauses.

Java 12 ships an experimental version of it.

Shenandoah GC: The Garbage Collector That Could (talk at JavaZone by Aleksey Shipilev)
JEP 189: Shenandoah: A Low-Pause-Time Garbage Collector

G1

Oracle's Garbage First (G1) collector also sees continuous improvements, most notably that it now promptly returns unused memory to the operating system (something Shenandoah does as well):

G1 only returns memory from the Java heap at either a full GC or during a concurrent cycle.

Since G1 tries hard to completely avoid full GCs, and only triggers a concurrent cycle based on Java heap occupancy and allocation activity, it will not return Java heap memory in many cases unless forced to do so externally.

From Java 12 on, G1 uses phases during which the application shows little activity to trigger/continue a concurrent cycle, thus using the existing functionality to make unused memory available to other processes more quickly. This is particularly interesting for applications that run in the cloud because it enables them to be more elastic with their resource requirements, thus becoming cheaper to run.

JEP 344: Abortable Mixed Collections for G1
JEP 346: Promptly Return Unused Committed Memory from G1

Constants API

Apparently an improvement that helps bytecode parsers and manipulation libraries, compilers and "offline transformers" (like jlink) as well as some code in the JDK, but I'm in over my head here. You've got to read the JEP yourself. 😋

JEP 334: JVM Constants API

Security Enhancements

Java 12 ships with a number of security enhancements that Oracle's Sean Mullan discusses on his blog:

JDK 12 Security Enhancements

Removed And Deprecated

Removed:

  • finalize() on FileInputStream, FileOutputStream, Inflater, Deflater, ZipFile
  • overrides of getCause() in ClassNotFoundException, ExceptionInInitializerError, UndeclaredThrowableException, PrivilegedActionException

The only notable new deprecations I found are in sun.misc.Unsafe:

  • getObject, getObjectVolatile, getObjectAcquire, getObjectOpaque
  • putObject, putObjectVolatile, putObjectOpaque, putObjectRelease
  • getAndSetObject, getAndSetObjectAcquire, getAndSetObjectRelease
  • compareAndSetObject, compareAndExchangeObject, compareAndExchangeObjectAcquire, compareAndExchangeObjectRelease
  • weakCompareAndSetObject, weakCompareAndSetObjectAcquire, weakCompareAndSetObjectPlain, weakCompareAndSetObjectRelease

All of these methods are marked for removal.

Summary

Executive summary on migration considerations:

  • no known technical challenges when moving to 12 except increased bytecode level
  • free LTS for Java 12 - 16 is unlikely
  • commercial LTS for 12 - 16 is sparse
  • moving from 11 to 12 likely implies that you have to make it all the way to 17

New features:

  • switch as an expression (preview feature)
  • use two stream collectors and combine their results with Collectors::teeing
  • more varied CompletableFuture error recovery with exceptionallyCompose, exceptionallyComposeAsync, and exceptionallyAsync
  • manage a multi-line string's indentation with String::indent
  • call string processing methods in a method chain with String::transform
  • format numbers as "5.4k" with CompactNumberFormat that you get from NumberFormat::getCompactNumberInstance
  • compare files byte by byte with Files::mismatch

Improved JVM:

  • shorter JVM boot times thanks to default class-data sharing for JDK classes
  • new garbage collector Shenandoah
  • improvements to default garbage collector G1
  • something, something, JVM constants

Have fun when you delve into Java 12. (I'll show myself out.)