Java 19 has seven main JEPs, which is a lower count than the nine JEPs in Java 18, the 14 JEPs in Java 17, the 17 JEPs in Java 16, the 14 JEPs in Java 15, and the 16 JEPs in Java 14. However, focusing on quantity doesn’t tell the story of Java 19, which contains extremely important JEPs for the future-looking
Panama,
Amber, and
Loom projects, as well as porting the JDK to the Linux/RISC-V instruction set.
I am using the Java 19.0.1 jshell tool to demonstrate the code in this article. If you want to test the features,
download JDK 19, fire up your terminal, check your version, and run jshell, as follows. Note that you might see a newer dot-release version of the JDK, but nothing else should change.
[mtaman]:~ java -version
java version "19.0.1" 2022-10-18
Java(TM) SE Runtime Environment (build 19.0.1+10-21)
Java HotSpot(TM) 64-Bit Server VM (build 19.0.1+10-21, mixed mode, sharing)
[mtaman]:~ jshell --enable-preview
| Welcome to JShell -- Version 19.0.1
| For an introduction type: /help intro
jshell>
Be aware of two important notes.
◉ Two of the JEPs covered in this article are published as incubator modules to solicit developer feedback. An incubator module’s API could be altered or disappear entirely, so don’t count on it being in a future Java release. You should play with incubator modules but not use them in production code. To use incubator modules, use the --add-modules JVM switch.
◉ If a JEP is a preview feature, it is fully specified and implemented but is not finalized. Thus, it should not be used in production code. Use the switch --enable-preview to use those features.
The following JEPs are in Java 19:
Project Loom
◉ JEP 425: Virtual threads (first preview)
◉ JEP 428: Structured concurrency (first incubator)
Project Amber
◉ JEP 405: Record patterns (first preview)
◉ JEP 427: Pattern matching for switch (third preview)
Project Panama
◉ JEP 424: Foreign Function and Memory API (first preview)
◉ JEP 426: Vector API (fourth incubator)
In addition, there’s the following hardware port JEP:
◉ JEP 422: Linux/RISC-V port
Project Loom JEPs
Project Loom is designed to deliver new JVM features and APIs to support easy-to-use, high-throughput, lightweight concurrency as well as a new programming model, which is called structured concurrency.
Virtual threads. In JEP 425, Java 19 introduces virtual threads to the Java platform as a first preview. It is one of the most significant updates to Java in a very long time, but it is also a change that is hardly noticeable. Even though there are many excellent articles regarding virtual threads, such as Nicolai Parlog’s “
Coming to Java 19: Virtual threads and platform threads,” I cannot discuss other impending features without first giving a brief overview of virtual threads.
Virtual threads fundamentally redefine the interaction between the Java runtime and the underlying operating system, removing significant barriers to scalability. Still, they don’t dramatically change how you create and maintain concurrent programs. Virtual threads behave almost identically to the threads you are familiar with, and there is barely any additional API.
Let’s look at them from a different view by asking the following question: Why do developers need virtual threads?
Anyone who has ever worked on a back-end application under high load is aware that threads are frequently the bottleneck. A thread is required for each incoming request to be processed. One Java thread corresponds to one operating system thread, consuming many resources. It’s best to start with a few hundred threads; otherwise, the entire system’s stability is jeopardized.
However, in real life, more than a few hundred threads are often required, especially if processing a request takes longer due to the need to wait for blocking data structures such as queues, locks, or external services such as databases, microservices, or cloud APIs.
For example, if a request takes two seconds and the thread pool is limited to 100 threads, the application could serve up to 50 requests per second. Even if several threads are served per CPU core, the CPU would be underutilized because it would spend most of its time waiting for responses from external services. So, you really need thousands of threads—or maybe tens of thousands. However, you’re not going to get that from your hardware.
Sadly, reactive code is often more complex than sequential code, and it can be hard to maintain. Here’s an example.
public DeferredResult<ResponseEntity<?>> createOrder(
CreateOrderRequest createOrderRequest, Long sessionId, HttpServletRequest context) {
DeferredResult<ResponseEntity<?>> deferredResult = new DeferredResult<>();
Observable.just(createOrderRequest)
.doOnNext(this::validateRequest)
.flatMap(
request ->
sessionService
.getSessionContainer(request.getClientId(), sessionId)
.toObservable()
.map(ResponseEntity::getBody))
.map(
sessionContainer ->
enrichCreateOrderRequest(createOrderRequest, sessionContainer, context))
.flatMap(
enrichedRequest ->
orderPersistenceService.persistOrder(enrichedRequest).toObservable())
.subscribeOn(Schedulers.io())
.subscribe(
success -> deferredResult.setResult(ResponseEntity.noContent()),
error -> deferredResult.setErrorResult(error));
return deferredResult;
}
In the reactive world, all the above code merely defines the reactive flow but doesn’t execute it; the code is executed only after the call to subscribe() (at the end of the method) in a separate thread pool. For this reason, it doesn’t make any sense to set a breakpoint at any line of the code above. Therefore, this code is hardly readable and is also tough to debug.
Additionally, the database and external services drivers’ maintainer must support the reactive model, and you’re not going to see that very often.
Virtual threads are a better solution because they allow you to write code that is quickly readable and maintainable without having to jump through hoops. That’s because virtual threads are like normal threads from a Java code perspective, but they are not mapped 1:1 to operating system threads.
Instead, there is a pool of so-called carrier threads onto which a virtual thread is temporarily mapped. The carrier thread can execute another virtual thread (a new thread or a previously blocked thread). As soon as the virtual thread encounters a blocking operation, the virtual thread is removed from the carrier threads.
Thus, blocking operations no longer block the executing thread; this lets the JVM process many requests in parallel with a small pool of carrier threads, allowing you to reimplement the reactive example above quite simply as the following:
public void createOrder(
CreateOrderRequest createOrderRequest, Long sessionId, HttpServletRequest context) {
validateRequest(createOrderRequest);
SessionContainer sessionContainer =
sessionService
.getSessionContainer(createOrderRequest.getClientId(), sessionId)
.execute()
.getBody();
EnrichedCreateOrderRequest enrichedCreateOrderRequest =
enrichCreateOrderRequest(createOrderRequest, sessionContainer, context);
orderPersistenceService.persistOrder(enrichedCreateOrderRequest);
}
As you can see, such code is easier to read and write, just as any sequential code is, and it’s also easier to debug by conventional means.
I believe that once you start using virtual threads, you will never switch back to reactive programming. Even better, you can continue to use your code unchanged with virtual threads because (thanks to this new JEP) it is part of the JDK. Well, it’s there as a preview; in a future Java version, virtual threads will be a standard feature.
Structured concurrency. In JEP 428, structured concurrency, which is an incubator module in Java 19, helps to simplify error management and subtask cancellation. Structured concurrency treats concurrent tasks operating in distinct threads as a single unit of work, improving observability and dependability.
Suppose a function contains several invoice-creating subtasks that need to be done in parallel, such as getting data from a database with getOrderBy(orderId), calling a remote API with getCustomerBy(customerId), and loading and reading data from a file with getTemplateFor(language). You could use the Java executable framework, for example, as in the following:
private final ExecutorService executor = Executors.newCachedThreadPool();
public Invoice createInvoice(int orderId, int customerId, String language)
throws InterruptedException, ExecutionException {
Future<Customer> customerFuture =
executor.submit(() -> customerService.getCustomerBy(customerId));
Future<Order> orderFuture =
executor.submit(() -> orderService.getOrderBy(orderId));
Future<String> invoiceTemplateFuture =
executor.submit(() -> invoiceTemplateService.getTemplateFor(language));
Customer customer = customerFuture.get();
Order order = orderFuture.get();
String template = invoiceTemplateFuture.get();
return invoice.generate(customer, order, template);
}
You can pass the three subtasks to the executor and wait for the partial results. It is easy to implement the basic task quickly, but consider these possible issues.
◉ How can you cancel other subtasks if an error occurs in one subtask?
◉ How can you cancel the subtasks if the invoice is no longer needed?
◉ How can you handle and recover from exceptions?
All these possible issues can be addressed, but the solution would require complex and difficult-to-maintain code.
And, more importantly, what if you want to debug this code? You can generate a thread dump, but it would give you a bunch of threads named pool-X-thread-Y. And you wouldn’t know which pool thread belongs to which calling threads since all calling threads share the executor’s thread pool.
The new structured concurrency API improves the implementation, readability, and maintainability of code for requirements of this type. Using the StructuredTaskScope class, you can rewrite the previous code as follows:
Invoice createInvoice(int orderId, int customerId, String language)
throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<Customer> customerFuture =
scope.fork(() -> customerService.getCustomerBy(customerId));
Future<Order> orderFuture =
scope.fork(() -> orderService.getOrderBy(orderId));
Future<String> invoiceTemplateFuture =
scope.fork(() -> invoiceTemplateService.getTemplateFor(language));
scope.join(); // Join all forks
scope.throwIfFailed(); // ... and propagate errors
Customer customer = customerFuture.resultNow();
Order order = orderFuture.resultNow();
String template = invoiceTemplateFuture.resultNow();
// Here, both forks have succeeded, so compose their results
return invoice.generate(customer, order, template);
}
}
There’s no need for the ExecutorService in the scope of the class, so I replaced it with a StructuredTaskScope located in the method’s scope. Similarly, I replaced executor.submit() with scope.fork().
By using the scope.join() method, you can wait for all tasks to be completed—or for 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 several improvements over the old one.
◉ When you run the task, the subtasks form a self-contained unit of work in the code; you no longer need ExecutorService in a higher scope. The threads do not come from a thread pool; 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.
To try the example yourself, you must explicitly add the incubator module to the module path and also enable preview features in Java 19. For example, if you have saved the code in a file named JDK19StructuredConcurrency.java, you can compile and run it as follows:
$ javac --enable-preview -source 19 --add-modules jdk.incubator.concurrent JDK19StructuredConcurrency.java
$ java --enable-preview --add-modules jdk.incubator.concurrent JDK19StructuredConcurrency
Project Amber JEPs
JEP 405 and JEP 427 are part of
Project Amber, which focuses on smaller Java language features that can improve developers’ everyday productivity.
Pattern matching for switch. This is a feature that has already gone through two rounds of previews. First appearing in Java 17, pattern matching for switch allows you to write code like the following:
switch (obj) {
case String s && s.length() > 8 -> System.out.println(s.toUpperCase());
case String s -> System.out.println(s.toLowerCase());
case Integer i -> System.out.println(i * i);
default -> {}
}
You can use pattern matching to check if an object within a switch statement is an instance of a particular type and if it has additional characteristics. In the Java 17–compatible example above, the goal is to find strings longer than eight characters.
To improve the readability of this feature, Java 19 changed the syntax. In Java 17 and Java 18, the syntax was to write String s && s.length() > 0; now, in Java 19, instead of &&, you must use the easier-to-read keyword when.
Therefore, the previous example would be written in Java 19 as the following:
switch (obj) {
case String s when s.length() > 8 -> System.out.println(s.toUpperCase());
case String s -> System.out.println(s.toLowerCase());
case Integer i -> System.out.println(i * i);
default -> {}
}
What’s also new is that the keyword when is a contextual keyword; therefore, it has a meaning only within a case label. If you have variables or methods with the name when in your code, you don’t need to change them. This change won’t break any of your other code.
Record patterns. I am still discussing the topic of pattern matching here because JEP 405 is related to it. If the subject of records is new to you, “Records come to Java” by Ben Evans should help.
A record pattern comprises three components.
◉ A type
◉ A list of record component pattern matches
◉ An optional identifier
Record and type patterns can be nested to allow for robust, declarative, and modular data processing. It is better to explain this with an example, so let me clarify what a record pattern is. Assume you have defined the following Point record:
public record Point(int x, int y) {}
You also have a print() method that can print any object, including positions.
private void print(Object object) {
if (object instanceof Point point) {
System.out.println("object is a point, x = " + point.x()
+ ", y = " + point.y());
}
// else ...
}
You might have seen this notation before; it was introduced in Java 16 as pattern matching for instanceof.
Record pattern for instanceof. As of Java 19, JEP 405 allows you to use a new feature called a record pattern. This new addition allows you to write the previous code as follows:
private void print(Object object) {
if (object instanceof Point(int x, int y)) {
System.out.println("object is a point, x = " + x + ", y = " + y);
}
// else ...
}
Instead of needing to match on Point point and access point fields with the whole object, as in the previous code, you now can match on Point(int x, int y) and can then access their x and y fields directly.
Record pattern with switch. Previously with Java 17, you could also write the original example as a switch statement.
private void print(Object object) {
switch (object) {
case Point point
-> System.out.println("object is a point, x = " + point.x()
+ ", y = " + point.y());
// other cases ...
}
}
You can now also use a record pattern in the switch statement.
private void print(Object object) {
switch (object) {
case Point(int x, int y)
-> System.out.println("object is a point, x = " + x + ", y = " + y);
// other cases ...
}
}
Nested record patterns. It is now possible to match nested records. Here’s another example that defines a second record, Line, with a start point and a destination point, as follows:
public record Line(Point from, Point to) {}
The 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 Line(Point(int x1, int y1), Point(int x2, int y2))) {
System.out.println("object is a Line, x1 = " + x1 + ", y1 = " + y1
+ ", x2 = " + x2 + ", y2 = " + y2);
}
// else ...
}
Alternatively, you can write the code as a switch statement.
private void print(Object object) {
switch (object) {
case Line(Point(int x1, int y1), Point(int x2, int y2))
-> System.out.println("object is a Line, x1 = " + x1 + ", y1 = " + y1
+ ", x2 = " + x2 + ", y2 = " + y2);
// other cases ...
}
}
Thus, record patterns provide an elegant way to access a record’s elements after a type check.
Project Panama JEPs
The
Project Panama initiative, which includes JEP 424 and JEP 426, focuses on interoperability between the JVM and well-defined foreign (non-Java) APIs. These APIs often include interfaces that are used in C libraries.
Foreign functions and foreign memory. In Project Panama, a replacement for the error-prone, cumbersome, and slow Java Native Interface (JNI) has been in the works for a long time.
The Foreign Linker API and the Foreign Memory Access API were already introduced in Java 14 and Java 16, respectively, as incubator modules. In Java 17, these APIs were combined to form the single Foreign Function and Memory API, which remained in the incubator stage in Java 18.
Java 19’s JEP 424 has promoted the new API from incubator to 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 and Memory API enables access to native memory (that is, memory outside the Java heap) and access to native code (usually C libraries) directly from Java.
The following examples store a string in off-heap memory, followed by a call to the C standard library’s strlen function to return the string length.
public class ForeignFunctionAndMemoryTest {
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 on 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);
}
}
The FunctionDescriptor expects the foreign function’s return type as the first parameter, with the function’s arguments coming in as extra parameters. The FunctionDescriptor handles accurate conversion of all Java types to C types and vice versa.
Since the Foreign Function and Memory API is still in the preview stage, you must specify a few parameters to compile and run the code.
$ javac --enable-preview -source 19 ForeignFunctionAndMemoryTest.java
$ java --enable-preview ForeignFunctionAndMemoryTest
As a developer who has worked with JNI—and remembers how much Java and C boilerplate code I had to write and keep in sync—I am delighted that the effort required to call the native function has been reduced by orders of magnitude.
Vector math. I’ll start by dispelling a possible point of confusion: The new Vector API has nothing to do with the java.util.Vector class. Instead, this is a new API for mathematical vector computation and mapping to modern Single-Instruction-Multiple-Data (SIMD) CPUs.
The Vector API attempts to make it easier for native code and JVM code to communicate with one another. The Vector API is also the fourth incubation of the API that defines vector computations that successfully compile at runtime to optimal vector instructions on supported CPU architectures, outperforming equivalent scalar computations.
With the help of the user model in the API, developers can use the HotSpot JVM’s autovectorizer to design sophisticated vector algorithms in Java that are more reliable and predictable.
The Vector API has been a part of the JDK since Java 16 as an incubator module, and in Java 17 and Java 18 it underwent significant development. The Foreign Function and Memory API preview defines improvements to loading and storing vectors to and from memory segments as part of the API proposed for JDK 19.
Along with the complementing vector mask compress operation, Java 19’s new JEP 426 adds the cross-lane vector operations of compress and expand. The compress operation maps the lanes of a source vector—which are chosen by a mask—to a destination vector in lane order. The compress procedure improves the query result filtering. The expand operation does the opposite.
You can also expand bitwise integral lane-wise operations, including counting the number of one bits, reversing the order of bits, and compressing and expanding bits.
The API’s objectives include being unambiguous, being platform-neutral, and having dependable runtime and compilation performance on the x64 and AArch64 architectures.
The hardware port
RISC-V is a free, open source instruction set architecture that’s becoming increasingly popular, and now there’s a Linux JDK for that architecture in JEP 422. A wide range of language toolchains already supports this hardware instruction set.
Currently, the Linux/RISC-V port will support only one general-purpose 64-bit instruction set architecture with vector instructions: an
RV64GV configuration of RISC-V. More may be supported in the future.
The HotSpot JVM subsystems that are supported with this new Java 19 feature are
◉ C1 (client) just-in-time (JIT) compiler
◉ C2 (server) JIT compiler
◉ Template interpreter
◉ All mainline garbage collectors, including ZGC and Shenandoah
Source: oracle.com