Skip to content

Java 9 Features (with Examples)

Cup of black coffee next to a coffee grinder, a cezve, and coffee beans on a wooden table – Java 9 Features

Java 9 was released on September 21, 2017 – three and a half years after Java 8. It was the last release in the old style: a bundle of 91 JDK Enhancement Proposals (JEPs) that was only released once the largest of them was finished – the module system. Half a year later, Java 10 started the six-month release cycle – and the releases became much smaller.

Java 9 was not a long-term support release. Support ended when Java 10 was released. Still, the look back is worth it: Almost everything you take for granted in Java today – List.of(), jshell, the module system, the Flow API, takeWhile(), private methods in interfaces – arrived with this release.

I have sorted the 91 JEPs by relevance for everyday development work: First come the changes to the language, then the module system and the extensions to the class library, followed by the tools, the JVM, incubator features, and deprecations. The 49 JEPs you do not need to know as a Java developer are grouped by topic at the end – each of them with one sentence and a link.

For the chapter headings, I use the JEPs’ original titles, so you recognize them right away.

You can download the reference implementation of Java 9 here. I recommend that only for trying out the examples – for production code, use a current LTS release.

Small Language Changes: Milling Project Coin – JEP 213

Java 7 brought a handful of small language changes under the name “Project Coin”: the diamond operator, try-with-resources, strings in switch. JDK Enhancement Proposal 213 mills the rough edges off these changes – and additionally allows private methods in interfaces.

There are five changes in total. I show them in order of relevance.

Private Methods in Interfaces

Since Java 8, interfaces may contain default and static methods with an implementation. What was missing was a way to extract code shared by several default methods without publishing it as part of the public API.

Since Java 9, a private method does that:

public interface Greeter {
  default String greet(String name) {
    return prefix() + "Hello " + name + "!";
  }

  default String greetAll(List<String> names) {
    return prefix() + "Hello " + String.join(", ", names) + "!";
  }

  private String prefix() {
    return "[" + getClass().getSimpleName() + "] ";
  }
}

The method prefix() is not visible from outside – not even to implementing classes. Private static methods are allowed as well.

Effectively Final Variables in try-with-resources

Until now, every resource in a try-with-resources statement had to be declared in a new variable – even if it was already stored in one:

BufferedReader reader = new BufferedReader(new StringReader("foo\nbar"));

try (BufferedReader r = reader) {
  System.out.println(r.readLine());
}

Since Java 9, it is enough for the variable to be final or effectively final (i.e., not modified after initialization):

try (reader) {
  System.out.println(reader.readLine());
}

In this example, that gains little – the declaration of reader could just as well have gone into the head of the try. The benefit shows when the variable comes from outside, e.g., as a method parameter:

void printFirstLine(BufferedReader reader) throws IOException {
  try (reader) {
    System.out.println(reader.readLine());
  }
}

Until now, you had to copy the parameter into a new variable just to have it closed at the end.

Diamond Operator with Anonymous Classes

In Java 7 and 8, the diamond operator <> could not be combined with anonymous classes. Java 9 lifts this restriction:

Comparator<String> byLength = new Comparator<>() {
  @Override
  public int compare(String a, String b) {
    return Integer.compare(a.length(), b.length());
  }
};

Before Java 9, you had to write new Comparator<String>() here.

@SafeVarargs on Private Methods

The @SafeVarargs annotation suppresses the “unchecked generic array creation” warning for varargs parameters with a generic type. Until now, it was only allowed on constructors and on static and final methods – that is, on methods that cannot be overridden.

Private instance methods cannot be overridden either. Since Java 9, the annotation is therefore allowed there, too:

@SafeVarargs
private void printAll(List<String>... lists) {
  System.out.println(Arrays.toString(lists));
}

Underscore Is No Longer an Identifier

Java 8 already warned against using a single underscore as a variable name. Since Java 9, it is a compiler error:

Underscore.java:3: error: as of release 9, '_' is a keyword, and may not be used as an identifier
    int _ = 5;
        ^

The JDK developers thus prepared a feature ten years in advance: Java 8 warned in 2014, Java 9 forbade the identifier – and since Java 22, the underscore stands for unnamed variables and patterns.

Java Platform Module System – JEP 261

The module system is the feature Java 9 took so long for. Work on it started in 2008 under the name Project Jigsaw; the specification is JSR 376, the implementation is JDK Enhancement Proposal 261. Five more JEPs belong to it – 200, 201, 220, 260, and 282 – and I present them in the following subsections.

The module system solves three problems the classpath could never solve:

  • Reliable configuration: A module declares which other modules it requires. If one is missing, the JVM aborts at startup – not at some point during runtime with a NoClassDefFoundError.
  • Strong encapsulation: A module declares which packages it exports. All other packages are invisible to code outside the module – even if the classes in them are public.
  • No split packages: Two modules must not contain the same package. On the classpath, two JARs could bring the same class, and which one got loaded was decided by their order on the classpath – the infamous JAR hell. The module system rejects such a configuration: If two modules eu.happycoders.greeting and eu.happycoders.farewell both contain the package eu.happycoders.shared, the compiler reports “module eu.happycoders.app reads package eu.happycoders.shared from both eu.happycoders.greeting and eu.happycoders.farewell” – and the JVM does not even start.

module-info.java: Defining Modules

A module is a directory (or a JAR) with a module-info.java file in its root directory. Here is a module that exports one package:

module eu.happycoders.greeting {
  exports eu.happycoders.greeting.api;
}

And here is a second module that requires the first one:

module eu.happycoders.app {
  requires eu.happycoders.greeting;
}

The requires names a module, the exports a package. The class App in the second module can now access the class Greeting in the exported package eu.happycoders.greeting.api. If the first module did not export the package, Greeting would not be visible to App – the compiler would reject the access with “package eu.happycoders.greeting.api is not visible”.

Besides requires and exports, the module declaration knows three more keywords:

  • requires transitive passes the dependency on to all modules that require your module. This is necessary when types of the dependency appear in your own public API.
  • opens opens a package for deep reflection (i.e., setAccessible(true)) – without making it visible at compile time. Frameworks like Hibernate or Jackson need this to read and write private fields of your classes without getters and setters.
  • uses and provides ... with declare services that are found via the ServiceLoader. The JDK itself uses this extensively; the java.sql module, for example, declares uses java.sql.Driver. That means: At runtime, java.sql loads all modules that declare provides java.sql.Driver with ... – without knowing a single driver itself. Your JDBC driver simply sits on the module path, and DriverManager finds it.

Modules are compiled and launched via the module path – the counterpart to the classpath:

$ javac -d out --module-source-path src $(find src -name "*.java")
$ java --module-path out --module eu.happycoders.app/eu.happycoders.app.App

Hello Module!

The options also exist in short form: -p for --module-path and -m for --module.

Code that lies on the classpath without a module-info.java ends up in the so-called unnamed module. It reads all other modules and exports all of its packages. That is why existing applications keep running on Java 9 – modularization is optional.

For the cases in which module boundaries get in the way, there are command-line options that soften them:

  • --add-modules adds modules that no other module requests via requires – e.g., an incubator module or a service provider known only to the ServiceLoader.
  • --add-exports <module>/<package>=<target-module> exports a package retroactively.
  • --add-opens <module>/<package>=<target-module> opens a package retroactively for deep reflection.
  • --add-reads <module>=<target-module> allows a module to use another one as if a requires stood in its module declaration – typical for tests, so the test framework does not end up as a requires in production code.

