Java 19: The new features

Farzin Pashaee
CodeX
6 min readJan 3, 2023

--

Oracle announced the availability of Java 19 on September 20, 2022. Numerous performance, stability, and security improvements are included in Java 19 (Oracle JDK 19), as well as platform upgrades that will help developers increase productivity and promote business-wide innovation.

Updates and Improvements

JEP 405 — Add record patterns (preview)

Add record patterns to the Java programming language to help you break down record values. It is possible to nest record patterns and type patterns to provide a robust, declarative, and modular method of data processing and navigation. This feature allows users to nest

Records were a suggestion made by JEP 359 and included as a preview feature in JDK 14. In JDK 16 Release, Records were no anymore a preview feature and stable to use after feedback from versions 14 and 15. The Java language now has a new type of class called a record class. Record classes, which are less formal than regular classes, assist in modeling simple data aggregates.

public record Student(int id, String firstName, String lastName) {}
public class Main {
public static void main(String[] args) {
Student student = new Student(1, "John", "Doe");
System.out.println( "Name: " + student.firstName() + " "
+ student.lastName() );
// Name: John Doe
}
}

In addition to that, JEP 394 expanded the instanceof operator in JDK 16 to allow pattern matching with a type pattern.

// Old version sample
if (object instanceof String) {
String s = (String) object;
... use s ...
}

// Newer version code
if (object instanceof String s) {
... use s ...
}

Combining the record pattern and type pattern will give developers a powerful data navigation and processing tool. In the following example, you can see how to utilize record patterns.

record Student(int id, String name) {}

void printName(Object o) {
if (o instanceof Student(int id, String name)) {
System.out.println("Name: " + name);
}
}

Student(int id, String name) is a record Pattern. When a value is matched against the pattern, it moves the declaration of local variables for extracted components into the pattern itself and initializes those variables by invoking the accessor methods. This can be extended into multiple nested records like the following example:

record Student(int id, String name) {}
record Exam(String name, Student student, Long score) {}

void printExamResult(Object o) {
if (o instanceof Exam(String name, Student student, Long score)) {
System.out.println("Name: " + student.name());
}
}

JEP 427 — Pattern Matching for switches (Third Preview)

Pattern Matching for switches was first proposed by JEP 406 as a preview feature and released in JDK 17, and then by JEP 420 as a second preview and released in JDK 18. We frequently want to compare several possibilities with a variable. Using switch statements and switch expressions (JEP 361), Java offers multi-way comparisons, however, the switch is sadly rather limited. Only integral primitive types (with the exception of long), their wrapped variants, enum types, and String values can be used with the switch.

Since the current switch does not do that, we end up with a chain of if…else tests instead of using patterns to test the same variable against a variety of possibilities and take a specific action on each.

if (object instanceof Integer i) {
// ...
} else if (object instanceof Long l) {
// ...
} else if (object instanceof String s) {
// ...
}

Using pattern instanceof expressions are advantageous in this code, but it is not flawless. We can rewrite the above code to be more understandable and reliable if we allow case labels to contain patterns rather than just constants and extend switch statements and expressions to work on any type.

switch (object) {
case Integer i -> // ...
case Long l -> // ...
case String s -> // ...
default -> // ...
};

This code’s intention is more obvious because the appropriate control construct is being used. It is also optimizable, which increases the likelihood that we will be able to complete the dispatch in O(1) time in this situation.

Library Tools

JEP 424Foreign Function and Memory API (Preview)

Foreign Function and Memory API introduce an API that Java programs can use to communicate with other programs and data that aren’t running on the Java runtime. This will allow them to safely access external memory and effectively call foreign functions. The goal for this API is Ease of use, Performance, Safety, and Generality. It will be an option to replace the Java Native Interface (JNI) with a superior, pure-Java development model and provide performance at the level of existing libraries.

Off-heap data is the data that is stored outside of the Java runtime memory. Popular Java libraries like Tensorflow, Ignite, Lucene, and Netty must access off-heap data in order to function properly, partly because doing so spares them the expense and unpredictability of garbage collection.

Since Java 1.1, JNI has supported calling native code but it is insufficient for a variety of reasons. JNA, JNR, and JavaCPP are just a few of the frameworks that have emerged throughout time to cover the voids left by JNI. Even though these frameworks frequently represent a significant step up from JNI, the situation is still far from ideal.

The Foreign-Memory Access API (JEPs 370, 383, and 393) and the Foreign Linker API, are combined into the Foreign Function & Memory (FFM) API (JEP 389) which introduces classes and interfaces that can Allocate foreign memory, Manipulate/Access structured foreign memory, and Call foreign functions.

JEP 426 — Vector API (Fourth Incubator)

Vector API was introduced in order to have a high-performance solution compared to any equivalent scalar computations for vector computing and vector instructions on supported CPU architectures.

Ports

JEP 422Linux/RISC-V Port

Linux/RISC-V Port By incorporating it into the JDK main-line repository, it paves the way for simpler Linux/RISC-V implementations. RISC-V which originally created at the University of California, Berkeley, currently collaboratively developing under the sponsorship of RISC-V International.

RISC-V ISA is a load–store architecture and is a family of related ISAs. The main focus of this JEP is the integration of the port into the JDK main-line repository.

Project Loom Preview/Incubator Features

JEP 425 —Virtual Threads (Preview)

Virtual threads are lightweight threads with small footprints which considerably reduce the effort of writing, maintaining, and observing high-throughput concurrent systems. For almost three decades, concurrent server applications have been built using threads by Java programmers. A sequential chunk of code that executes simultaneously with and generally independently of other such units is known as a “thread” in Java.

Because the JDK implements threads as wrappers around operating system (OS) threads, the total number of threads is unfortunately constrained. We cannot have too many OS threads because they are expensive, hence the implementation is not suitable for the thread-per-request method. We should work to maintain the thread-per-request style by implementing threads more effectively so they can be more plentiful, allowing apps to scale while remaining in harmony with the platform.

A virtual thread is a Java.lang.Thread the instance that is not tied to a particular OS thread. It works as a platform thread implemented like a thin wrapper around an OS thread. Because they are inexpensive and plentiful, virtual threads shouldn’t ever be pooled: Each task of the application should start a new virtual thread. Therefore, the majority of virtual threads will be short-lived and have shallow call stacks, carrying out just one HTTP client call or one JDBC query.

Executor executor = Executors.newVirtualThreadPerTaskExecutor();
executor.execute( () -> {
// do something inside thread
});

JEP 428 — Structured Concurrency (Incubator)

Structured Concurrency Programming for multiple threads can be made easier by using a defined concurrency API. Multiple tasks executing on various threads are treated as a single unit of work by structured concurrency, which streamlines error handling and cancellation increases dependability and improves observability.

StructuredTaskScope scope = new StructuredTaskScope();
Future<String> task1 = scope.fork( () -> {
return "Task 1";
});
Future<String> task2 = scope.fork( () -> {
return "Task 2";
});
scope.join();
System.out.println(task1.get());
System.out.println(task2.get());

In structured concurrency, subtasks carry out a task’s instructions. The task watches for failures and awaits the results of the subtasks. The lifespan of a concurrent subtask is limited to the syntactic block of its parent task since the entry and exit locations of a block of code are clearly specified. Because sibling subtasks’ lifetimes are nested inside that of their parent tasks, they can be thought of and controlled as a single entity.

I hope this article helped you, and please support me by applauding 👏 for the story. If you don’t know how it works, it’s just like this:

Or buy me a coffee here!

--

--

Farzin Pashaee
CodeX
Writer for

Software Engineer at Maybank, AI and ML enthusiastic