Java 19 Features with Examples

Java 19 Features (with Examples)

Author image
by Sven WoltmannJune 14, 2022

Java 19 has been in the so-called "Rampdown Phase One" since June 9, 2022, which means that no further JDK Enhancement Proposals (JEPs) will be included in the release. The feature set is therefore fixed. Only bugs will be corrected, and, if necessary, minor improvements will be made.

The target release date is September 20, 2022. You can download the current early-access version here.

The most exciting new feature for me is virtual threads, which have been under development for several years within Project Loom and are now finally included as a preview in the JDK.

Virtual threads are a prerequisite for Structured Concurrency, another exciting new incubator feature in Java 19.

For those who need to access non-Java code (e.g., the C standard library), there is also good news: The Foreign Function & Memory API has reached the preview stage after five incubator rounds.

New System Properties for System.out and System.err

One change that you won't find in the Java 19 feature announcements but only buried deep in the release notes is something every Java developer should know about.

If you run an existing application with Java 19, you may see question marks on the console instead of special characters.

This is because, as of Java 19, the operating system's default encoding is used for printing to System.out and System.err – for example, "Cp1252" on Windows. To change the output to UTF-8, you have to add the following VM options when calling the application:

-Dstdout.encoding=utf8 -Dstderr.encoding=utf8

If you don't want to do this every time you start the program, you can also set these settings globally by defining the following environment variable (yes, it begins with an underscore):

_JAVA_OPTIONS="-Dstdout.encoding=utf8 -Dstderr.encoding=utf8"

New Methods to Create Preallocated HashMaps

If we want to create an ArrayList for a known number of elements (e.g., 120), we can do it as follows since ever:

List<String> list = new ArrayList<>(120);
Code language: Java (java)

Thus the array underlying the ArrayList is allocated directly for 120 elements and does not have to be enlarged several times (i.e., newly created and copied) to insert the 120 elements.

Similarly, we have always been able to generate a HashMap as follows:

Map<String, Integer> map = new HashMap<>(120);
Code language: Java (java)

Intuitively, one would think that this HashMap offers space for 120 mappings.

However, this is not the case!

This is because the HashMap is initialized with a default load factor of 0.75. This means that as soon as the HashMap is 75% full, it is rebuilt ("rehashed") with double the size. This ensures that the elements are distributed as evenly as possible across the HashMap's buckets and that as few buckets as possible contain more than one element.

Thus, the HashMap initialized with a capacity of 120 can only hold 120 × 0.75 = 90 mappings.

To create a HashMap for 120 mappings, you had to calculate the capacity by dividing the number of mappings by the load factor: 120 ÷ 0.75 = 160.

So a HashMap for 120 mappings had to be created as follows:

// for 120 mappings: 120 / 0.75 = 160 Map<String, Integer> map = new HashMap<>(160);
Code language: Java (java)

Java 19 makes it easier for us – we can now write the following instead:

Map<String, Integer> map = HashMap.newHashMap(120);
Code language: Java (java)

If we look at the source code of the new methods, we see that they do the same as we did before:

public static <K, V> HashMap<K, V> newHashMap(int numMappings) { return new HashMap<>(calculateHashMapCapacity(numMappings)); } static final float DEFAULT_LOAD_FACTOR = 0.75f; static int calculateHashMapCapacity(int numMappings) { return (int) Math.ceil(numMappings / (double) DEFAULT_LOAD_FACTOR); }
Code language: Java (java)

The newHashMap() method has also been added to LinkedHashMap and WeakHashMap.

There is no JDK enhancement proposal for this extension.

Preview- und Incubator-Features

Java 19 provides us with six preview and incubator features, i.e., features that have not yet been completed but can already be tested by the developer community. The feedback from the community is usually incorporated into the further development and completion of these features.

Pattern Matching for switch (Third Preview)

Let's start with a feature that has already gone through two rounds of previews. First introduced in Java 17, "Pattern Matching for switch" allowed us to write code like the following:

switch (obj) { case String s && s.length() > 5 -> System.out.println(s.toUpperCase()); case String s -> System.out.println(s.toLowerCase()); case Integer i -> System.out.println(i * i); default -> {} }
Code language: Java (java)

We can check within a switch statement if an object is of a particular class and if it has additional characteristics (like in the example: longer than five characters).