ALL-UNNAMED is allowed as the target module – the option then applies to all code on the classpath. These options are meant for transition phases and tests, not for the permanent configuration of an application.

The Modular JDK – JEP 200 and 201

The JDK itself was split into modules by JEP 200. The naming rule is simple: Modules whose specification is governed by the Java Community Process are called java.*; everything else is called jdk.*. At the very bottom of the module graph is java.base with packages such as java.lang, java.util, and java.io – every other module depends on it, and it depends on nothing.

java --list-modules shows you which modules your JDK contains. And java --describe-module tells you what a single module exports and requires:

$ java --describe-module java.sql

java.sql@21.0.2
exports java.sql
exports javax.sql
requires java.transaction.xa transitive
requires java.base mandated
requires java.xml transitive
requires java.logging transitive
uses java.sql.Driver

JEP 201 is the groundwork for this: It reorganized the JDK’s source code into a module structure so that module boundaries can be checked while building the JDK.

Modular Run-Time Images – JEP 220

Anyone who looked at a JDK directory before Java 9 found a jre subdirectory and, inside it, the file rt.jar with the entire class library. Both are gone since Java 9.

JEP 220 reorganized the directory structure:

  • There is no longer a distinction between JRE and JDK. A JDK is a run-time image that happens to also contain the development tools.
  • The classes of all modules are stored in a single container format optimized for fast access in the lib directory – no longer in JAR files.
  • Configuration files that you are allowed to edit live in the new conf directory. Everything else in lib is an implementation detail.
  • The release file in the root directory lists all contained modules under MODULES.

Since rt.jar no longer exists, APIs like ClassLoader.getSystemResource() no longer return jar: URLs for JDK classes, but URLs with the new scheme jrt: – for example, jrt:/java.base/java/lang/Class.class. Via an NIO file system for this scheme, you can browse the contents of a run-time image without knowing its internal format.

If the JDK consists of modules and so does your application, one thought suggests itself: Why not build a run-time image that contains only the required modules?

That is exactly what jlink, defined in JEP 282, does. For the example application above, the call looks like this:

$ jlink --module-path out --add-modules eu.happycoders.app --output image

The result is a complete run-time image with its own java command that contains only three modules – java.base and the two application modules:

$ ./image/bin/java --list-modules

eu.happycoders.app
eu.happycoders.greeting
java.base@21.0.2

On my machine, this image is 30 MB (with the options --strip-debug, --no-man-pages, --no-header-files, and --compress zip-6), whereas the complete JDK 21 is 327 MB. A container image that holds your program together with its runtime shrinks by around 300 MB – and the same goes for the installer of a desktop application that ships its own runtime.

However, jlink requires that all dependencies are real modules. jlink cannot process a JAR without module-info.java (a so-called automatic module). In practice, this is the most common hurdle.

The Java Packager (javapackager) was adapted to the module system by JEP 275 and has since generated the bundled runtime with jlink. It did not benefit for long: In Java 11, the Java Packager was removed from the JDK together with JavaFX.

Encapsulate Most Internal APIs – JEP 260

With the module system, the JDK could have made all internal packages – sun.*, com.sun.*, jdk.internal.* – inaccessible in one go. That would have broken a large part of the Java ecosystem, since many libraries used sun.misc.Unsafe and other internals.

JEP 260 therefore divides the internal APIs into three groups:

  1. Non-critical internal APIs – those hardly used outside the JDK, or easily replaced by a supported API or a library, e.g., sun.misc.BASE64Decoder – have been inaccessible at compile time since Java 9, whether a replacement exists or not. Anyone who continues to use them must set --add-exports.
  2. Critical internal APIs for which a supported replacement existed in Java 8 are inaccessible as well.
  3. Critical internal APIs without a replacement – first and foremost sun.misc.Unsafe, plus sun.misc.Signal and sun.reflect.ReflectionFactory – remain accessible in the jdk.unsupported module. If the replacement only arrives with Java 9 (like VarHandle for Unsafe), the old API is deprecated and encapsulated or removed in a later release.

The three groups apply at compile time. At runtime, Java 9 was more lenient: The --illegal-access option defaulted to permit, so deep reflection on JDK internals continued to work and merely issued a warning on the first access (WARNING: An illegal reflective access operation has occurred). With --illegal-access=warn or debug, you could have all accesses displayed; with deny, you could test the future behavior.

The end of this transition phase came in two steps: Java 16 changed the default to deny, and Java 17 removed the option entirely. Since then, only --add-opens helps – or better: a library version that no longer needs internals.

The jdeps tool with the --jdk-internals option tells you which JDK internals your code or your dependencies use.

Convenience Factory Methods for Collections – JEP 269

Creating a small, unmodifiable list was surprisingly cumbersome until Java 8:

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list = Collections.unmodifiableList(list);

Or shorter, but not more understandable: Collections.unmodifiableList(Arrays.asList("a", "b", "c")). For sets and maps, it was even more cumbersome.

On top of that, Collections.unmodifiableList() does not create a new list but only an unmodifiable view of the one passed in. Anyone still holding a reference to the original list can keep changing it – and every change is visible through the view.

JEP 269 brings static factory methods on the List, Set, and Map interfaces:

List<String> list = List.of("a", "b", "c");
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("one", 1, "two", 2, "three", 3);

Map.of() exists for up to ten key-value pairs. For larger maps, you use Map.ofEntries() together with Map.entry():

import static java.util.Map.entry;

Map<String, Integer> map = Map.ofEntries(
    entry("one", 1),
    entry("two", 2),
    entry("three", 3));

The collections created this way have four properties you should know:

  • They are unmodifiable. add(), put(), remove(), and all other mutating methods throw an UnsupportedOperationException.
  • They do not allow null – neither as an element nor as a key or value. The attempt ends with a NullPointerException.
  • Set.of() and Map.of() reject duplicates – with an IllegalArgumentException (“duplicate element”). With Set.of(1, 1), that is probably a programming error, and you would rather see it immediately.
  • The iteration order of Set.of() and Map.of() is undefined – and changes from one JVM start to the next. Anyone who relies on an order will find the bug in the very first test. That is intentional.

Unlike the Collections.unmodifiable...() wrappers, these are not views of another collection but compact implementations tailored to small sizes. A map created with Map.of() or Map.ofEntries(), for example, does without the bucket array and the Node objects that a HashMap would allocate for a few elements.

What was still missing was a way to copy an existing collection into an unmodifiable one. Java 10 brought that with List.copyOf(), Set.copyOf(), and Map.copyOf().

New Stream Methods

The Stream API from Java 8 gets four additions in Java 9. They are not defined in any JEP, but they are more useful in everyday work than some things that have a JEP.

takeWhile() returns all elements from the beginning of the stream until the predicate evaluates to false for the first time – and dropWhile() skips exactly those elements:

Stream.of(1, 2, 3, 4, 5, 1, 2)
    .takeWhile(i -> i < 4)
    .forEach(System.out::print);
123
Stream.of(1, 2, 3, 4, 5, 1, 2)
    .dropWhile(i -> i < 4)
    .forEach(System.out::print);
4512

