

Java 19 has been released on September 20, 2022. You can download Java 19 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) – JEP 427
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) – JEP 405
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) – JEP 425
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.
Structured Concurrency (Incubator) – JEP 428
Also developed in Project Loom and initially released as an incubator feature in Java 19 with JDK Enhancement Proposal 428 is the so-called "Structured Concurrency."
When a task consists of several subtasks that can be processed in parallel, Structured Concurrency allows us to implement this in a particularly readable and maintainable way.
You can learn more about how this works in the main article about Structured Concurrency.
Foreign Function & Memory API (Preview) – JEP 424
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 FFMTest.java
$ 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.
Vector API (Fourth Incubator) – JEP 426
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 returnsfalse
.ThreadGroup.setDaemon()
– sets thedaemon
flag, but this has no effect anymore.ThreadGroup.getDaemon()
– returns the value of the unuseddaemon
flags.ThreadGroup.suspend()
,resume()
, andstop()
throw anUnsupportedOperationException
.
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.
Summary
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 Java 19 here.
You don't want to miss any HappyCoders.eu article and always be informed about new Java features? Then click here to sign up for the free HappyCoders newsletter.