The next version of the Java platform continues the buildout of the Loom, Amber, and Panama projects.
Java 20, the next planned version of standard Java, will become generally available on March 21, 2023. It’s been in Rampdown Phase Two since January 19, 2023, and it’s now feature-complete, which means no more JEPs will target this release.
The six JEPs officially marked for release are either in incubation or in a preview stage. These include JEPs for scoped values, record patterns, pattern matching for switch statements and expressions, a foreign function and memory API, virtual threads, and structured concurrency.
The most innovative and remarkable addition is called scoped values, and it’s intended to replace thread-local variables, which have various disadvantages.
For this preview article, I am using Java 20 Early Access Build 29’s jshell tool to demonstrate the code. If you want to test the features, download JDK 20, fire up the terminal, check the version, and run jshell. (Note that you might see a newer version of the JDK.)
[mtaman]:~ java -version openjdk version "20-ea" 2023-03-21 OpenJDK Runtime Environment (build 20-ea+29-2280) OpenJDK 64-Bit Server VM (build 20-ea+29-2280, mixed mode, sharing) [mtaman]:~ jshell --enable-preview | Welcome to JShell -- Version 20-ea | For an introduction type: /help intro jshell>
Features targeted for Java 20
Table 1 shows the six JEPs targeted for Java 20, grouped based on their projects. As mentioned, scoped values, provided by JEP 429, is a new feature entering the Java ecosystem. The other five JEPs provide updates to already-known incubator and preview features.
Table 1. Six JEPs targeted for Java 20, grouped by project
| JEP | Project Loom |
| 429 | Scoped values (incubator) |
| 436 | Virtual threads (second preview) |
| 437 | Structured concurrency (second incubator) |
| Project Amber | |
| 432 | Record patterns (second preview) |
| 433 | Pattern matching for switch (fourth preview) |
| Project Panama | |
| 434 | Foreign function and memory API (second preview) |
Project Loom
Project Loom has three JEPs targeting Java 20. The project is designed to deliver JVM features and APIs that support easy-to-use, high-throughput, lightweight concurrency (virtual threads) and new programming models (structured concurrency). In addition, it supplies a handy construct (scoped values) to provide a thread (and, if needed, a group of child threads) with a read-only, thread-specific value during its lifetime. You could think of scoped values as a modern alternative to thread-local variables.
Scoped values (incubator), JEP 429. With the advent of virtual threads, the issues with thread-local variables have worsened. Thread-local variables require more complexity than is typically required for data sharing and come at a high cost that cannot be avoided.
To move toward lightweight sharing for thousands or millions of virtual threads, Java 20 offers scoped values. (These were formerly known as extent-local variables; that term is gone now.) Scoped values maintain immutable and inheritable per-thread data. Because per-thread variables are immutable, they enable effective data sharing between child threads.
Additionally, the lifetime of a per-thread variable ought to be constrained: Once the method that shared the data initially is finished, any data shared via that per-thread variable should no longer be accessible.
A scoped value allows data to be safely and efficiently shared between components in a large program without using method arguments. It is usually a variable of type ScopedValue, and it’s typically declared as a final static field that’s easily accessible from many components.
Unlike a thread-local variable, a scoped value is written once and is then immutable. It is available only for a fixed period during the execution of the thread. Like a thread-local variable, a scoped value has multiple incarnations, one per thread. The incarnation that is used depends on which thread calls its methods.
Consider the following example of a web framework that uses the traditional ThreadLocal class. It has a server component and the data access application component, both of which are running in the same request-handling thread and can use a thread-local variable to share the current user, as follows:
- First, the server component declares a
ThreadLocal<User>variable,CURRENT_USER(line 1 in the code snippet below). - When
serve(...)is executed in a request-handling thread, it writes a suitable user to the thread-local variable (line 2), and then it calls the following code to handle theApplication.handle(request, response)request, which in turn will call theDatabaseManager.open()method. - When the application component code calls
DatabaseManager.open(), the data access component reads the thread-local variable (line 3) to obtain the user of the request-handling thread. - Finally, if the
userindicates proper permissions, the database access is permitted (line 4).
Here is the code.
class Server {
final static ThreadLocal<User> CURRENT_USER = new ThreadLocal<>(); // (1)
void serve(Request request, Response response) {
var level = (request.isAuthorized() ? ADMIN : GUEST);
var user = new User(level);
CURRENT_USER.set(user); // (2)
Application.handle(request, response);
}
}
class DatabaseManager {
DBConnection open() {
var user = Server.CURRENT_USER.get(); // (3)
if (!user.canOpen()) throw new InvalidUserException();
return new DBConnection(...); // (4)
}
}
Here’s how to migrate the previous code from using ThreadLocal to the new, lightweight ScopedValue.
- The Server component replaces the ThreadLocal with a ScopedValue (line 1 in the code snippet below).
- The
serve(...)method is executed, andScopedValue.where(CURRENT_USER, user)presents a scoped value and the object to which it is to be bound (line 2) instead of a thread-local variable’sset(...)method. - The call to
run(...)binds the scoped value, providing an incarnation specific to the current thread, and then it executes the lambda expression passed as an argument (line 3). - During the lifetime of the
run(...)call, the lambda expression, or any method called directly or indirectly from that expression, can read the value from the scoped value via theget()method (line 4), which is the value written byserve(...)earlier in the thread. - After the
run(...)method finishes, the binding is destroyed.
class Server {
final static ScopedValue<User> CURRENT_USER = new ScopedValue<>(); // (1)
void serve(Request request, Response response) {
var level = (request. isAuthorized()? ADMIN : GUEST);
var user = new User(level);
ScopedValue.where(CURRENT_USER, user) // (2)
.run(() -> Application.handle(request, response)); // (3)
}
}
class DatabaseManager {
DBConnection open() {
var user = Server.CURRENT_USER.get(); // (4)
if (!user.canOpen()) throw new InvalidUserException();
return new DBConnection(...);
}
}
Together, where() and run() provide one-way data sharing from the server component to the data access component. The scoped value passed to where() is bound to the corresponding object for the lifetime of the run() call. Therefore, CURRENT_USER.get() in any method called from run() will read that value.
Virtual threads (second preview), JEP 436. Virtual threads are lightweight threads that do not block operating system threads when they must wait for locks, blocking data structures, or responses from external systems.
To allow more time to collect feedback, JEP 436, Virtual threads (second preview), is a second preview of virtual threads, which were first previewed by JEP 425, Virtual threads (preview), in Java 19.
Note that there are no changes in this second preview except that the following small number of APIs from JEP 425 were finalized in JDK 19 and are no longer proposed for preview in JEP 436:
- Two new overloaded methods in the Thread class:
join(Duration)andsleep(Duration). They accept Duration as an argument, as well as a newthreadId()method to return the identifier of this thread. - New methods in the Future interface:
resultNow(): A default method meant to be called on a CompletableFuture, which does the same asget()but does not declare any exception in its signature. This is nice if you want to stream a list of futures and get their results.exceptionNow(): A default method that returns the exception thrown by the task without waiting. This method is for cases where the caller knows that the task has already been completed with an exception.state(): A default method that tells you about the state of this future. The state itself is an enumeration with four values, telling you if the task was canceled, completed with an exception, not completed yet, or finished with a result.
- A change to the ExecutorService, which now extends the AutoCloseable interface so you can create an executive service in try-with-resources statements. It will automatically close this executive service.
- Preparation for the removal of several ThreadGroup methods. Their implementations have been changed to do nothing.
Structured concurrency (second incubator), JEP 437. When a task consists of multiple subtasks that can be processed in parallel, structured concurrency allows you to implement that parallelism in a particularly readable and maintainable way.
JEP 437, Structured concurrency (second incubator), was promoted from the Proposed to Target state to the Targeted state for JDK 20. This JEP proposes reincubating this feature from Java 19’s JEP 428, Structured concurrency (incubator), to allow time for further feedback collection.
The only change is an updated StructuredTaskScope class to support the inheritance of scoped values by threads created in a task scope. This streamlines the sharing of immutable data across all child threads.
Project Amber
Project Amber aims to explore and incubate smaller, productivity-oriented Java language features that have been accepted as candidate JEPs.
Record patterns (second preview), JEP 432. Record patterns were first introduced in Java 19. A record pattern can be used with instanceof or switch to access the fields of a record without casting and calling accessor methods. The following example illustrates what was added in Java 19. Consider the following example record:
record Point(int i, int j) {}
With the record pattern, you can write an instanceof expression as follows:
Object object = new Point(1, 2);
if (object instanceof Point(int i, int j)) {
System.out.println("Object is a Point(" + i + "," + j+")");
}
Notice how you can directly access the x and y values if the object is of type Point. The same can be done in a switch statement as well.
switch (object) {
case Point(int i, int j) -> System.out.println("Object is a Point(" + i + "," + j+")");
// other cases ...
default -> throw new IllegalStateException("Unexpected value: " + object);
}
What is new in Java 20 for record patterns? There are three changes.
- Inference of type arguments for generic record patterns
- Record patterns in
forloops - Removal of support for named record patterns
Inference of type arguments for generic record patterns. Consider a generic interface, Box<T>, and two implementing records, RoundBox<T> and TriangleBox<T>, which contain two and three values of type T, respectively:
interface Box<T> { }
record RoundBox<T>(T t1, T t2) implements Box<T> { }
record TriangleBox<T>(T t1, T t2, T t3) implements Box<T> { }
You can check which concrete implementation a given Box<T> object is by using the following printBoxInfo() method:
Box<String> box = new RoundBox<>("Hello", "World");
printBoxInfo(box);
....
public static <T> void printBoxInfo(Box<T> box) {
if (box instanceof RoundBox<T>(var t1, var t2)) {
System.out.println("RoundBox: " + t1 + ", " + t2);
} else if (box instanceof TriangleBox<T>(var t1, var t2, var t3)) {
System.out.println("TriangleBox: " + t1 + ", " + t2 + ", " + t3);
}
}
Note that with each instanceof check, you had to specify the parameter type (T, in this case). With Java 20, the compiler can infer the type, so you may omit it from the instanceof checks and the method signature as well.
public static void printBoxInfo(Box box) {
if (box instanceof RoundBox(var t1, var t2)) {
System.out.println("RoundBox: " + t1 + ", " + t2);
} else if (box instanceof TriangleBox(var t1, var t2, var t3)) {
System.out.println("TriangleBox: " + t1 + ", " + t2 + ", " + t3);
}
}
Also, with Java 20, the parameter type can be omitted from switch statements. You will see this shortly in the discussion of switch pattern enhancements.
Record patterns in for loops. Using the previous point record definition as an example, if you have a list of points of type record () and you want to do some operations on them, such as to print them to the console, you can do that as follows:
List<Point> points = ...
for (Point p : points) {
System.out.printf("(%d, %d)%n", p.i(), p.j());
}
Starting with Java 20, you can also specify a record pattern in the for loop and then access x and y directly (the same as you can with instanceof and switch).
for (Point(int i, int j) : points) {
System.out.printf("(%d, %d)%n", i, j);
}
Removal of support for named record patterns. In Java 19, there were three ways to perform pattern matching on a record, as follows:
Object object = new Point(1, 2);
// 1. Pattern matching for instanceof
if (object instanceof Point p) {
System.out.println("object is a Point, p.i = " + p.i() + ", p.j = " + p.j());
}
// 2. Record pattern
if (object instanceof Point(int i, int j)) {
System.out.println("object is a Point, i = " + i + ", j = " + j);
}
// 3. Named record pattern
if (object instanceof Point(int i, int j) p) {
System.out.println("object is a Point, p.i = " + p.i() + ", p.j = " + p.j()
+ ", i = " + i + ", j = " + j);
}
In the third variant, a named record pattern, there are two ways to access the fields of the record: via p.i() and p.j() or via the i and j variables. This variant was determined to be superfluous and was removed in Java 20. If you try using it, you should get the following error:
jshell> if (object instanceof Point(int i, int j) p) {
...> System.out.println("object is a Point, p.i = " + p.i() + ", p.j = " + p.j()
...> + ", i = " + i + ", j = " + j);
...> }
...>
| Error:
| ')' expected
| if (object instanceof Point(int i, int j) p) {
| ^
| Error:
| not a statement
| if (object instanceof Point(int i, int j) p) {
| ^
| Error:
| ';' expected
| if (object instanceof Point(int i, int j) p) {
|
Pattern matching for switch (fourth preview), JEP 433. Pattern matching for switch was first previewed in Java 17 as JEP 406, and it lets you write a switch statement such as the following:
jshell> Object obj = new Point(1,4);
obj ==> Point[i=1, j=4]
jshell> 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);
...> case Point(int i, int j) -> System.out.println("i= " + i + ", j= " + j);
...> default -> throw new IllegalStateException("Unexpected: " + object);
...> }
i= 1, j= 4
This feature allows you to use a switch statement to check whether an object is of a specific type (and, if necessary, satisfies additional conditions, as in the first case of String). It also allows you to cast this object simultaneously and implicitly to the target type. You can also combine the switch statement with record patterns to access the record fields directly, as in the case of the Point record.
The following two enhancements were added in Java 20 with the fourth preview of JEP 433:
- MatchException for exhaustive switch
- Inference of type arguments for generic record patterns in switch expressions and statements
MatchException for exhaustive switch. An exhaustive switch (that is, a switch that includes every possible value) now throws a MatchException (rather than an IncompatibleClassChangeError) if it is determined at runtime that no switch label matches—in other words, if it turns out that the switch wasn’t actually exhaustive.
This situation can happen if you extend previously written and compiled code but recompile only the changed classes. Consider this example: Using the Point record shown for JEP 432 in the previous “Record patterns in for loops” section, in the jshell console, define a sealed interface called Shape with the implementations Rectangle and Circle of type record, as follows:
sealed interface Shape permits Rectangle, Circle {}
record Rectangle(Point topLeft, Point bottomRight) implements Shape {}
record Circle(Point center, int radius) implements Shape {}
In jshell, create the following printShapeInfo(Shape shape) method, which prints different information depending on the Shape implementation:
jshell> public void printShapeInfo(Shape shape) {
...> switch (shape) {
...> case Circle(var center, var radius) ->
...> System.out.println( "Circle: Center = " + center + "; Radius = " + radius);
...>
...> case Rectangle(var topLeft, var bottomRight) ->
...> System.out.println( "Rectangle: Top left = " + topLeft +
...> ", Bottom right = " + bottomRight);
...> }
...> }
| created method printShapeInfo(Shape)
Since the compiler knows all possible implementations of the sealed Shape interface, it can ensure that this switch expression is exhaustive. To run a test, from jshell, declare a circle object and call the printShapeInfo method. The result is the following:
jshell> var circle = new Circle(new Point(10, 10), 50) circle ==> Circle[center=Point[i=10, j=10], radius=50] jshell> printShapeInfo(circle); Circle: Center = Point[i=10, j=10]; Radius = 50
Now, imagine that you want to add an Oval shape. Add it to the permits list of the Shape interface, as follows:
jshell> sealed interface Shape permits Rectangle, Circle, Oval {}
| modified interface Shape
jshell> record Oval(Point center, int width, int height) implements Shape {}
| created record Oval
If you do the following in an IDE or jshell, Java will immediately warn that the switch statement in the printShapeInfo() method does not cover all possible values:
jshell> var oval = new Oval(new Point(60, 60), 20, 10);
oval ==> Oval[center=Point[i=60, j=60], width=20, height=10]
jshell> printShapeInfo(oval)
| attempted to call method printShapeInfo(Shape), which cannot be invoked until this error is corrected:
| the switch statement does not cover all possible input values
| switch (shape) {
| ^---------------...
What if you have a big program and each class is in a separate source file—and you recompile only the changed classes and then start execution? The main program will raise the following exception:
$ javac --enable-preview -source 20 Shape.java Oval.java App.java
$ java --enable-preview Main
Exception in thread "main" java.lang.MatchException
at App.main(App.java:56)
The JRE throws a MatchException because the switch statement in the printShapeInfo() method has no case label for the Oval class. A similar error can appear with an exhaustive switch expression over the values of an enum if you subsequently extend the enum.
Inference of type arguments for generic record patterns in switch expressions and statements. As was previously discussed for record patterns with instanceof, the compiler can now also infer the type argument of generic records in a switch statement. Building on the example from the previous “Inference of type arguments for generic record patterns” section, note that before, you had to write a switch statement as follows:
Box<String> box = ...
switch(box) {
case RoundBox<String>(var t1, var t2) ->
System.out.println("RoundBox: " + t1 + ", " + t2);
case TriangleBox<String>(var t1, var t2, var t3) ->
System.out.println("TriangleBox: " + t1 + ", " + t2 + ", " + t3);
...
}
Starting with Java 20, you may omit the <String> type argument inside the switch statement.
switch(box) {
case RoundBox(var t1, var t2) ->
System.out.println("RoundBox: " + t1 + ", " + t2);
case TriangleBox(var t1, var t2, var t3) ->
System.out.println("TriangleBox: " + t1 + ", " + t2 + ", " + t3);
...
}
Project Panama
Project Panama introduces an API for Java applications to interoperate with code and data outside the Java runtime by efficiently invoking foreign functions and safely accessing foreign memory that the JVM does not manage.
Foreign function and memory API (second preview), JEP 434. The foreign function and memory API developed in Project Panama has been evolving for years, starting out as two separate incubator JEPs: JEP 370, Foreign-memory access API, and JEP 389, Foreign linker API. Since Java 19, the unified API has been in the preview stage. Its goal is to replace the cumbersome, error-prone, and slow Java Native Interface (JNI).
The unified API allows access to native memory (that is, memory outside the Java heap) and the execution of native code (for example, from C libraries) from Java. With JEP 434, quite a few changes were made to the API.
I shall repeat the example I used in “Hidden gems in Java 19, Part 1: The not-so-hidden JEPs” updated to show the changes for Java 20 in the following code. The example program stores a String in off-heap memory and calls a C standard library function, strlen, which returns the length of a given string. Finally, it prints the result to the console.
public long getStringLength(String content) throws Throwable {
// 1. Get a lookup object for commonly used libraries
SymbolLookup stdlib = Linker.nativeLinker().defaultLookup();
// 2. Get a handle on the strlen function in the C standard library
MethodHandle strlen = Linker.nativeLinker().downcallHandle(
stdlib.find("strlen").orElseThrow(),
FunctionDescriptor.of(JAVA_LONG, ADDRESS));
long len = 0;
// 3. Convert Java String to C string and store it in off-heap memory
try (Arena offHeap = Arena.openConfined()) {
MemorySegment str = offHeap.allocateUtf8String(content);
// 4. Invoke the foreign function
len = (long) strlen.invoke(str);
}
// 5. Off-heap memory is deallocated at the end of try-with-resources
// 6. Return the length.
return len;
}
To run the program with Java 20, perform the following four steps:
- Include the
--enable-previewand--enable-native-access=ALL-UNNAMEDflags when you launchjshell, as follows:
[mtaman]:~ jshell --enable-preview --enable-native-access=ALL-UNNAMED | Welcome to JShell -- Version 20-ea | For an introduction type: /help intro jshell>
- Import the required classes for successful creation and invocation of the previous
getStringLength(String content)function, as follows:
jshell> import java.lang.foreign.*; ...> import java.lang.invoke.MethodHandle; ...> import static java.lang.foreign.ValueLayout.ADDRESS; ...> import static java.lang.foreign.ValueLayout.JAVA_LONG; jshell>
- Create the
getStringLength()method, as follows:
jshell> public static long getStringLength(String content) throws Throwable { ...> // 1. Get a lookup object for commonly used libraries ...> SymbolLookup stdlib = Linker.nativeLinker().defaultLookup(); ...> ...> // 6. Return the length. ...> return len; ...> } | created method getStringLength(String) - Call the function with a string, which will return the length of the string, as follows:
jshell> getStringLength("Welcome to FFM in Java 20!") $6 ==> 26
Conclusion
One of the critical improvements in Java 20, and probably the most significant improvement I discussed, is scoped values, which provide a thread (and, if needed, a group of child threads) with a read-only, thread-specific value during its lifetime. Scoped values are a modern alternative to thread-local variables.
Overall, Java 20 is a significant release that will bring many valuable and vital improvements to the language and platform.