You can see in the second example: The 1 and the 2 at the end are retained. Unlike filter(), dropWhile() only tests the predicate until it fails once.

Since Java 9, Stream.iterate() comes with an additional predicate that terminates the stream – the replacement for the combination of iterate() and limit() when you know the termination condition rather than the number of elements:

Stream.iterate(1, i -> i <= 100, i -> i * 2)
    .forEach(System.out::println);
1
2
4
8
16
32
64

Stream.ofNullable() creates a stream with one element – or an empty stream if the argument is null. That is handy in flatMap() calls that would otherwise contain a null check. In the following example, config contains only an entry for "host". config.get("port") therefore returns null, Stream.ofNullable() turns that into an empty stream, and the result contains only the value for "host":

Map<String, String> config = Map.of("host", "localhost");

List<String> values = Stream.of("host", "port")
    .flatMap(key -> Stream.ofNullable(config.get(key)))
    .collect(Collectors.toList());
[localhost]

In addition, there are two new collectors: Collectors.filtering() and Collectors.flatMapping(). Both are intended for use as downstream collectors in groupingBy() or partitioningBy(). The following example groups words by their first letter and keeps only the words with more than five letters in each group:

List<String> words = List.of("apple", "fig", "banana", "cherry");

Map<Character, List<String>> longWords = words.stream()
    .collect(Collectors.groupingBy(word -> word.charAt(0),
        Collectors.filtering(word -> word.length() > 5, Collectors.toList())));
{a=[], b=[banana], c=[cherry], f=[]}

The difference from a filter() before the groupingBy(): The groups a and f, in which no word passes the filter, are retained as empty lists. With filter() in front, they would disappear from the map, and the result would be {b=[banana], c=[cherry]}. Collectors.flatMapping() does the same for flatMap(): Each element is expanded into a stream whose elements end up in the group.

New Optional Methods

Optional also gets three methods that developers had been wishing for since Java 8.

ifPresentOrElse() extends ifPresent() with the case that no value is present:

optional.ifPresentOrElse(
    value -> System.out.println("Value: " + value),
    () -> System.out.println("no value"));

or() returns another Optional if the own one is empty – in contrast to orElse(), not the value but again an Optional you can continue working with:

Optional<String> result = fromCache(key).or(() -> fromDatabase(key));

And stream() converts an Optional into a stream with zero or one element. This lets you reduce a stream of optionals to the present values in a single step:

List<String> values = optionals.stream()
    .flatMap(Optional::stream)
    .collect(Collectors.toList());

Reactive Streams and CompletableFuture Enhancements – JEP 266

JEP 266 carries the unassuming title “More Concurrency Updates” and contains two things that would have deserved a title of their own.

Flow API

The new class java.util.concurrent.Flow contains four interfaces: Publisher, Subscriber, Subscription, and Processor. They correspond one-to-one to the Reactive Streams specification – the standard that RxJava, Project Reactor, and Akka Streams had agreed upon so that their components can work together.

The basic principle is called backpressure: A Subscriber tells the Publisher via Subscription.request(n) how many elements it can process. The publisher never sends more than requested. This way, a fast producer cannot flood a slow consumer.

The JDK ships an implementation of the publisher, SubmissionPublisher. Here is a subscriber that requests the elements one at a time:

SubmissionPublisher<String> publisher = new SubmissionPublisher<>();

publisher.subscribe(new Flow.Subscriber<>() {
  private Flow.Subscription subscription;

  @Override
  public void onSubscribe(Flow.Subscription subscription) {
    this.subscription = subscription;
    subscription.request(1);
  }

  @Override
  public void onNext(String item) {
    System.out.println("Received: " + item);
    subscription.request(1);
  }

  @Override
  public void onError(Throwable throwable) {
    throwable.printStackTrace();
  }

  @Override
  public void onComplete() {
    System.out.println("Done");
  }
});

publisher.submit("foo");
publisher.submit("bar");
publisher.submit("baz");
publisher.close();
Received: foo
Received: bar
Received: baz
Done

The elements are delivered asynchronously in a thread of the ForkJoinPool – so in a main() program, you have to wait briefly before it ends.

For application code, you will continue to use a library like Project Reactor; the four interfaces offer no operators like map() or filter(). Their purpose is a different one: Through them, one library hands data to another without depending on it. The HTTP Client from Java 11, for example, uses them for request and response bodies: A BodyPublisher is a Flow.Publisher<ByteBuffer>, and any library that implements the Flow interfaces can supply or consume a body without knowing the HTTP Client.

New CompletableFuture Methods

CompletableFuture gets methods for everything that has to do with time:

  • orTimeout(timeout, unit) makes the future fail with a TimeoutException after the time has elapsed.
  • completeOnTimeout(value, timeout, unit) completes it with a default value instead.
  • CompletableFuture.delayedExecutor(delay, unit) returns an Executor that starts tasks only after the delay. Together with supplyAsync(), this yields a delayed future.

An example of completeOnTimeout():

CompletableFuture<String> future = fetchFromRemoteService();
String result = future.completeOnTimeout("default", 500, TimeUnit.MILLISECONDS).get();

If the service does not respond within 500 milliseconds, result contains the string "default".

In addition, there are four more methods:

  • failedFuture(exception) creates an already failed future – the counterpart to completedFuture(value).
  • completeAsync(supplier) completes an existing future with the result of the supplier, which is executed asynchronously for that purpose – in the default executor or in one you pass as the second argument.
  • copy() returns a new future that receives the same value (or the same exception) as the original – like thenApply(x -> x). This lets you hand out a future that callers can attach follow-up actions to, without them being able to complete the original via complete().
  • newIncompleteFuture() and defaultExecutor() are meant for subclasses: The first creates the futures returned by thenApply(), thenCompose(), and the other chaining methods, so a subclass can return its own type. The second determines the executor used by the ...Async() methods without an executor argument – by default, the common pool of the ForkJoinPool.

Process API Updates – JEP 102

With ProcessBuilder and Process, you could start a process until Java 8, redirect its input and output, terminate it, and wait for it to end. What was missing was information about the process – the class did not even reveal its process ID. Anyone who needed it resorted to reflection on platform-specific fields – or to native code.

JEP 102 introduces the ProcessHandle interface. While Process continues to stand for a process your JVM has started – including access to its streams, ProcessHandle stands for an arbitrary operating system process: your own, one you started, or a foreign one. A handle knows the process ID, metadata, and the process tree, but no streams:

ProcessHandle current = ProcessHandle.current();
System.out.println("PID: " + current.pid());

ProcessHandle.Info info = current.info();
System.out.println("Command: " + info.command().orElse("?"));
System.out.println("Start: " + info.startInstant().orElse(null));
System.out.println("User: " + info.user().orElse("?"));
PID: 66097
Command: /Users/sven/.sdkman/candidates/java/21.0.2-open/bin/java
Start: 2026-09-18T11:38:00.970Z
User: sven

The methods of Info return Optional because not every operating system reveals every piece of information.

Via parent(), children(), and descendants(), you navigate the process tree; with destroy(), you terminate a process; and onExit() returns a CompletableFuture that is triggered after the process has ended:

Process process = new ProcessBuilder("sleep", "1").start();
process.toHandle().onExit()
    .thenAccept(h -> System.out.println("Process " + h.pid() + " exited"));

