The Java Development Kit (JDK) 23 release (in September 2024) marks a significant milestone in the history of the Java programming language, which is a robust, versatile, and crucial tool in the software development environment.

 

Since 1995, when Java was first presented by Sun Microsystems with the slogan “Write One, Run Anywhere”, it has been providing the possibility to execute code on any device. Over the years, Java has been evolving through various versions, presenting new features and improvements that maintain backward compatibility.


Tradition continues with this new version, which has the objective to improve developers’ productivity and application performance, while ensuring Java remains relevant and powerful.

 

 

What’s coming in Java 23

This release includes a total of 12 Java Enhancement Proposals (JEPs), namely:


New features
  • Primitive Types in Patterns, instanceof, and switch (preview feature)
  • Markdown Documentation Comments
  • Module Import Declarations (preview feature)

 

Improved features
  • Class-File API (second preview feature)
  • Vector API (eighth incubator)
  • Stream Gatherers (second preview feature)
  • ZGC: Generational Mode by Default
  • Implicitly Declared Classes and Instance Main Methods (third preview feature)
  • Structured Concurrency (third preview feature)
  • Scoped Values (third preview feature)
  • Flexible Constructor Bodies (second preview feature)

 

Deprecated features
  • Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal

 

Let’s take a look into some of the most relevant updates.

 

 

Some language improvements

Primitive Types in Patterns, instanceof, and switch

This new feature has the following objectives:

  • Enable uniform data exploration by allowing type patterns for all types, whether primitive or reference.
  • Aligns type patterns with instanceof and align instanceof with safe casting.
  • Allows pattern matching to use primitive type patterns in both nested and top-level contexts.
  • Provide constructs that remove the risk of information loss due to unsafe casts.
  • Allow switch to process values of any primitive type. This improvement makes the Java language more uniform and expressive.

 

Example:

//improve the switch expression:

switch (x.getStatus()) {

    case 0 -> "ok";

    case 1 -> "warning";

    case 2 -> "error";

    default -> "unknown status: " + x.getStatus();

}

//exposing the matched value:

switch (x.getStatus()) {

    case 0 -> "okay";

    case 1 -> "warning";

    case 2 -> "error";

    case int i -> "unknown status: " + i;

}

//allowing guards to inspect the corresponding value:

switch (x.getYearlyFlights()) {

    case 0 -> ...;

    case 1 -> ...;

    case 2 -> issueDiscount();

    case int i when i >= 100 -> issueGoldCard();

    case int i -> ... appropriate action when i > 2 && i < 100 ...

}

 

More information about this feature here.

 

 

Flexible Constructor Bodies

In the Java programming language, constructors allow statements to appear before an explicit constructor invocation, such as super(..) or this(..). While these statements cannot reference the instance under construction, they can initialise its fields.

 

Initialising fields before invoking another constructor enhances class reliability, particularly when methods are overridden. This feature is currently in preview.

 

Example:

//Flexible Constructor Bodies

class Parent {

    int x;

 

    public Parent(int x) {

        this.x = x;

    }

}

 

class Child extends Parent {

    int y;

 

    public Child(int x, int y) {

        // Statements before calling the parent constructor

        int temp = x * 2; // Cannot reference instance fields

        super(temp); // Explicit constructor invocation

        this.y = y; // Instance fields can be initialized after the invocation

    }

}

 

public class Main {

    public static void main(String[] args) {

        Child child = new Child(5, 10);

        System.out.println("x: " + child.x + ", y: " + child.y); // Outputs: x: 10, y: 10

    }

}

 

More information about this feature here.

 

 

Structured Concurrency

The structured concurrency feature simplifies multithreaded programming by treating multiple tasks running in different threads as a single unit of work, thereby streamlining error handling and cancellation.

 

Example:

 

// Structured Concurrency

public class StructuredConcurrencyExample {

    public static void main(String[] args) throws InterruptedException, ExecutionException {

        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

            Future<String> future1 = scope.fork(() -> fetchDataFromService1());

            Future<String> future2 = scope.fork(() -> fetchDataFromService2());

 

            scope.join(); // Join both forks

            scope.throwIfFailed(); // Propagate exceptions

 

            String result1 = future1.resultNow();

            String result2 = future2.resultNow();

            System.out.println(result1 + " " + result2);

        }

    }

 

    private static String fetchDataFromService1() {

        // Simulate fetching data

        return "Data1";

    }

 

    private static String fetchDataFromService2() {

        // Simulate fetching data

        return "Data2";

    }

}


More information about this feature here.

 

 

Scoped Values

