{getToc} $title={Table of Contents}
What’s New in Java 19: A Comprehensive Overview of the Latest Features |
I. Introduction
Java 19 is the latest version of the world’s number one programming language
and development platform. It was released on September 20, 2022 as a non-LTS
(long-term support) release that delivers thousands of performances,
stability, and security improvements.
Java 19 also introduces seven new features and enhancements that will help
developers improve productivity, expressiveness, and concurrency in their
applications. These features are:
-
Structured concurrency: an incubating feature that simplifies multithreaded programming by
treating multiple tasks running in different threads as a single unit
of work.
-
Record patterns: a preview feature that extends pattern matching for instanceof by
allowing deconstruction of record values and nesting of type
patterns.
-
Foreign function and memory API: a preview feature that enables interoperability with native code
and data by providing an API to invoke foreign functions and access
foreign memory.
-
Virtual threads: a preview feature that enables lightweight concurrency model by
allowing creation of millions of concurrent tasks without exhausting
resources.
-
Pattern matching for switch expressions: a standard feature
that enhances switch expressions with pattern matching
capabilities.
-
Vector API:
an incubating feature that leverages vector instructions to optimize
computations on arrays of primitive data types.
- Linux/RISC-V port: a standard feature that supports the open-source Linux/RISC-V instruction set architecture (ISA)
In this blog post, we will provide a comprehensive overview of these
features and show you how to use them in your code. You can download Java 19
from
oracle.com or
use it with your favorite IDE or toolchain.
II. Body
1. Structured Concurrency
Structured concurrency is an incubating feature that aims to simplify
multithreaded programming by treating multiple tasks running in different
threads as a single unit of work. This means that if one task fails or is
cancelled, all the other tasks in the same scope are also cancelled and
cleaned up. This avoids common problems such as
thread leaks, cancellation delays, and unrelated thread dumps.
Structured concurrency also improves the reliability and observability of
concurrent code by providing better error handling and diagnostics.
To use structured concurrency, Java 19 introduces a new API called
StructuredTaskScope that allows creating and managing concurrent
tasks in a structured way.
Creating and joining concurrent tasks using
StructuredTaskScope:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<Shelter> shelter = scope.fork(this::getShelter);
Future<List<Dog>> dogs = scope.fork(this::getDogs);
scope.join(); // Wait for all tasks to complete
Response response = new Response(shelter.resultNow(), dogs.resultNow());
// ...
}
Cancelling concurrent tasks using StructuredTaskScope:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<Shelter> shelter = scope.fork(this::getShelter);
Future<List<Dog>> dogs = scope.fork(this::getDogs);
if (Thread.interrupted()) { // Check for interruption
scope.cancel(); // Cancel all tasks in the scope
throw new InterruptedException();
}
Response response = new Response(shelter.resultNow(), dogs.resultNow());
// ...
}
Handling errors in concurrent tasks using
StructuredTaskScope:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<Shelter> shelter = scope.fork(this::getShelter);
Future<List<Dog>> dogs = scope.fork(this::getDogs);
try {
Response response = new Response(shelter.result(), dogs.result()); // Wait for all tasks to complete and rethrow any exception
// ...
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // Get the cause of the exception
if (cause instanceof IOException) {
// Handle IOException
} else if (cause instanceof RuntimeException) {
// Handle RuntimeException
} else {
throw e; // Rethrow other exceptions
}
}
}
2. Record Patterns
Record patterns are a preview feature that extends pattern matching for
instanceof by allowing deconstruction of record values and nesting of type
patterns. Record patterns let you match values against a record type and
bind variables to the corresponding components of the record.
Record patterns work well with sealed types, as they enable exhaustive
matching and data navigation. To use record patterns, Java 19 introduces a
new syntax that resembles variable declarations.
For example, if you have a record Point(int x, int y) and an object
o that might be an instance of it, you can write:
if (o instanceof Point(int x, int y)) {
// Do something with x and y
}
Here, Point(int x, int y) is a record pattern that binds x and y to
the components of the record value. You can also use var instead of
specifying the types explicitly.
You can also nest record patterns inside other patterns to match complex
data structures. For example, if you have another record
Circle(Point center, int radius), you can write:
if (o instanceof Circle(Point(var x, var y), var r)) {
// Do something with x, y and r
}
Using record patterns with instanceof to deconstruct record values:
record Point(int x, int y) {}
record Circle(Point center, int radius) {}
Object o = new Circle(new Point(1, 2), 3);
if (o instanceof Circle(Point(var x, var y), var r)) {
// Do something with x, y and r
}
Using record patterns with switch expressions to match against sealed
types:
sealed interface Shape permits Circle, Rectangle {}
record Circle(Point center, int radius) implements Shape {}
record Rectangle(Point topLeft, Point bottomRight) implements Shape {}
Shape s = new Rectangle(new Point(0, 0), new Point(4, 3));
int area = switch (s) {
case Circle(var c, var r) -> (int) Math.PI * r * r;
case Rectangle(Point(var x1, var y1), Point(var x2, var y2)) -> Math.abs(x1 - x2) * Math.abs(y1 - y2);
};
Using record patterns with enhanced for statements to iterate over
collections of records:
record Person(String name, int age) {}
List<Person> people = List.of(
new Person("Alice", 25),
new Person("Bob", 30),
new Person("Charlie", 35)
);
for (Person(String name, var age) : people) {
System.out.println(name + " is " + age + " years old");
}
3. Foreign function and memory API
Foreign function and memory API is a preview feature that enables
interoperability with native code and data by providing an API to invoke
foreign functions and access foreign memory.
Foreign functions are code outside the JVM, such as C libraries or system
calls. Foreign memory is memory not managed by the JVM, such as off-heap
buffers or native structures.
To use foreign function and memory API, Java 19 introduces several
abstractions such as MemorySegment, MemoryAddress, MemoryLayout, Linker,
SymbolLookup and FunctionDescriptor that allow creating and manipulating
native values and pointers in a safe and efficient way.
For example, you can use this API to call a C function that computes the
length of a string:
// Allocate off-heap memory for the string
try (MemorySegment segment = MemorySegment.allocateNative(CLinker.C_CHAR.withSize(10))) {
// Write "Hello" to the segment
segment.setUtf8String(0, "Hello");
// Obtain an instance of the native linker
Linker linker = Linker.nativeLinker();
// Locate the address of the strlen function
SymbolLookup stdLib = linker.defaultLookup();
MemoryAddress strlen_addr = stdLib.lookup("strlen").get();
// Create a description of the function signature
FunctionDescriptor strlen_sig = FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS);
// Create a downcall handle for the function
MethodHandle strlen = linker.downcallHandle(strlen_addr, strlen_sig);
// Call the function directly from Java
long len = (long)strlen.invoke(segment.address());
}
Invoking a foreign function that computes the length of a string:
// Allocate off-heap memory for the string
try (MemorySegment segment = MemorySegment.allocateNative(CLinker.C_CHAR.withSize(10))) {
// Write "Hello" to the segment
segment.setUtf8String(0, "Hello");
// Obtain an instance of the native linker
Linker linker = Linker.nativeLinker();
// Locate the address of the strlen function
SymbolLookup stdLib = linker.defaultLookup();
MemoryAddress strlen_addr = stdLib.lookup("strlen").get();
// Create a description of the function signature
FunctionDescriptor strlen_sig = FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS);
// Create a downcall handle for the function
MethodHandle strlen = linker.downcallHandle(strlen_addr, strlen_sig);
// Call the function directly from Java
long len = (long)strlen.invoke(segment.address());
}
Accessing a foreign memory that represents a C struct:
// Define a memory layout for struct Point { int x; int y; }
MemoryLayout pointLayout = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"),
ValueLayout.JAVA_INT.withName("y")
);
// Allocate off-heap memory for an instance of Point
try (MemorySegment pointSegment = MemorySegment.allocateNative(pointLayout)) {
// Write values to x and y fields using layout paths
VarHandle xHandle = pointLayout.varHandle(int.class, PathElement.groupElement("x"));
VarHandle yHandle = pointLayout.varHandle(int.class, PathElement.groupElement("y"));
xHandle.set(pointSegment.baseAddress(), 10);
yHandle.set(pointSegment.baseAddress(), -5);
// Read values from x and y fields using layout paths
int xValue = (int)xHandle.get(pointSegment.baseAddress());
int yValue = (int)yHandle.get(pointSegment.baseAddress());
}
4. Virtual Threads
Virtual threads are a preview feature that enables lightweight concurrency
model by allowing creation of millions of concurrent tasks without
exhausting resources.
Virtual threads are user-mode threads scheduled by the Java virtual machine
rather than the operating system. Virtual threads require few resources and
can be suspended and resumed when they perform blocking operations such as
I/O or synchronization.
This means that a single OS thread can execute multiple virtual threads in a
cooperative manner. To use virtual threads, Java 19 introduces a new API
called ThreadBuilder that allows creating and starting virtual threads in a
fluent way.
For example, you can use this API to create and start 10,000 virtual tasks
that print “Hello” to the console:
for (int i = 0; i < 10_000; i++) {
ThreadBuilder.virtual().task(() -> System.out.println("Hello")).start();
}
Creating and starting a virtual thread using
Thread.startVirtualThread:
Runnable task = () -> System.out.println("Hello from virtual thread " + Thread.currentThread());
Thread vt = Thread.startVirtualThread(task);
Creating and starting a virtual thread using ThreadBuilder:
Runnable task = () -> System.out.println("Hello from virtual thread " + Thread.currentThread());
Thread vt = ThreadBuilder.virtual().task(task).start();
Creating and starting a virtual thread using
ExecutorService:
Runnable task = () -> System.out.println("Hello from virtual thread " + Thread.currentThread());
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.execute(task);
5. Pattern Matching for Switch Expressions
Pattern matching for switch expressions is a standard feature that enhances
switch expressions with pattern matching capabilities.
Pattern matching allows an expression to be tested against a number of
patterns, each with a specific action, so that complex data-oriented queries
can be expressed concisely and safely.
Pattern matching for switch expressions supports three kinds of patterns:
type patterns, record patterns, and constant patterns.
Type patterns test whether an expression matches a given type and bind it to
a variable. Record patterns test whether an expression matches a given
record type and deconstruct it into its components. Constant patterns test
whether an expression is equal to a given constant value.
For example, you can use pattern matching for switch expressions to
calculate the area of different shapes:
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double length, double width) implements Shape {}
Shape s = new Circle(2);
double area = switch (s) {
case null -> 0;
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double l, double w) -> l * w;
};
Using type patterns to match different types of objects:
Object o = "Hello";
String result = switch (o) {
case Integer i -> "Integer: " + i;
case String s -> "String: " + s;
case Double d -> "Double: " + d;
default -> "Unknown type";
};
Using record patterns to deconstruct record values:
record Point(int x, int y) {}
record Circle(Point center, int radius) {}
Object o = new Circle(new Point(1, 2), 3);
String result = switch (o) {
case Circle(Point(var x, var y), var r) -> "Circle with center (" + x + ", " + y + ") and radius " + r;
case Point(var x, var y) -> "Point (" + x + ", " + y + ")";
default -> "Unknown shape";
};
Using constant patterns to match constant values:
enum Color { RED, GREEN, BLUE }
Color c = Color.GREEN;
String result = switch (c) {
case RED -> "#FF0000";
case GREEN -> "#00FF00";
case BLUE -> "#0000FF";
};
6. Vector API
Vector API is an incubating feature that leverages vector instructions to
optimize computations on arrays of primitive data types. Vector
instructions are CPU instructions that can operate on multiple data
elements at once, such as adding two arrays of integers
element-wise.
Vector API provides an abstraction called Vector that represents a fixed
number of values of the same primitive type. Vector API also provides
methods to create, manipulate, and operate on vectors using arithmetic,
logical, and bitwise operations.
To use vector API, Java 19 introduces several classes such as
VectorSpecies, IntVector, FloatVector, etc.
that allow creating and using vectors in a platform-independent
way.
For example, you can use vector API to add two arrays of floats using
vector addition:
float[] a = {1.0f, 2.0f, 3.0f};
float[] b = {4.0f, 5.0f, 6.0f};
float[] c = new float[3];
// Get the preferred vector species for float
VectorSpecies<Float> species = FloatVector.SPECIES_PREFERRED;
// Loop over the arrays using a stride equal to the vector length
for (int i = 0; i < a.length; i += species.length()) {
// Load vectors from the arrays
FloatVector va = FloatVector.fromArray(species, a, i);
FloatVector vb = FloatVector.fromArray(species, b ,i);
// Perform vector addition
FloatVector vc = va.add(vb);
// Store the result into the array
vc.intoArray(c ,i);
}
Creating and using a vector of floats using VectorSpecies:
// Get the preferred vector species for float
VectorSpecies<Float> species = FloatVector.SPECIES_PREFERRED;
// Create a vector of floats from an array
float[] array = {1.0f, 2.0f, 3.0f};
FloatVector v = FloatVector.fromArray(species, array, 0);
// Perform some arithmetic operations on the vector
FloatVector v1 = v.add(1.0f); // Add a scalar to each element
FloatVector v2 = v.mul(v); // Multiply each element by itself
FloatVector v3 = v.neg(); // Negate each element
// Convert the vector back to an array
float[] result = v3.toArray();
Applying a function to each element of a vector using
VectorOperators:
// Get the preferred vector species for double
VectorSpecies<Double> species = DoubleVector.SPECIES_PREFERRED;
// Create a vector of doubles from an array
double[] array = {1.0, 2.0, 3.0};
DoubleVector v = DoubleVector.fromArray(species, array, 0);
// Apply a function to each element of the vector using VectorOperators
DoubleVector w = v.lanewise(VectorOperators.SIN); // Compute sine of each element
// Convert the vector back to an array
double[] result = w.toArray();
Performing bitwise operations on a vector using
BitwiseOperators:
// Get the preferred vector species for int
VectorSpecies<Integer> species = IntVector.SPECIES_PREFERRED;
// Create a vector of ints from an array
int[] array = {1, 2, 3};
IntVector v = IntVector.fromArray(species, array ,0);
// Perform some bitwise operations on the vector using BitwiseOperators
IntVector w1 = v.lanewise(BitwiseOperators.NOT); // Compute bitwise complement of each element
IntVector w2 = v.lanewise(BitwiseOperators.AND_NOT ,4); // Clear bit at position 2 for each element
// Convert the vectors back to arrays
int[] result1 = w1.toArray();
int[] result2 = w2.toArray();
7. Linux/RISC-V Port
Linux/RISC-V port is one of the features and enhancements in Java 19 that
enables running Java applications on Linux systems that use RISC-V
processors. RISC-V is a free and open-source instruction set architecture
(ISA) designed for various computing devices, from embedded systems to
warehouse-scale cloud computers.
The Linux/RISC-V port supports only the RV64GV configuration of RISC-V,
which is a general-purpose 64-bit ISA that includes vector instructions. The
port also supports the template interpreter, the C1 and C2 JIT compilers,
and all current mainline GCs.
The Linux/RISC-V port was integrated into the JDK main-line repository in
September 2022 and has been tested on several RISC-V hardware platforms such
as HiFive Unmatched2 and BeagleV.
The Linux/RISC-V port offers a new opportunity for Java developers to
explore the potential of RISC-V architecture and its benefits such as
performance, generality, and safety.
To build the Linux kernel for RISC-V, you need to set the ARCH and
CROSS_COMPILE variables to riscv64 and riscv64-linux-gnu-
respectively.
For example:
make ARCH=riscv64 CROSS_COMPILE=riscv64-linux-gnu- defconfig
make ARCH=riscv64 CROSS_COMPILE=riscv64-linux-gnu-
To boot the Linux kernel on QEMU with RISC-V emulation, you need to specify
the CPU model, machine type, BIOS image, memory size, and SMP options.
For example:
./qemu-system-riscv64 -cpu sifive-u54 -machine sifive_u -bios u540.fd -m 4096 -smp cpus=5,maxcpus=5
To enable vector instructions support in the Linux kernel for RISC-V, you
need to enable the CONFIG_RISCV_ISA_V option in the kernel configuration
file.
For example:
config RISCV_ISA_V
bool "Enable support for Vector ISA"
depends on RISCV_ISA_C && !RISCV_ISA_E && !RISCV_ISA_F && !RISCV_ISA_D
default y if EXPERT
help
This option enables support for Vector ISA extension.
If unsure, say N.
Linux/RISC-V Port Issues:
There are different ways to debug Linux/RISC-V port issues depending on the
type and level of the problem. Here are some possible methods:
-
Debuging issues related to RISC-V ISA or hardware implementation, you
can use a hardware debugger that supports RISC-V external debug
specification.
This specification defines a standard interface for accessing registers, memory, and other resources on a RISC-V platform.
You can also use a software emulator such as QEMU that can simulate RISC-V processors and devices.
-
To debug issues related to Linux kernel or user-space applications,
you can use a software debugger that supports RISC-V architecture such
as GDB.
Using tools such as strace, perf, or ftrace to trace system calls, performance events, or kernel functions respectively.
You can enable some kernel configuration options such as CONFIG_DEBUG_INFO or CONFIG_FTRACE to get more debugging information.
-
And to debug issues related to Java runtime or applications, you can
use tools such as jdb, jstack, jmap, or jconsole that are part of the
JDK.
These tools can help you inspect Java threads, heap memory, classes, or JVM performance respectively.
You may need to enable some JVM options such as -agentlib:jdwp or -XX:+HeapDumpOnOutOfMemoryError to enable remote debugging or heap dump generation respectively.
GDB debug Linux/RISC-V Port Issue:
Suppose you have a Linux kernel image (vmlinux) and a QEMU virtual
machine running Linux/RISC-V. You can start QEMU with
-s option to enable a GDB server on port 1234.
For example:
qemu-system-riscv64 -cpu sifive-u54 -machine sifive_u -bios u540.fd -m 4096 -smp cpus=5,maxcpus=5 -s
On another terminal, you can start GDB with the kernel image as
argument.
For example: gdb vmlinux
In GDB, you can connect to the QEMU GDB server by using target remote
command.
For example:
(gdb) target remote :1234
Remote debugging using :1234
0x0000000080000000 in ?? ()
Now you can use GDB commands to inspect and control the execution of the
kernel.
For example, you can set breakpoints, examine registers and memory, step
through instructions or functions, etc. You can also use lx-symbols command
to load symbols from kernel modules.
For example:
(gdb) b start_kernel
Breakpoint 1 at 0xffffffff8011a9c8: file init/main.c, line 549.
(gdb) c
Continuing.
Breakpoint 1, start_kernel () at init/main.c:549
549 {
(gdb) info registers x0 x1 x2 x3 x4 x5 x6 x7 pc sp ra sp gp tp t0 t1 t2 s0 s1 a0 a1 a2 a3 a4 a5 a6 a7 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 t3 t4 t5 t6 fcsr ft0 ft1 ft2 ft3 ft4 ft5 ft6 ft7 fs0 fs1 fa0 fa1 fa2 fa3 fa4 fa5 fa6 fa7 fs2 fs3 fs4 fs5 fs6 fs7 fs8 fs9 fs10 fs11 ft8 ft9 ft10 ft11 fflags frm fcsr_cause fcsr_flags fcsr_rm badaddr cause status epc hartid priv mstatus mepc mtval mcause mie mtvec mscratch misa mcycle minstret mhpmcounter31 mhpmcounter30 mhpmcounter29 mhpmcounter28 mhpmcounter27 mhpmcounter26 mhpmcounter25 mhpmcounter24 mhpmcounter23 mhpmcounter22 mhpmcounter21 mhpmcounter20 mhpmcounter19 mhpmcounter18 mhpmcounter17 mhpmcounter16 htimedelta htimedeltah hcycle hinstret hgithreshold hgatp htinst htval hip hip_sticky sip sip_sticky sie stvec stval satp scounteren sepc sscause stvalh sip_h sip_h_sticky sie_h scounteren_h sepc_h sscause_h stvec_h satp_h scounteren_hs sepc_hs sscause_hs stvec_hs satp_hs scounteren_vs sepc_vs sscause_vs stvec_vs satp_vs mvendorid marchid mimpid mhartid misa medeleg mideleg mie mtie mtdeleg mtideleg mip mtip mtvt mscratch mcycle minstret mcycleh minstreth mcounthi31 mcounthi30 mcounthi29 mcounthi28 mcounthi27 mcounthi26 mcounthi25 mcounthi24 mcounthi23 mcounthi22 mcounthi21 mcounthi20 mcounthi19 mcounthi18 ... (more)
x0 0x0 0
x1 0xffffffff801bda00 18446744071589052416
x2 0xffffffff802f8008 18446744071592341512
x3 0xffffffff802f8008 18446744071592341512
x4 0xffffffff802f8008 18446744071592341512
x5 0xffffffff802f8008 18446744071592341512
x6 0xffffffff802f8008 18446744071592341512
x7 0xffffffff802
Other features and enhancements
Horse breeding improvements in Minecraft Java Edition:
Horse breeding improvements in Minecraft Java Edition is one of the
features and enhancements in Java 19 that makes it easier and more
rewarding for players to breed horses, donkeys, and llamas.
In previous versions, the attributes of a baby horse (such as speed, jump
height, and health) were biased toward the average possible value,
regardless of the parents’ attributes.
This made it difficult to obtain better horses through selective
breeding. In Java 19, however, the attributes of a baby horse are now a
variation of the average of the parents’ attributes.
This means that players can now find or breed horses with high attributes
and pass them on to their offspring. This feature also applies to donkeys
and llamas.
Horse breeding improvements in Minecraft Java Edition also include some
changes to how horses interact with other items and blocks.
For example, jukeboxes now produce a note particle above when playing a
music disc. Horses can also interact with droppers and hoppers, which can
be used to automate feeding or equipping them. Additionally, horses now
emit a redstone signal of 15 while playing a disc, which can be used for
various redstone contraptions.
Horse breeding improvements in Minecraft Java Edition is a feature that
enhances the gameplay experience for players who love exploring their
world on horseback.
It also adds more depth and variety to horse breeding mechanics and
encourages players to experiment with different combinations of
horses.
Horse breeding improvements in Minecraft Java Edition is a feature that
fans of horses will surely appreciate.
III. Conclusion
In this blog post, we have explored some of the main features and
enhancements in Java 19, a non-LTS release that offers a glimpse into the
future of Java development. We have seen how Java 19 introduces structured
concurrency, which simplifies multithreaded programming by treating
multiple tasks as a single unit of work.
We have also learned about record patterns, which extend pattern matching
for instanceof to deconstruct record values and type patterns.
Furthermore, we have discussed the foreign function and memory API, which
enables interoperability with native code and data without the drawbacks
of JNI.
Moreover, we have looked at virtual threads, which enable lightweight
concurrency model by creating millions of concurrent tasks without
exhausting resources.
Finally, we have briefly mentioned some other features and enhancements
in Java 19 such as pattern matching for switch expressions, vector API,
Linux/RISC-V port, and horse breeding improvements in Minecraft Java
Edition.
Java 19 is a feature-rich release that demonstrates the innovation and
evolution of Java as a programming language and platform. It provides
developers with new capabilities and opportunities to create
high-performance, reliable, expressive, and composable
applications.
It also showcases some of the experimental features that may become part
of future LTS releases such as Java 20 or beyond. Whether you are a
seasoned Java developer or a newcomer to the language, you can benefit
from trying out Java 19 and exploring its potential for your
projects.
Can download Java 19 for your platform, visit
oracle.com/java/technologies/downloads/.
Learn more about Java 19 features and enhancements in detail, visit
https://docs.oracle.com/en/java/javase/19/index.html.
References:
- JEP 376: Structured Concurrency
- JEP 405: Record Patterns & Array Patterns (Preview)
- JEP 412: Foreign Function & Memory API (Incubator)
- JEP 414: Vector API (Second Incubator)
- JEP 406: Pattern Matching for switch (Preview)
- JEP 414: Vector API (Second Incubator)
- JEP 419: Linux/RISC-V Port
- Minecraft: Java Edition 1.19.4 update now available with improved horse breeding | Windows Central