From a Process, you get to its ProcessHandle with toHandle(). Process has also received the query methods pid(), info(), onExit(), children(), and descendants() itself – they delegate to the handle, so you do not have to switch for a process you started yourself.

Stack-Walking API – JEP 259

Anyone who wanted to know where a method was called from had two options until now: Thread.getStackTrace() and new Throwable().getStackTrace(). Both create a snapshot of the entire stack as a StackTraceElement array – even if you are only interested in the top two frames.

In a Spring or Jakarta EE application, that quickly amounts to a few hundred frames. Each of them becomes a StackTraceElement object on the heap that the JVM first has to fill and the garbage collector has to collect shortly afterwards – CPU time and memory for data nobody reads. A logger that writes the location into every line pays this price for every single log line.

A second problem: Both return class names as strings, not Class objects. Anyone who needs the class itself – for instance, to load further classes with its class loader – cannot reliably recover it from the name, because Class.forName() searches with the caller’s class loader, not with that of the class being looked for.

The Stack-Walking API turns this into a stream that materializes frames only when you read them. A logger that writes the calling class and method into every line thus only needs to search up to the first frame outside its own class:

class Logger {
  static void log(String message) {
    Optional<StackFrame> caller = StackWalker.getInstance()
        .walk(frames -> frames
            .filter(frame -> !frame.getClassName().equals(Logger.class.getName()))
            .findFirst());
    System.out.println(
        caller.map(f -> f.getClassName() + "." + f.getMethodName()).orElse("?")
            + ": " + message);
  }
}

Called from a method App.run(), Logger.log("Hello") prints:

App.run: Hello

findFirst() ends the stream after the second frame – the remaining frames are never created. That is exactly what Thread.getStackTrace() cannot do: It always returns the whole array.

The stream is only valid inside the function passed to walk() – hence the lambda form. While walk() is running, the JVM keeps the thread’s stack unchanged. As soon as walk() returns, it may change the frames again, e.g., when the JIT compiler deoptimizes a method; the frames in the stream would then no longer be those of the stack.

For the case that you only need the calling class – for instance, to use the caller’s class loader as ResourceBundle.getBundle() does – there is a shortcut:

class Util {
  static Class<?> caller() {
    return StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass();
  }
}

getCallerClass() returns the class from which the enclosing method was called: If App.run() calls Util.caller(), it returns App.class.

For this to work, the StackWalker must be created with the RETAIN_CLASS_REFERENCE option. Without the option, getCallerClass() throws an UnsupportedOperationException.

The option is a deliberate opt-in: A Class object gives access to the class loader and thus to more than just a name. That is why the JVM checks the permission for it once when the StackWalker is created (under a security manager, the RuntimePermission getStackWalkerWithClassReference) and not for every frame.

Logging frameworks have benefited the most: Until now, they had to either read the calling class from the complete stack array for every log line or use the JDK-internal sun.reflect.Reflection.getCallerClass(). With StackWalker, there is an official API for this for the first time – and JEP 260 keeps the internal one only as a transitional solution in the jdk.unsupported module.

Variable Handles – JEP 193

Anyone who needed atomic operations on a field – a compareAndSet(), say – had the choice until Java 8 between an AtomicInteger instance (or AtomicLong or AtomicReference – in any case, an additional object per field), an AtomicIntegerFieldUpdater (slow), and sun.misc.Unsafe (unsupported and dangerous: Its methods write to arbitrary memory addresses without type or bounds checks – a wrong offset corrupts the heap or crashes the JVM).

JEP 193 brings an official API for exactly these operations with VarHandle. A VarHandle is a typed reference to a variable – an instance field, a static field, or an array element – through which you access the variable with different access modes: get() and set() in plain, opaque, acquire/release, or volatile semantics (see the box below), plus compareAndSet(), getAndAdd(), getAndBitwiseOr(), and more.

You create a VarHandle via a Lookup, just like a MethodHandle:

public class Counter {
  private static final VarHandle COUNT;

  static {
    try {
      COUNT = MethodHandles.lookup().findVarHandle(Counter.class, "count", int.class);
    } catch (ReflectiveOperationException e) {
      throw new ExceptionInInitializerError(e);
    }
  }

  private volatile int count;

  void increment() {
    COUNT.getAndAdd(this, 1);
  }

  boolean resetIfEquals(int expected) {
    return COUNT.compareAndSet(this, expected, 0);
  }
}

The methods for the access modes – get(), set(), compareAndSet(), getAndAdd(), and the rest – are signature polymorphic, like MethodHandle.invoke(). That means: They are declared with Object..., getAndAdd(), for example, as Object getAndAdd(Object... args). The compiler nevertheless generates a signature from the actual types for each call: COUNT.getAndAdd(this, 1) in the example above becomes a call with the signature (Counter, int)void in the bytecode – the 1 is not boxed into an Integer, and no Object[] is created for the varargs. The notation is that of the MethodType class: the parameter types in the parentheses, the return type behind them, here void.

For the JIT compiler to fully optimize these calls, the VarHandle should be stored in a static final field: It is then a constant for the compiler, and the type checks of the calls fall away.

VarHandle is the official replacement for most Unsafe methods. That started the usual path towards retirement for the old methods: Java 23 marked the memory-access methods of sun.misc.Unsafe as “deprecated for removal”; Java 24 issues a warning when they are used.

Enhanced Method Handles – JEP 274

JEP 274 is aimed at authors of runtimes for other JVM languages – Nashorn, JRuby, Groovy – and of bytecode frameworks that assemble code from MethodHandles. It extends MethodHandles with combinators that express control flow without generating bytecode: whileLoop(), doWhileLoop(), countedLoop(), and iteratedLoop() build a loop from handles for initialization, condition, and body, and tryFinally() combines a handle with a cleanup action that also runs in case of an exception:

MethodHandle target = lookup.findStatic(Demo.class, "work",
    MethodType.methodType(String.class, String.class));
MethodHandle cleanup = lookup.findStatic(Demo.class, "cleanup",
    MethodType.methodType(String.class, Throwable.class, String.class, String.class));

MethodHandle guarded = MethodHandles.tryFinally(target, cleanup);
String result = (String) guarded.invoke("hello");

cleanup() receives the exception (or null), the result of work(), and its argument, and returns the final result. In addition, there are variants of foldArguments(), asSpreader(), and asCollector() that operate at any position in the argument list instead of only at the end, as well as new lookups: findClass() loads a class under the lookup’s access rules, and default methods in interfaces are now reachable via lookup.

Platform Logging API and Service – JEP 264

The JDK itself logs via java.util.logging. Anyone using Log4j or Logback in their application therefore got the JDK’s log output in a different format and in a different place.

JEP 264 separates interface and implementation: System.Logger is the minimal logging interface the JDK itself uses, and System.LoggerFinder is the service through which an application plugs in its own logging framework as a backend. For you, that means: The JDK’s log messages end up in the same log as your own – with the same format, in the same file, and with the same level configuration.

If no LoggerFinder is found via the ServiceLoader, the messages end up in java.util.logging as before.

You can also use System.Logger directly:

private static final Logger LOGGER = System.getLogger(MyClass.class.getName());

LOGGER.log(Level.INFO, "Application started");
LOGGER.log(Level.WARNING, "Config value {0} is deprecated", "foo");

