What’s New in Java 19: A Comprehensive Overview of the Latest Features

{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:

  1. Structured concurrency: an incubating feature that simplifies multithreaded programming by treating multiple tasks running in different threads as a single unit of work.

  2. Record patterns: a preview feature that extends pattern matching for instanceof by allowing deconstruction of record values and nesting of type patterns.

  3. 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.

  4. Virtual threads: a preview feature that enables lightweight concurrency model by allowing creation of millions of concurrent tasks without exhausting resources.

  5. Pattern matching for switch expressions: a standard feature that enhances switch expressions with pattern matching capabilities.

  6. Vector API: an incubating feature that leverages vector instructions to optimize computations on arrays of primitive data types.

  7. 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.


(Image credit: Xbox Game Studios | Twitter)


Image via Mojang



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.






Previous Post Next Post