Core Java

What is new in Java 19

Java 19 was released in September 2022 aiming to bring new features and enhancements for the Java community along with some preview features. Let us delve into these features.

1. JEP 405: Record Patterns (Preview)

JEP 405, Record Patterns (Preview), has progressed from Proposed to Targeted for JDK 19. As part of Project Amber, this JEP suggests enhancing the language with record patterns to destructure record values. Record patterns can be utilized alongside type patterns, aiming to facilitate a robust, declarative, and composable approach to data navigation and processing. It’s important to note that this feature is still in preview.

In JDK 16, JEP 394, Pattern Matching for instanceof, extended the instanceof operator to accommodate type patterns for pattern matching. Consider the following example:

Code Snippet

public void print(Object o) {
    if (o instanceof Double) {
        Double d = (Double) o;
        System.out.println("double = " + d);
    }
}

This code can be expressed more succinctly using pattern matching:

Code Snippet

public void print(Object o) {
    if (o instanceof Double d) {
        System.out.println("double = " + d);
    }
}

In the revised code, o matches the type pattern Double d if, at runtime, its value is an instance of Double. This reduces the need for explicit typecasting, resulting in more concise and manageable code.

JEP 395, Records, introduced Record classes, offering a transparent way to represent data and simplifying the creation of immutable objects. For instance:

Code Snippet

record Point(int x, int y) { }

Developers are no longer required to explicitly write constructors, accessor methods, or other boilerplate code, making the code cleaner and less verbose. When dealing with an instance of a record class within a code block, developers typically extract data using its accessor methods. For example:

Code Snippet

public void printSum(Object o) {
    if (o instanceof Point p) {
        int x = p.x();
        int y = p.y();
        System.out.println(x + y);
    }
}

In this code, the pattern variable p is used to invoke accessor methods x() and y() to retrieve the values of x and y. With the introduction of the record pattern, the variable p is no longer necessary. The code can be simplified as follows:

Code Snippet

public void printSum(Object o) {
   if (o instanceof Point(int x int y)) {
       System.out.println(x + y);
   }
}

This enhancement enables developers to destructure more complex object graphs. For instance, consider the following code snippet:

Code Snippet

enum Color {RED, GREEN, BLUE}
record ColoredPoint(Point p, Color color) {}
record Point(int x, int y) {}
record Square(ColoredPoint upperLeft, ColoredPoint lowerRight) {}

If developers need to print the upper-left ColoredPoint in a pattern-matching scenario using the Record pattern, it can be deconstructed as follows:

Code Snippet

public void printUpperLeftColoredPoint(Square s) {
    if (s instanceof Square(ColoredPoint(Point(var x, var y), var color), var lowerRight))){
    }
}

2. JEP 422: Linux/RISC-V Port

Beyond programmatic enhancements, JEP 422 stands out as the port of OpenJDK to Linux on the RISC-V chip architecture. While there are currently limited commercially available RISC-V devices, the potential of this architecture makes Java support a valuable addition.

3. JEP 424: Foreign Function & Memory API (Preview)

Following its review, JEP 454, the Foreign Function & Memory API, has advanced from Targeted to Integrated for JDK 22. This JEP seeks to finalize the feature after completing two rounds of incubation and three rounds of preview. The evolutionary process includes JEP 412, JEP 419, JEP 424, JEP 434, and JEP 442, each contributing to the maturation of the API across multiple JDK releases.

Enhancements in the latest release encompass a new Enable-Native-Access manifest attribute, enabling executable JARs to invoke restricted methods without the need for the --enable-native-access flag. Additionally, clients can now construct C function descriptors programmatically, addressing platform-specific constants. Improved support for variable-length arrays in native memory and the inclusion of support for multiple charsets in native strings further refine the API.

The Foreign Function & Memory (FFM) API, housed in the java.lang.foreign package, comprises key classes:

  • Linker: An interface facilitating the linking of Java code with foreign functions in libraries adhering to a specific Application Binary Interface (ABI). It supports both downcalls to foreign functions and upcalls from foreign functions to Java code.
  • SymbolLookup: An interface for retrieving the address of a symbol (function or global variable) in a specific library. It supports various types of lookups, including library, loader, and default lookups provided by a Linker.
  • MemorySegment: An interface providing access to a contiguous region of memory, either on the Java heap (heap segment) or outside it (native segment). It offers various access operations while ensuring spatial and temporal bounds.
  • MethodHandle: A class serving as a strongly typed, directly executable reference to an underlying method, constructor, or field. It provides two special invoker methods, invokeExact and invoke, and is immutable with no visible state.
  • FunctionDescriptor: Models the signature of a foreign function, comprising argument layouts and a return layout. It is used to create downcall method handles and upcall stubs.
  • Arena: An interface controlling the lifecycle of native memory segments, offering methods for allocation and deallocation within specified scopes. It comes in different types, each with unique characteristics regarding lifetime, thread accessibility, and manual control.