Log4j 2 (module log4j-jpl) and SLF4J (slf4j-jdk-platform-logging) ship a LoggerFinder implementation – the JDK’s own messages then end up in your application’s log.

Filter Incoming Serialization Data – JEP 290

Java deserialization is a classic attack vector against server applications. Wherever an application accepts serialized objects from outside – in a session cookie, a message from a queue, an RMI call – an ObjectInputStream reads data that someone else wrote.

The problem: The stream contains not only data but also the names of the classes the objects are to be restored from. So the sender decides which classes readObject() loads and instantiates – and it can pick from everything on the classpath.

That turns every class into a weapon that executes code of its own while being deserialized, for instance in readObject(), equals(), or hashCode(). Attackers chain several such classes from widely used libraries into a so-called gadget chain that ends in a Runtime.exec(): The server then runs the command the sender supplied – a remote code execution. This is exactly how the best-known of these chains worked: It builds a chain from Apache Commons Collections classes that ends in Runtime.exec() – and in November 2015, it hit WebLogic, WebSphere, JBoss, Jenkins, and OpenNMS, among others (CVE-2015-7501).

JEP 290 introduces deserialization filters. A filter decides for each class in the stream whether it is allowed and additionally limits array sizes, object depth, the number of references, and the stream length.

The simplest way to define a filter is a pattern string:

ObjectInputFilter filter =
    ObjectInputFilter.Config.createFilter("java.util.*;java.lang.*;!*");

try (ObjectInputStream ois = new ObjectInputStream(inputStream)) {
  ois.setObjectInputFilter(filter);
  Object object = ois.readObject();
}

The pattern allows classes from java.util and java.lang and rejects everything else (!*). If a class that is not allowed is encountered, readObject() aborts with an InvalidClassException (“filter status: REJECTED”).

You set a process-wide filter via the system property jdk.serialFilter – without changing any code, e.g., -Djdk.serialFilter='java.lang.*;maxarray=1000;!*'.

Java 17 extended the mechanism with context-specific filters that are determined dynamically per stream.

Further Extensions to the Class Library

Some extensions are too small for a chapter of their own but too useful to keep quiet about:

  • InputStream.readAllBytes() reads a stream completely into a byte array, readNBytes() reads a specific number of bytes, and transferTo(OutputStream) copies the entire content to an output stream – three methods you needed Apache Commons IO for until now.
  • Objects.requireNonNullElse(obj, default) and requireNonNullElseGet(obj, supplier) return a default value if obj is null. Objects.checkIndex(), checkFromToIndex(), and checkFromIndexSize() check array and list indices and throw an IndexOutOfBoundsException with a readable message (“Index 3 out of bounds for length 3”).
  • Arrays.equals(), compare(), and mismatch() are now also available for subranges of two arrays. mismatch() returns the index of the first difference – or -1 if the arrays are equal.
  • Enumeration.asIterator() turns the ancient Enumeration interface into an Iterator on which forEachRemaining() works.
  • Matcher.results() returns all matches of a regular expression as a Stream<MatchResult>, and Matcher.replaceAll(Function<MatchResult, String>) computes the replacement per match with a function. Scanner.tokens() and findAll() return streams as well.
  • Math.fma() computes a * b + c with a single rounding (fused multiply-add, via CPU instruction where available), Math.multiplyHigh() the upper 64 bits of a 128-bit product of two long values, and Math.multiplyFull() the exact long product of two int values.
  • Integer.parseInt() and parseUnsignedInt() (likewise the Long variants) now parse a subrange of a CharSequence – without a preceding substring(). More on this in the article Converting String to int.
  • Thread.onSpinWait() from JEP 285 signals to the CPU that the thread is waiting in a spin loop – on x86, this becomes the PAUSE instruction, which reduces the loop’s latency and power consumption.
  • java.lang.ref.Cleaner is the replacement for finalize(): You register an object together with a cleanup action that runs as soon as the object is no longer reachable. Consequently, Java 18 marked finalize() as “deprecated for removal”.

Use CLDR Locale Data by Default – JEP 252

What a date, a number, or a currency amount looks like in a given language was, until Java 8, defined by locale data that Sun Microsystems had collected in the 1990s. JEP 252 switches it to the Unicode Consortium’s Common Locale Data Repository (CLDR), which serves the same purpose for operating systems, browsers, and programming languages, and which is maintained continuously.

This change can have unexpected consequences, because the Java 9 date formats differ from the old ones in details. Three examples for the German locale:

Java 8since Java 9
DateFormat.SHORT, date and time21.03.17 10:3021.03.17, 10:30 (with a comma)
Weekday (EEE)DiDi. (with a period)
DateFormat.FULL, date and timeDienstag, 21. März 2017 10:30 Uhr MEZDienstag, 21. März 2017 um 10:30:00 Mitteleuropäische Normalzeit

As long as the output is only read by humans, this hardly stands out. It becomes critical where a program reads its own output back in, or where tests check for exact strings: The comma in the short date is enough to make a parser fail.

For the transition period, you can bring back the old data:

$ java -Djava.locale.providers=COMPAT,CLDR MyApp

I recommend doing that only to buy time, and migrating the affected places during it. That bridge has since been torn down: Up to Java 22, COMPAT worked with a warning; since Java 23, the JVM reports “COMPAT locale provider has been removed” and always formats according to CLDR.

jshell: The Java Shell – JEP 222

Every scripting language has a so-called read-eval-print loop (REPL): a prompt in which you type an expression and immediately see the result. Java did not have one until version 8 – anyone who wanted to try out an API had to write a throwaway class with a main() method and System.out.println().

JEP 222 brings the jshell:

$ jshell

jshell> int x = 10
x ==> 10

jshell> x * 2
$2 ==> 20

jshell> List.of(1, 2, 3).stream().mapToInt(i -> i).sum()
$3 ==> 6

Semicolons are optional, expressions without assignment end up in automatically named variables ($2, $3), and the common packages such as java.util, java.io, and java.util.stream are already imported. The tab key completes names.

With the commands /vars, /methods, and /types, you see what you have defined so far; with /edit, you open an editor in which you can change variables or methods, among other things; and /exit ends the session.

I recommend trying the jshell out once. It is the fastest way to get to know an unfamiliar API, to test a regular expression against a few sample strings, or to look up what a method returns in an edge case – with no project, no class, and no build.

Behind the tool is the jdk.jshell API, with which IDEs and notebooks can evaluate Java snippets as well.

Multi-Release JAR Files – JEP 238

Library authors face a dilemma: Using new JDK APIs means no longer supporting old Java versions – or maintaining two artifacts. Anyone who replaced sun.misc.BASE64Decoder with java.util.Base64, added in Java 8, thereby lost the users of Java 7.

JEP 238 allows shipping several versions of a class in one JAR. The structure:

jar root
  - A.class
  - B.class
  - C.class
  - META-INF
     - MANIFEST.MF  (contains "Multi-Release: true")
     - versions
        - 9
           - A.class
           - B.class

On Java 8, the class loader only sees the classes in the root directory. On Java 9 and newer, it first searches the directory of the running version, then the directories of lower versions, and finally the root directory. There is only one C, but two versions of A and B.

The jar tool builds such archives with the --release option:

$ jar --create --file library.jar -C classes . --release 9 -C classes-9 .