Scoped values enable methods to share immutable data with their callees within a thread and with child threads. They are simpler to reason about compared to thread-local variables and offer lower space and time costs.


When used with virtual threads and structured concurrency, scoped values are particularly efficient. This feature is currently a preview API.

 

Example:

 

import java.util.concurrent.Executors;

import java.util.concurrent.ExecutorService;

import jdk.incubator.concurrent.ScopedValue;

 

public class Main {

    public static void main(String[] args) {

        ScopedValue<String> scopedValue = ScopedValue.create("Hello, World!");

 

        ExecutorService executor = Executors.newSingleThreadExecutor();

        executor.submit(() -> {

            System.out.println("Scoped Value: " + scopedValue.get());

        });

 

        executor.shutdown();

    }

}


More information about this feature here.

 

 

Stream Gatherers

This feature aims to enhance the Java Stream API by introducing two new terminal operations: ‘Stream.gather(toList(), toSet())’ and ‘Stream.gather(toMap(), toSet())’. These operations enable collecting elements of a stream into multiple collections simultaneously, which simplifies the code and improves readability when a stream needs to be collected into several containers.


The feature, initially proposed in Java 22, has been refined based on feedback and is now being presented for a second preview to ensure its functionality and performance meet community needs before being finalised.

 

Example:

import java.util.List;

import java.util.Set;

import java.util.stream.Collectors;

import java.util.stream.Stream;

 

public class StreamGatherersExample {

    public static void main(String[] args) {

        // Example stream

        Stream<String> stream = Stream.of("apple", "banana", "cherry", "apple", "date", "banana");

 

        // Using Stream.gather to collect elements into a List and a Set simultaneously

        var result = stream.collect(Collectors.gather(

            Collectors.toList(),

            Collectors.toSet()

        ));

 

        // Extracting the results

        List<String> list = result.getFirst();

        Set<String> set = result.getSecond();

 

        // Printing the results

        System.out.println("List: " + list);

        System.out.println("Set: " + set);

    }

}

 

More information about this feature here.

 

 

Best practices for Java 23 development

Here are my tips to Java Developers using the new Java 23:

 

Adopt efficient coding techniques:

  1. Use pattern matching for switch statements: it simplifies code logic and reduces nested structures, leading to more concise and readable code.
  2. Optimise records: customise serialisation behavior for records to enhance data encapsulation and streamline object-oriented programming practices. This promotes code efficiency and maintainability.
  3. Leverage enhanced Vector API: take advantage of improvements in the Vector API for numerical computations to boost performance, especially in tasks involving matrix operations and scientific computing.


Error handling guidelines:

  1. Adopt modern serialisation frameworks: transition from legacy serialisation APIs to modern frameworks like Protocol Buffers or JSON to ensure future compatibility and improved data interchange capabilities.
  2. Implement secure coding practices: embrace secure coding principles and tools provided in Java 23 to mitigate vulnerabilities such as injection attacks and data exposure, enhancing application security.
  3. Fine-tune Garbage Collection: optimise garbage collection algorithms to minimise pauses and optimise memory usage, contributing to smoother application performance and reduced resource overhead.

 

Performance optimisation strategies:

  1. Use Just-In-Time (JIT) compilation enhancements: benefit from JIT compiler optimisations to dynamically compile frequently executed code paths, resulting in faster execution speeds and improved response times.
  2. Optimise HTTP Client performance: use streamlined HTTP Client optimisations for enhanced communication with web services, including improved connection pooling and request/response handling.
  3. Reduce memory footprint: implement memory management techniques to reduce the memory footprint of Java applications, ensuring more efficient memory allocation and usage.

 

 

Conclusion

Java 23 introduces a set of advanced features and enhancements designed to improve the efficiency and performance of software development, while reinforcing Java’s compatibility and interoperability with other programming languages and platforms. This release includes improvements to the Java Virtual Machine (JVM), new language features, and updates to existing APIs and libraries, all aimed at providing developers with more tools to write high-performance, maintainable code. Notable features include enhancements to the pattern matching for switch expressions, improvements to the foreign function and memory API, and new garbage collection options, which collectively contribute to the robustness and adaptability of Java applications in diverse environments.

 

As a senior Java software developer, I find Java 23’s new features both exciting and essential for modern development. The enhancements to pattern matching simplify complex conditional logic, making code more readable and maintainable. The foreign function and memory API improvements are particularly important, as they open up new possibilities for integrating Java with native code and other languages, enhancing Java’s versatility in multi-language ecosystems.

 

Overall, Java 23 continues the tradition of evolving the language and platform to meet contemporary development needs, ensuring that Java remains a top choice for developers worldwide.

Share this article