This API aims to enhance interaction between Java and native code, providing a more efficient and secure way to access native libraries and manage native memory. Notably, it consists of two main components: the Foreign Function Interface (FFI) and the Memory Access API.

In the past, dealing with off-heap memory in Java posed significant challenges. Java developers were previously limited to using ByteBuffer objects for off-heap memory operations. However, the FFM API introduces MemorySegment objects, offering greater control over the allocation and deallocation of off-heap memory. Additionally, the MemorySegment::asByteBuffer and MemorySegment::ofBuffer methods serve to strengthen the connection between traditional byte buffers and the new memory segment objects.

The FFM API aligns seamlessly with the java.nio.channels API, presenting a deterministic approach to deallocating off-heap byte buffers. This eliminates the reliance on non-standard, non-deterministic techniques like invoking sun.misc.Unsafe::invokeCleaner, providing a more dependable and standardized method for memory management.

The advancements in the FFM API signify a stride towards making the Java platform safer, more efficient, and interoperable. The focus extends beyond facilitating Java-native interactions to ensuring their security and reliability.

4. JEP 425: Virtual Threads (Preview)

JEP 425, Virtual Threads (Preview), has progressed from Proposed to Targeted status for JDK 19. As part of Project Project Loom, this JEP introduces virtual threads, a lightweight alternative designed to significantly streamline the development, maintenance, and observation of high-throughput concurrent applications on the Java platform. Notably, this feature is currently in preview.

Java pioneered the incorporation of threads into its core language as the fundamental unit for concurrent programming. Classic Java threads, represented by instances of java.lang.Thread, are direct counterparts to operating system (OS) threads, also known as platform threads. In contrast, virtual threads are lightweight implementations of the Thread class provided by the JDK itself, not the OS. While virtual threads leverage the platform thread to execute their code, they don’t monopolize the OS thread for the entirety of their code’s lifespan. This innovative approach allows multiple virtual threads to execute their Java code on the same OS thread.

The advantages of virtual threads are manifold. Primarily, they simplify and economize the process of creating threads, contributing to enhanced application throughput. Contemporary server applications often manage concurrent user requests by assigning each to an independent unit of execution, represented by a platform thread. This thread-per-request programming style is straightforward, easy to program, debug, and profile. However, the limitation lies in the finite number of platform threads, as the JDK implements threads as wrappers around OS threads. With the introduction of virtual threads, the scalability bottleneck associated with platform threads is alleviated, thanks to the ease of creating a large number of virtual threads.

Moreover, when code executed on a virtual thread encounters a blocking I/O operation, the virtual thread is automatically suspended until it can resume later. During this suspension, other virtual threads can take control of the OS thread and continue executing their code. This stands in contrast to OS threads, which would be blocked in a similar scenario, highlighting the limitations of the latter.

Virtual threads seamlessly support features like thread-local variables, synchronization blocks, thread interruptions, and more. From a Java developer’s perspective, virtual threads are essentially threads that are both cost-effective and abundant. Importantly, the programming approach remains unchanged, and existing Java code originally targeting classic threads can seamlessly run on virtual threads without any modifications.

The Thread class introduces a novel method, startVirtualThread(Runnable), designed to generate virtual threads. Consider the example below:

Code Snippet

public class MyExample {
    public static void main(String[] args) throws InterruptedException {
        var jcgvThread = Thread.startVirtualThread(() -> {
            System.out.println("Hello from the JavaCodeGeeks virtual thread");
        });
        jcgvThread.join();
    }
}

Since this is a preview feature in Java 19 you will have to provide the --enable-preview to compile and run this code. Although Thread.startVirtualThread(Runnable) offers a convenient approach to generating a virtual thread, additional APIs like Thread.Builder, Thread.ofVirtual(), and Thread.ofPlatform() have been introduced for creating both virtual and platform threads.

It’s important to note that a virtual thread cannot be instantiated using the public constructor. It operates as a daemon thread with NORM_PRIORITY. Consequently, the methods Thread.setPriority(int) and Thread.setDaemon(boolean) cannot alter the priority of a virtual thread or transform it into a non-daemon thread.

Pooling virtual threads is discouraged, as each is designed to execute a single task throughout its lifespan. The recommended approach is the unrestrained creation of virtual threads. To facilitate this, an unbounded executor has been introduced, accessible through the new factory method Executors.newVirtualThreadPerTaskExecutor(). These methods facilitate the transition and compatibility with existing code employing thread pools and ExecutorService. Consider the example below:

Code Snippet

public class MyExample1 {
    public static void main(String[] args) {        
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            IntStream.range(0, 10).forEach(i -> {
                executor.submit(() -> {
                    Thread.sleep(Duration.ofSeconds(1));
                    return i;
                });
            });
        }
    }
}

The default scheduler for virtual threads is the work-stealing scheduler introduced within ForkJoinPool.

5. JEP 426: Vector API (Fourth Incubator)