All versions of a class must have the same public API, that is, the same public and protected methods and fields; private methods may differ. The jar tool checks this and otherwise refuses the archive with “contains a class with different api from earlier version”. It is about different implementations, not different interfaces.

Build tools like Maven and Gradle now support the format; many well-known libraries (e.g., Log4j 2 and Byte Buddy) ship multi-release JARs.

Compile for Older Platform Versions – JEP 247

The options -source and -target have existed since the early JDK versions: With javac -source 8 -target 8, for example, you compile bytecode for Java 8 with a new Java compiler – but against the class library of the new JDK. If you accidentally call a method that did not exist in Java 8, you only notice at runtime on Java 8 with a NoSuchMethodError.

For this, JEP 247 introduces the new --release option, which combines both settings and additionally prescribes the class library of the target version:

$ javac --release 8 -d out Rel.java

Rel.java:2: error: cannot find symbol
  symbol:   method of(int,int)
  location: interface List

The call to List.of() now fails at compile time instead of on the target system. This is made possible by the lib/ct.sym file in the JDK: It contains the signatures of the public APIs of all supported older versions.

--release replaces -source and -target – the options cannot be combined; javac aborts with “option --source cannot be used together with --release”. Maven and Gradle offer the new option as maven.compiler.release and options.release, respectively.

I recommend using --release (or the corresponding Maven and Gradle options) everywhere.

Enhanced Deprecation – JEP 277

The @Deprecated annotation has existed since Java 5. Up to Java 8, it only said: “Please do not use this anymore.” The annotation gave no indication of whether an API would ever be removed, nor when. For decades, hardly anything disappeared from the JDK anyway, so many treated the marking as inconsequential.

JEP 277 gives the annotation two elements:

@Deprecated(since = "9", forRemoval = true)
public void oldMethod() { ... }

since names the version since which the API has been deprecated. forRemoval = true announces that it will be removed in a future release – the Java developers call this “terminally deprecated”. The compiler issues a separate warning for such calls, which cannot be suppressed with @SuppressWarnings("deprecation") but only with @SuppressWarnings("removal"):

Dep.java:12: warning: [removal] Integer(int) in Integer has been deprecated and marked for removal
    Integer i = new Integer(42);
                ^

In addition, there is the jdeprscan tool, which scans compiled classes or JARs for the use of deprecated JDK APIs – even without source code:

$ jdeprscan --for-removal library.jar

Since Java 9, the JDK developers mean the announcement seriously: What is marked as forRemoval usually disappears within a few releases. Thread.stop(Throwable), for instance, had been deprecated since Java 1.2, was additionally marked forRemoval in Java 9 – and has been gone since Java 11. The constructors of the wrapper classes (new Integer(42)) are halfway down that road: deprecated in Java 9, marked for removal in Java 16.

A small addition comes from JEP 211: Until Java 8, the compiler warned twice – once at the import line and once at every use. The warning at the import could not be suppressed with @SuppressWarnings, because an import is not a statement an annotation could be attached to. Since Java 9, it is gone; the warning at the actual use remains. An import without a use therefore produces no warning at all; your IDE points out a superfluous import instead.

New Version-String Scheme – JEP 223

Which version has more security fixes: JDK 7 Update 55 or Update 60? The answer is: both the same. Update 60 was released later but was a feature update; its security baseline was 1.7.0_55, so it brought not a single security fix beyond Update 55. The gap of five numbers suggested the opposite, and the version number did not reveal it.

JEP 223 replaces the 1.8.0_60 scheme with $MAJOR.$MINOR.$SECURITY, modeled on Semantic Versioning. Java 9 is called 9, the first security update 9.0.1, the first minor update 9.1.2 – the security number is not reset on a minor update, so a higher third number always means more security fixes. The build number follows after a plus sign: 9.0.1+11.

The new class Runtime.Version parses and compares such strings:

Runtime.Version version = Runtime.version();
System.out.println(
    version.major() + "." + version.minor() + "." + version.security());

So since Java 9, the system property java.version returns 9.0.1 instead of 1.9.0_01. Anyone who takes it apart themselves gets wrong results – read the version via Runtime.version() instead, and your code will survive the next change of scheme as well.

Javadoc: HTML5, Search, and New Doclet API – JEP 224, 225, and 221

Three JEPs modernize the javadoc tool:

  • JEP 224 adds the -html5 option, with which Javadoc generates semantic HTML5 with WAI-ARIA roles for accessibility. In Java 9, HTML 4.01 is still the default; since Java 10, it is HTML5. Anyone who needed the old markup could request it with -html4 up to Java 12. Since Java 13, there is only HTML5, and -html5 is merely tolerated (“This option is no longer required”).
  • JEP 225 adds a search box to every generated API documentation. The search runs purely client-side in JavaScript, understands camel-case abbreviations (addFL finds addFocusListener), and searches modules, packages, types, members, and terms marked with {@index ...}. The search in the official JDK documentation is exactly this feature.
  • JEP 221 replaces the old Doclet API (com.sun.javadoc) with jdk.javadoc.doclet, which builds on the Language Model API (javax.lang.model) and the Compiler Tree API. This is only relevant for you if you write your own doclets.

Unified JVM Logging and Unified GC Logging – JEP 158 and 271

Until Java 8, every JVM subsystem – garbage collector, class loader, JIT compiler, threads – had its own logging options: -XX:+PrintGC, -XX:+PrintGCDetails, -XX:+TraceClassLoading, and dozens more, each with its own output format.

JEP 158 replaces them with one logging system with one option: -Xlog. Every message carries one or more tags (gc, class, compiler, safepoint, …) and a level (error, warning, info, debug, trace). The simplest call names only a tag and leaves the rest to the defaults – level info, output to stdout:

$ java -Xlog:gc MyApp

[0.003s][info][gc] Using G1
[0.153s][info][gc] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 27M->3M(32M) 1.524ms

The square brackets at the beginning are the decorations: here uptime, level, and tags.

The full form of the option is -Xlog:<tags>=<level>:<output>:<decorations>, and every part after the tags may be omitted. This lets you select precisely:

  • -Xlog:gc* – all messages that carry the gc tag in combination with other tags (the counterpart to -XX:+PrintGCDetails).
  • -Xlog:gc*:file=gc.log:time,uptime – the same into a file, with time and uptime as decorations. Log rotation is built in.
  • -Xlog:class+load=info – every loaded class.
  • -Xlog:help – the complete syntax and the list of all tags.

JEP 271 migrated GC logging to this system. Anyone who used to generate their GC logs with -XX:+PrintGCDetails -Xloggc:gc.log has to use -Xlog:gc*:file=gc.log from Java 9 on – and gets a format that has remained stable ever since.

The configuration can be changed at runtime via jcmd <pid> VM.log.

Performance

Three JEPs change the runtime behavior of every Java application. Two of them take effect the moment you switch to Java 9; the third, as soon as you recompile your code.

Compact Strings – JEP 254

Until Java 8, a String stored its characters in a char[] – two bytes per character, even if the string consisted only of ASCII characters. And that is what most strings in a typical application do: class names, JSON keys, URLs, log messages.

JEP 254 replaces the char[] with a byte[] plus a coder field: Strings that contain only Latin-1 characters occupy one byte per character; all others take two bytes in UTF-16. Nothing changes on the outside – charAt(), length(), and all other methods behave as before. On the inside, the memory footprint of most strings is halved, and with it the number of garbage collector runs.