In Java 19, JDK Enhancement Proposal 427 changed the syntax of the so-called "Guarded Pattern" (in the example above "String s && s.length() > 5"). Instead of &&, we now have to use the new keyword when.

The example from above is notated in Java 19 as follows:

switch (obj) { case String s when s.length() > 5 -> System.out.println(s.toUpperCase()); case String s -> System.out.println(s.toLowerCase()); case Integer i -> System.out.println(i * i); default -> {} }
Code language: Java (java)

when is a so-called "contextual keyword" and therefore only has a meaning within a case label. If you have variables or methods with the name "when" in your code, you don't need to change them.

Record Patterns (Preview)

We stay with the topic "pattern matching" and come to "record patterns". If the subject "records" is new to you, I recommend reading the article "Records in Java" first.

I'll best explain what a record pattern is with an example. Let's assume we have defined the following record:

public record Position(int x, int y) {}
Code language: Java (java)

We also have a print() method that can print any object, including positions:

private void print(Object object) { if (object instanceof Position position) { System.out.println("object is a position, x = " + position.x() + ", y = " + position.y()); } // else ... }
Code language: Java (java)

If you stumble over the notation used – it was introduced in Java 16 as "Pattern Matching for instanceof".

Record Pattern with instanceof

As of Java 19, JDK Enhancement Proposal 405 allows us to use a so-called "record pattern". This allows us to write the code as follows:

private void print(Object object) { if (object instanceof Position(int x, int y)) { System.out.println("object is a position, x = " + x + ", y = " + y); } // else ... }
Code language: Java (java)

Instead of matching on "Position position" and accessing position in the following code, we now match on "Position(int x, int y)" and can then access x and y directly.

Record Pattern with switch

Since Java 17, we can also write the original example as a switch statement:

private void print(Object object) { switch (object) { case Position position -> System.out.println("object is a position, x = " + position.x() + ", y = " + position.y()); // other cases ... } }
Code language: Java (java)

We can now also use a record pattern in the switch statement:

private void print(Object object) { switch (object) { case Position(int x, int y) -> System.out.println("object is a position, x = " + x + ", y = " + y); // other cases ... } }
Code language: Java (java)

Nested Record Patterns

It is also possible to match nested records – let me demonstrate this with another example.

We first define a second record, Path, with a start position and a destination position:

public record Path(Position from, Position to) {}
Code language: Java (java)

Our print() method can now use a record pattern to print all the path's X and Y coordinates easily:

private void print(Object object) { if (object instanceof Path(Position(int x1, int y1), Position(int x2, int y2))) { System.out.println("object is a path, x1 = " + x1 + ", y1 = " + y1 + ", x2 = " + x2 + ", y2 = " + y2); } // else ... }
Code language: Java (java)

We can also write this alternatively as a switch statement:

private void print(Object object) { switch (object) { case Path(Position(int x1, int y1), Position(int x2, int y2)) -> System.out.println("object is a path, x1 = " + x1 + ", y1 = " + y1 + ", x2 = " + x2 + ", y2 = " + y2); // other cases ... } }
Code language: Java (java)

Record patterns thus provide us with an elegant way to access the record's elements after a type check.

Virtual Threads (Preview)

The most exciting innovation in Java 19 for me is "Virtual Threads". Virtual threads have been developed in Project Loom for several years and could only be tested with a self-compiled JDK so far.

With JDK Enhancement Proposal 425, virtual threads finally make their way into the official JDK – and they do so directly in the preview stage, so no more significant changes to the API are expected.

To find out why we need virtual threads, what they are, how they work, and how to use them, check out the main article on virtual threads. You definitely shouldn't miss it.

Foreign Function & Memory API (Preview)

In Project Panama, a replacement for the cumbersome, error-prone, and slow Java Native Interface (JNI) has been in the works for a long time.

The "Foreign Memory Access API" and the "Foreign Linker API" were already introduced in Java 14 and Java 16 – both initially individually in the incubator stage. In Java 17, these APIs were combined to form the "Foreign Function & Memory API" (FFM API), which remained in the incubator stage until Java 18.

In Java 19, JDK Enhancement Proposal 424 finally promoted the new API to the preview stage, which means that only minor changes and bug fixes will be made. So it's time to introduce the new API!

The Foreign Function & Memory API enables access to native memory (i.e., memory outside the Java heap) and access to native code (e.g., C libraries) directly from Java.

I will show how this works with an example. However, I won't go too deep into the topic here since most Java developers rarely (or never) need to access native memory and code.

Here is a simple example that stores a string in off-heap memory and calls the "strlen" function of the C standard library on it:

public class FFMTest { public static void main(String[] args) throws Throwable { // 1. Get a lookup object for commonly used libraries SymbolLookup stdlib = Linker.nativeLinker().defaultLookup(); // 2. Get a handle to the "strlen" function in the C standard library MethodHandle strlen = Linker.nativeLinker().downcallHandle( stdlib.lookup("strlen").orElseThrow(), FunctionDescriptor.of(JAVA_LONG, ADDRESS)); // 3. Convert Java String to C string and store it in off-heap memory MemorySegment str = implicitAllocator().allocateUtf8String("Happy Coding!"); // 4. Invoke the foreign function long len = (long) strlen.invoke(str); System.out.println("len = " + len); } }
Code language: Java (java)

Interesting is the FunctionDescriptor in line 9: it expects as the first parameter the return type of the function and as additional parameters the function's arguments. The FunctionDescriptor ensures that all Java types are adequately converted to C types and vice versa.

Since the FFM API is still in the preview stage, we must specify a few additional parameters to compile and start it:

$ javac --enable-preview -source 19 $ java --enable-preview FFMTest
Code language: plaintext (plaintext)

Anyone who has worked with JNI – and remembers how much Java and C boilerplate code you had to write and keep in sync – will realize that the effort required to call the native function has been reduced by orders of magnitude.

If you want to delve deeper into the matter: you can find more complex examples in the JEP.

Structured Concurrency (Incubator)

If a task consists of different subtasks that can be done in parallel (e.g., accessing data from a database, calling a remote API, and loading a file), we could so far use the Java executable framework for this.

This could then look like this, for example:

private final ExecutorService executor = Executors.newCachedThreadPool(); public Invoice createInvoice(int orderId, int customerId, String language) throws ExecutionException, InterruptedException { Future<Order> orderFuture = executor.submit(() -> loadOrderFromOrderService(orderId)); Future<Customer> customerFuture = executor.submit(() -> loadCustomerFromDatabase(customerId)); Future<String> invoiceTemplateFuture = executor.submit(() -> loadInvoiceTemplateFromFile(language)); Order order = orderFuture.get(); Customer customer = customerFuture.get(); String invoiceTemplate = invoiceTemplateFuture.get(); return Invoice.generate(order, customer, invoiceTemplate); }
Code language: Java (java)

We pass the three subtasks to the executor and wait for the partial results. The happy path is quickly implemented. But how do we handle exceptions?

  • If an error occurs in one subtask – how can we cancel the others?
  • How can we cancel the subtasks if the entire invoice is no longer needed?

Both are possible but require pretty complex and difficult-to-maintain code.

And what if we want to debug code of this kind? A thread dump, for example, would give us a bunch of threads named "pool-X-thread-Y" – but we wouldn't know which pool thread belongs to which calling threads since all calling threads share the executor's thread pool.

JDK Enhancement Proposal 428 introduces an API for so-called "structured concurrency", a concept intended to improve the implementation, readability, and maintainability of code for requirements of this type.

Using a StructuredTaskScope, we can rewrite the example as follows:

Invoice createInvoice(int orderId, int customerId, String language) throws ExecutionException, InterruptedException { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Future<Order> orderFuture = scope.fork(() -> loadOrderFromOrderService(orderId)); Future<Customer> customerFuture = scope.fork(() -> loadCustomerFromDatabase(customerId)); Future<String> invoiceTemplateFuture = scope.fork(() -> loadInvoiceTemplateFromFile(language)); scope.join(); scope.throwIfFailed(); Order order = orderFuture.resultNow(); Customer customer = customerFuture.resultNow(); String invoiceTemplate = invoiceTemplateFuture.resultNow(); return new Invoice(order, customer, invoiceTemplate); } }
Code language: Java (java)

So we replace the ExecutorService in the scope of the class with a StructuredTaskScope located in the method's scope – and executor.submit() with scope.fork().

Using scope.join(), we wait for all tasks to be completed – or at least one to fail or be canceled. In the latter two cases, the subsequent throwIfFailed() throws an ExecutionException or a CancellationException.

The new approach brings the following improvements over the old one:

  • Task and subtasks form a self-contained unit in the code – there is no ExecutorService in a higher scope. The threads do not come from a thread pool; instead, each subtask is executed in a new virtual thread.
  • As soon as an error occurs in one of the subtasks, all other subtasks get canceled.
  • When the calling thread is canceled, the subtasks are also canceled.
  • The call hierarchy between the calling thread and the subtask-executing threads is visible in the thread dump.

If you want to try the example yourself: you must explicitly enable preview features and add the incubator module to the module path. For example, if you have saved the code in a file named, you can compile and run it as follows:

$ javac --enable-preview -source 19 --add-modules jdk.incubator.concurrent $ java --enable-preview --add-modules jdk.incubator.concurrent StructuredConcurrencyTest
Code language: plaintext (plaintext)

Please note that Incubator features may still be subject to fundamental changes.

Vector API (Fourth Incubator)

The new Vector API has nothing to do with the java.util.Vector class. In fact, it is about a new API for mathematical vector computation and its mapping to modern SIMD (Single-Instruction-Multiple-Data) CPUs.

The Vector API has been part of the JDK since Java 16 as an incubator and was further developed in Java 17 and Java 18.

With JDK Enhancement Proposal 426, Java 19 delivers the fourth iteration in which the API has been extended to include new vector operations – as well as the ability to store vectors in and read them from memory segments (a feature of the Foreign Function & Memory API).

Incubator features may still be subject to significant changes, so that I won't present the API in detail here. I will do that as soon as the Vector API has moved to the preview stage.

Deprecations and Deletions

In Java 19, some functions have been marked as "deprecated" or made inoperable.

Deprecation of Locale class constructors

In Java 19, the public constructors of the Locale class were marked as "deprecated".

Instead, we should use the new static factory method Locale.of(). This ensures that there is only one instance per Locale configuration.

The following example shows the use of the factory method compared to the constructor:

Locale german1 = new Locale("de"); // deprecated Locale germany1 = new Locale("de", "DE"); // deprecated Locale german2 = Locale.of("de"); Locale germany2 = Locale.of("de", "DE"); System.out.println("german1 == Locale.GERMAN = " + (german1 == Locale.GERMAN)); System.out.println("germany1 == Locale.GERMANY = " + (germany1 == Locale.GERMANY)); System.out.println("german2 == Locale.GERMAN = " + (german2 == Locale.GERMAN)); System.out.println("germany2 == Locale.GERMANY = " + (germany2 == Locale.GERMANY));
Code language: Java (java)

When you run this code, you will see that the objects supplied via the factory method are identical to the Locale constants – those created via constructs logically are not.

java.lang.ThreadGroup is degraded

In Java 14 and Java 16, several Thread and ThreadGroup methods were marked as "deprecated for removal". The reasons are explained in the linked sections.

The following of these methods have been decommissioned in Java 19:

  • ThreadGroup.destroy() – invocations of this method will be ignored.
  • ThreadGroup.isDestroyed() – always returns false.
  • ThreadGroup.setDaemon() – sets the daemon flag, but this has no effect anymore.
  • ThreadGroup.getDaemon() – returns the value of the unused daemon flags.
  • ThreadGroup.suspend(), resume(), and stop() throw an UnsupportedOperationException.

Other Changes in Java 19

In this section, you will find changes/enhancements that might not be relevant for all Java developers.

Linux/RISC-V Port

Due to the increasing use of RISC-V hardware, a port for the corresponding architecture was made available with JEP 422.

Complete List of All Changes in Java 19

In addition to the JDK Enhancement Proposals (JEPs) and class library changes presented in this article, there are numerous smaller changes that are beyond the scope of this article. You can find a complete list in the JDK 19 Release Notes.


In Java 19, the long-awaited virtual threads developed in Project Loom have finally found their way into the JDK (albeit in preview stage for now). I hope you are as excited as I am and can't wait to use virtual threads in your projects!

Structured Concurrency (still in the incubator stage) will build on this to greatly simplify the management of tasks that are split into parallel subtasks.

The pattern matching capabilities in instanceof and switch, which have been gradually enhanced in recent JDK versions, have been extended to include record patterns.

The preview and incubator features "Pattern Matching for switch", "Foreign Function & Memory API", and "Vector API" were sent to the next preview and incubator rounds.

Various other changes round off the release as usual. You can download the current Java 19 Early-Access Release here.

You don't want to miss any article? Then click here to sign up for the free HappyCoders newsletter.