This JEP enhances the performance of the Vector API and incorporates other improvements based on feedback. More details about JEP 426 can be found on this page.

6. JEP 427: Pattern Matching for Switch (Third Preview)

Pattern matching is a feature of Java 17 and it simplifies the syntax for common coding patterns and enhances the expressiveness of code. It allows for more concise and readable code when working with data types. Here’s a brief example of how pattern matching might be used with switch in Java 17:

Code Snippet

public class MyExample {
    public static void main(String[] args) {
        Object obj = "Hello";
        switch (obj) {
            case String s -> System.out.println("It's a String: " + s);
            case Integer i -> System.out.println("It's an Integer: " + i);
            case Double d -> System.out.println("It's a Double: " + d);
            default -> System.out.println("It's something else");
        }
    }
}

In this example, the switch statement uses the arrow (->) syntax for pattern matching. Each case checks if the object matches a specific type and extracts the value if it does. The default case handles other cases.

In JEP 427 Guarded patterns are replaced with the when clause in the switch block.

Code Snippet

static void understandJEP427(Object o) {
  switch (o) {
	  case String s
			  when s.length() > 50 ->
				System.out.println("String's length longer than 50");
	  case String s ->
				System.out.println("String's length is " + s.length());
	  default -> {}
  }
}

7. JEP 428: Structured Concurrency (Incubator)

Structured concurrency, currently in its incubation phase, aims to enhance multithreaded programming by providing a well-defined concurrency API. This approach considers multiple tasks executing in distinct threads as a cohesive unit of work, leading to more streamlined error handling and cancellation. The result is an improvement in both reliability and observability. Project Loom, known for introducing a novel lightweight concurrency model, is the source of this feature.

Structured concurrency and Java’s existing concurrency mechanisms share the common goal of managing concurrent tasks, yet they employ different approaches, each with its own set of advantages and disadvantages.

  • Hierarchical Organization: Structured concurrency imposes a parent-child relationship between tasks, necessitating that parent tasks await the completion of child tasks before concluding.
  • Reduced resource leaks: By ensuring that parent tasks wait for child tasks, structured concurrency minimizes resource leaks, ensuring all resources are cleaned up when the entire task hierarchy finishes.
  • Prevention of orphaned tasks: Structured concurrency averts orphaned tasks by mandating that child tasks be completed before their parents, thereby preventing tasks from being left unattended.
  • Easier error handling: Structured concurrency simplifies error handling and propagation, as errors can be addressed and propagated within the task hierarchy more seamlessly.

JEP 428 allows developers to modify the concurrency code using the StructuredTaskScope class. This class treats a family of subtasks as a unit. Subtasks will be spawned individually on separate threads through individual forks. However, they will later be joined as a cohesive unit and potentially canceled as a unified entity. The parent task will aggregate and manage their exceptions or successful results.

Code Snippet

Response handle() throws ExecutionException, InterruptedException {
   try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
       Future customer = scope.fork(() -> findCustomer());
       Future product = scope.fork(() -> fetchProduct());

       scope.join();          // Join both forks
       scope.throwIfFailed(); // ... and propagate errors

       // Here, both forks have succeeded, so compose their results
       return new Response(customer.resultNow(), product.resultNow());
   }
}

The handle() method serves as a task within a server application, managing an incoming request by initiating two subtasks. Similar to ExecutorService.submit(), StructuredTaskScope.fork() accepts a Callable and produces a Future. Notably, in contrast to ExecutorService, the resulting Future is not synchronized through Future.get(). This API operates atop JEP 425, Virtual Threads (Preview), slated for incorporation in JDK 19. Since this is a preview feature in Java 19 you will have to provide the --enable-preview to compile and run this code.

Structured concurrency offers numerous advantages. It establishes a parent-child relationship between the invoking method and its associated subtasks.

8. Conclusion

In conclusion, the Java Enhancement Proposals (JEPs) discussed cover a diverse array of features and improvements, showcasing the ongoing evolution of the Java programming language. The introduction of JEP 405, focusing on Record Patterns in a preview state, demonstrates a commitment to enhancing language expressiveness and conciseness. Meanwhile, the Linux/RISC-V Port in JEP 422 extends the platform reach of Java, broadening its compatibility. JEP 424 introduces the Foreign Function & Memory API in a preview state, providing more flexibility for interacting with native code. JEP 425’s Virtual Threads (Preview) presents a novel lightweight concurrency model, aiming to simplify multithreaded programming. The Vector API (Fourth Incubator) in JEP 426 emphasizes performance improvements through vectorization, catering to modern hardware architectures. Pattern Matching for Switch (Third Preview) in JEP 427 enhances readability and conciseness in code. Lastly, JEP 428 brings Structured Concurrency into incubation, offering a promising approach to managing concurrent tasks with a focus on improved error handling and resource management. These initiatives collectively signify Java’s commitment to adaptability, performance, and developer-friendly features.

Yatin

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button