The additional coder field costs nothing: The JVM aligns every object to a multiple of eight bytes, and up to Java 8, a String object had four bytes of waste at the end. That is exactly where the one byte for the coder sits since Java 9. A String object itself is 24 bytes in both versions – the saving happens in the byte[], not in the String.

You can read what this means for individual methods in my article on substring(). With -XX:-CompactStrings, the optimization can be switched off – I know of no reason to do that.

Indify String Concatenation – JEP 280

For "Hello " + name + "!", javac used to generate a chain of StringBuilder.append() calls. If the JDK team wanted to make concatenation faster, it had to change the compiler – and every application would have had to be recompiled to benefit.

JEP 280 makes javac generate a single invokedynamic instruction instead. Which code actually joins the strings is decided at runtime by the StringConcatFactory class. The strategy can improve from one JDK version to the next without the bytecode having to change.

(The “indify” in the JEP title is a coinage of the JDK developers and means something like “switch it to invokedynamic”.)

One effect of this change: Since Java 9, "" + i is just as fast as Integer.toString(i). I show the bytecode before and after the change in the article Converting int to String.

Make G1 the Default Garbage Collector – JEP 248

Until Java 8, the Parallel GC was the default garbage collector – optimized for throughput, with correspondingly long stop-the-world pauses. JEP 248 makes G1 (“Garbage First”) the default on server configurations. G1 limits pause times by dividing the heap into regions and cleaning up only as many of them per cycle as fit into the pause target. That target is 200 milliseconds by default and can be shifted with -XX:MaxGCPauseMillis.

The reasoning in the JEP: For most applications, short pauses are more important than maximum throughput. Anyone who needs the throughput still selects the Parallel GC explicitly with -XX:+UseParallelGC.

The restriction “on server configurations” mentioned above had a long echo: On machines with only one CPU or less than 1,792 MB of RAM, the Serial GC remained the default. That mainly affected small virtual machines and containers – precisely the environments in which Java applications predominantly ran in the years that followed. Only Java 27 made G1 the default in all environments.

Incubator and Experimental Features

Java 9 is the first release with an incubator module – the mechanism defined in JEP 11 for shipping an API before it becomes part of Java SE. Incubator modules are called jdk.incubator.*, are not resolved automatically, and may change in every release.

Preview features, as you know them from more recent Java versions, did not exist in 2017: The mechanism for them arrived with JEP 12 only in Java 12. The difference from an incubator lies in the degree of maturity. A preview feature is fully specified and fully implemented and only waits for the verdict of practice; an incubating API is explicitly not finished yet and therefore lives in a module of its own outside Java SE.

Both are to be distinguished from experimental features: those are properties of the JVM that sit behind -XX options and appear in no specification – like the two compilers in the following sections.

HTTP/2 Client (Incubator) – JEP 110

HttpURLConnection dates from Java 1.1, knows only blocking calls, and is – as the JEP puts it – “hard to use, with many undocumented behaviors”. JEP 110 delivers a new HTTP client with a builder API, synchronous and asynchronous modes (based on CompletableFuture), HTTP/2, and WebSocket.

In Java 9, it lives in the jdk.incubator.httpclient module and the jdk.incubator.http package. To use it, you have to activate the module when compiling and launching with --add-modules jdk.incubator.httpclient:

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://www.happycoders.eu/"))
    .GET()
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandler.asString());

System.out.println(response.statusCode());

In Java 11, the client was standardized under the java.net.http package – with minor changes to the API (BodyHandler.asString() became BodyHandlers.ofString(), for example). I show it in detail there.

Ahead-of-Time Compilation (Experimental) – JEP 295

The JIT compiler needs time to warm up, and rarely called methods are never compiled. JEP 295 introduces jaotc, a tool that translates Java classes into native code before startup:

$ jaotc --output libHelloWorld.so HelloWorld.class
$ java -XX:AOTLibrary=./libHelloWorld.so HelloWorld

The code generator is the Graal compiler, written in Java; the feature was limited to Linux/x64 and experimental. It did not catch on: Java 17 removed jaotc again. The topic of startup time has since been tackled by Project Leyden with a different approach.

Java-Level JVM Compiler Interface (Experimental) – JEP 243

JEP 243 defines an interface (JVMCI) through which a compiler written in Java can be used by the JVM as a JIT compiler. The only notable user was the Graal compiler, which you could activate in Java 10 with -XX:+UseJVMCICompiler.

This chapter, too, has since been closed: Graal is being developed further as GraalVM outside the JDK, and Java 27 removed the JVMCI from the JDK.

Deprecations and Deletions

Deprecate the Applet API – JEP 289

In 2017, most browser vendors had already abolished the NPAPI plug-in interface or announced its end – and with it the basis on which the Java plug-in ran applets in the browser. JEP 289 marks java.applet.Applet, javax.swing.JApplet, and the associated classes as @Deprecated(since = "9") – initially without forRemoval. That followed in Java 17, and Java 26 removed the API.

Deprecate the Concurrent Mark Sweep (CMS) Garbage Collector – JEP 291

With G1 as the new default, the Concurrent Mark Sweep collector had lost its purpose: Both are optimized for short pauses, but CMS is expensive to maintain and slows down the development of the GC code base. JEP 291 marks it as deprecated – the -XX:+UseConcMarkSweepGC option has issued a warning ever since. CMS was removed in Java 14.

Remove GC Combinations Deprecated in JDK 8 – JEP 214

Java 8 had marked some rarely used combinations of young- and old-generation collectors as deprecated; JEP 214 removes them: DefNew + CMS, ParNew + SerialOld, and the incremental CMS mode (-Xincgc, -XX:+CMSIncrementalMode). Since Java 9, a JVM started with one of these options aborts with an error message.

Remove the JVM TI hprof Agent and Remove the jhat Tool – JEP 240 and 241

The hprof agent (-agentlib:hprof) was demo code for the JVM Tool Interface that was never intended as a production tool. Its functions have better successors: You create heap dumps with jcmd <pid> GC.heap_dump or jmap -dump; for profiling, there are VisualVM and – free since Java 11 – the Java Flight Recorder. JEP 240 removes the agent, JEP 241 the associated analysis tool jhat.

Remove Launch-Time JRE Version Selection – JEP 231

Since JDK 5, an application could demand to be started with a specific JRE version via the JRE-Version manifest entry or the java -version:<version> option – a never fully documented feature that had long had better alternatives in Java Web Start and native installers. JEP 231 removes it: The option leads to an error, the manifest entry to a warning.

Remove Demos and Samples – JEP 298

The JDK’s demo and sample directories contained code that had not been maintained for years. JEP 298 removes them.

Other Changes in Java 9

You will hardly notice the following 49 JEPs in everyday work. To keep the list complete, I list them here in thematic groups with one sentence each.

Security

  • JEP 219 implements DTLS 1.0 and 1.2 – TLS for UDP-based protocols – in javax.net.ssl.
  • JEP 244 adds the TLS extension ALPN (Application-Layer Protocol Negotiation), via which client and server negotiate the application protocol – the prerequisite for HTTP/2 over TLS.
  • JEP 249 implements OCSP stapling: The server delivers the proof of validity of its certificate right away, so the client does not have to ask the certificate authority itself.
  • JEP 229 makes PKCS12 the default format for new keystores instead of the JDK-specific JKS format; existing JKS keystores are still recognized.
  • JEP 273 adds the three DRBG mechanisms from NIST SP 800-90Ar1 to SecureRandom, configurable via the new DrbgParameters class – more on this in the article on random numbers in Java.
  • JEP 287 implements the hash functions SHA3-224, SHA3-256, SHA3-384, and SHA3-512.
  • JEP 288 extends the jdk.certpath.disabledAlgorithms security property with conditions (jdkCA, denyAfter, usage) that allow SHA-1-signed certificates to be blocked selectively.
  • JEP 246 accelerates GHASH (part of AES-GCM) and RSA via CPU instructions of Intel x64 and SPARC.
  • JEP 232 reduces the performance loss of 10 to 15% that applications with an active security manager had to accept until now – mainly through ConcurrentHashMap instead of synchronized maps in the permission classes.

JVM Internals

  • JEP 143 speeds up contended monitors: monitorenter, monitorexit, notify(), and notifyAll(), as well as waking up parked threads.
  • JEP 197 divides the code cache into three segments – JVM-internal code, profiled code with a short lifetime, and fully optimized code – so the JIT compiler no longer has to search for space in a single heap.
  • JEP 250 stores interned strings in the class-data-sharing archive so that several JVM processes can share them.
  • JEP 270 reserves an area at the end of each thread stack that critical sections (e.g., in ReentrantLock) may still use in the event of a stack overflow, so they do not leave behind inconsistent data structures.
  • JEP 228 adds further diagnostic commands to jcmd, for instance for listing loaded classes and the compiler queues.
  • JEP 165 allows controlling the behavior of the JIT compilers per method via a JSON-like directives file (-XX:CompilerDirectivesFile) – also at runtime via jcmd.

Desktop, Graphics, and JavaFX

  • JEP 251 introduces the MultiResolutionImage interface, with which an image is available in several resolutions and the appropriate one is chosen for the screen.
  • JEP 263 brings HiDPI support for Windows and Linux, which already existed on macOS.
  • JEP 262 adds a plugin for the TIFF format to Image I/O.
  • JEP 272 defines a public API for platform-specific desktop features – taskbar and dock, login and logout events – as a replacement for the macOS-internal packages com.apple.eawt and com.apple.eio.
  • JEP 265 makes the Marlin renderer the default rasterizer of Java 2D.
  • JEP 258 replaces the ICU font layout engine with HarfBuzz.
  • JEP 283 allows AWT, Swing, and JavaFX to use GTK 3 on Linux.
  • JEP 253 makes previously internal APIs of the JavaFX UI controls and the CSS system public so that they survive modularization.
  • JEP 257 updates the GStreamer included in JavaFX/Media.
  • JEP 256 replaces the @beaninfo Javadoc tags with annotations from which BeanInfo classes are generated at runtime.

Internationalization and XML

  • JEP 226 reads .properties files for ResourceBundle as UTF-8 by default – escaping umlauts as \u00e4 is history. If a file is not valid UTF-8, it is read as ISO-8859-1 as before.
  • JEP 227 and JEP 267 update Character, String, and the other Unicode-dependent classes to Unicode 7.0 and 8.0.
  • JEP 268 brings a public API for XML catalogs according to the OASIS standard 1.1, with which external references in XML, XSD, and XSL are redirected to local copies.
  • JEP 255 merges selected changes from Xerces 2.11.0 into the JDK’s XML parser.

Nashorn

Three JEPs extend the JavaScript engine Nashorn: JEP 236 defines a public parser API with which IDEs can analyze the syntax tree of ECMAScript code; JEP 292 implements a subset of ECMAScript 6 (arrow functions, classes, template strings, let and const); and JEP 276 makes Nashorn’s linker mechanism for invokedynamic available as a standalone module, jdk.dynalink.

Nashorn was deprecated in Java 11 and removed in Java 15.

Ports

JEP 237 ports the JDK to Linux/AArch64 (64-bit ARM), JEP 294 to Linux/s390x (IBM mainframes), and JEP 297 integrates the unified port for arm32 and arm64 contributed by Oracle.

Development of the JDK Itself

13 JEPs concern exclusively the build and testing of the JDK: the compiler server sjavac (JEP 199), resolving lint and doclint warnings in the JDK source code (JEP 212), three changes to javac itself – faster type checking of arguments (JEP 215), correct processing of imports regardless of their order (JEP 216), and a new annotations pipeline (JEP 217) – the validation of all JVM command-line arguments (JEP 245), automatically generated compiler tests (JEP 233), tests for the class-file attributes generated by javac (JEP 235) and for humongous objects in G1 (JEP 278), automatic diagnostic data on test failures (JEP 279), a C++ unit-test framework based on Google Test for HotSpot (JEP 281), a new build system for HotSpot (JEP 284), and the reorganization of the documentation (JEP 299).

Complete List of All Changes in Java 9

This article has presented all 91 JEPs of Java 9, plus the most important extensions to the class library that are not assigned to any JEP. You can find the list of JEPs on the JDK 9 project page and a complete list of all changes in the official Java 9 release notes.

Conclusion

Java 9 was a release that many teams skipped – and that nevertheless laid the foundation for everything that followed. The module system split the JDK from the monolithic rt.jar into dozens of modules from which jlink builds tailor-made run-time images; the strict encapsulation of JDK internals has forced libraries and frameworks into clean dependencies in the years since – and thereby given the JDK developers back the freedom to change internals without breaking half the Java ecosystem. Without that freedom, Java would hardly have evolved as fast after 2017 as it did.

For everyday work, it is the small things that have stayed: List.of(), private methods in interfaces, takeWhile(), Optional.or(), ProcessHandle, jshell. Two improvements were even handed to every application for free when switching from Java 8 to Java 9, without recompiling a single line: Compact strings halved the memory footprint of most strings, and with G1 as the default, long stop-the-world pauses became short ones. The invokedynamic-based string concatenation, in contrast, only arrives after a recompile – it sits in the bytecode that javac generates from version 9 on.

If you are still maintaining a Java 8 application today, Java 9 is not the destination but the first hurdle: The encapsulation of internals, the Java EE modules removed in Java 11, and the changed locale data are the points where a migration typically gets stuck. I recommend checking the dependencies with jdeps --jdk-internals first and then going directly to the current LTS release – the hurdles between Java 9 and today are much lower than those between 8 and 9.

Did you take something away from this article? With a review on my ProvenExpert profile, you help other developers assess whether these articles are worth reading – and you help me understand which content is most useful to you.

👉 Leave a review

The HappyCoders newsletter keeps you up to date on new Java features and articles – click here to sign up.

👉 Newsletter Sign-up

Want Even More Knowledge?

My blog features many articles on Java, software architecture, and performance — from foundational concepts to advanced patterns.

If you want to go deeper, check out my trainings: hands-on, easy to understand, and directly applicable to your day-to-day project work. Instead of theory, I teach principles that help you write code that is better, more maintainable, and more performant in the long run.

Explore the Java Trainings

Become a Better Java Developer

My free newsletter keeps you ahead. Modern Java: new versions & features, performance, and JVM insights – once a month.

Search