Latest Features from Java 11 to Java 21

Java has continued to evolve significantly from Java 11 to Java 21, introducing a range of new features and improvements. Here’s a summary of key features from each version:

6 month release

Starting with Java 9, Oracle adpoted regular release for Java to become more agile and to collect more feedback from the community.

Verbosity improvement

Java is verbose, that what it make it a the best programming language for beginners, and it has another side: it provides explicit context and programmers. Which reduces mental overhead and simplifies reading the code.

There was many improvement to reduce Verbosity such as :

Records

Provide a concise and easy way to create simple, immutable data carrier classes. Before records, creating such classes involved writing a lot of boilerplate code: constructors, getters, equals(), hashCode(), and toString() methods. Records simplify this process significantly.

Key Characteristics of Records:

  1. Immutable by Default: The fields of a record are final and must be initialized in the constructor.
  2. Concise Syntax: Records reduce boilerplate code by automatically generating constructors, getters, equals(), hashCode(), and toString() methods based on the fields.
  3. Local Declarations: Records can be declared locally, within methods.
  4. Canonical Constructor: Records automatically have a canonical constructor (one with all the fields as parameters), but you can also define custom constructors.
public record Person(String name, int age) {
}

This one line of code does everything for you. It creates a class with two fields (name and age), a constructor to initialize these fields, and appropriate implementations of equals(), hashCode(), and toString() methods.

Accessing Fields and Using the Record:

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);

        // Accessing fields
        System.out.println("Name: " + person.name());
        System.out.println("Age: " + person.age());

        // Using toString() method
        System.out.println(person);
    }
}

Records in Java simplify the creation of immutable data classes and are particularly useful in situations where you need to quickly define classes to hold data without much additional behavior or customization. This feature enhances the readability and maintainability of Java code, especially for data-centric applications.

Pattern Matching


Pattern Matching for instanceof in Java, enhances the Java language by reducing boilerplate code and improving readability.

Traditional Approach Without Pattern Matching:

Before the introduction of pattern matching, you typically checked the type of an object using instanceof, and then you had to explicitly cast the object to the target type to access its methods or fields. For example:

Object obj = "Hello, world!";

if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

In this traditional approach, you need two steps:

  1. Check if obj is an instance of String.
  2. Explicitly cast obj to String to access the length() method.

Using Pattern Matching for instanceof:

With pattern matching, Java simplifies this process. The instanceof operator can be used to perform both the type check and the casting in one step:

Object obj = "Hello, world!";

if (obj instanceof String s) {
    // 's' is now directly usable as a String within this block
    System.out.println(s.length());
}

In this updated approach:

  1. The instanceof check and the casting to String are combined.
  2. If obj is an instance of String, it is automatically cast to String and assigned to the variable s.
  3. The variable s is then directly usable within the scope of the if block.

Project LOOM

the primary goal of Project Loom is to support a high-throughput, lightweight concurrency model in Java. introduced changes on the JVM as well as the Java library.

Loom is to make concurrency easier for developers by introducing lightweight, user-mode threads (virtual threads) and enabling better resource management. It aims to address challenges and limitations associated with traditional concurrency models that rely on OS-level threads.

freestar

However, since Java uses the OS kernel threads for the implementation, it fails to meet today’s requirement of concurrency. There are two major problems in particular:

  1. Threads cannot match the scale of the domain’s unit of concurrency. For example, applications usually allow up to millions of transactions, users or sessions. However, the number of threads supported by the kernel is much less. Thus, a Thread for every user, transaction, or session is often not feasible.
  2. Most concurrent applications need some synchronization between threads for every request. Due to this, an expensive context switch happens between OS threads.

Virtual Threads

These are lightweight threads that are managed by the Java Virtual Machine (JVM), not the operating system. Unlike traditional threads (platform threads), virtual threads are cheap to create and destroy, consume less memory, and can number in the millions. They are particularly useful for IO-bound tasks, where threads spend a lot of time waiting.

Key Characteristics of Virtual Threads:

  1. Lightweight: They consume significantly less memory and resources compared to platform threads.
  2. Easy to Create in Large Numbers: Because of their lightweight nature, you can create millions of virtual threads, which is not feasible with platform threads.
  3. Simplifies Concurrent Programming: Virtual threads simplify the concurrent programming model, especially for IO-bound operations, as you no longer need to deal with the complexity of thread pools or managing a limited number of threads.

Example of Virtual Threads:

Here’s a basic example demonstrating how you might use virtual threads in Java. Let’s say you have a server application that handles multiple client connections. With virtual threads, each client connection can be handled by its own thread without worrying about overwhelming the system with too many OS threads.

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class VirtualThreadExample {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        while (true) {
            Socket clientSocket = serverSocket.accept();

            // Creating a virtual thread for each client connection
            Thread.startVirtualThread(() -> handleClient(clientSocket));
        }
    }

    private static void handleClient(Socket clientSocket) {
        // Logic to interact with the client
        // This could involve IO-bound operations like reading from or writing to the socket
        System.out.println("Handling client: " + clientSocket);
    }
}

In this example:

  • A server socket listens for client connections.
  • For each incoming connection, a new virtual thread is created using Thread.startVirtualThread().
  • Each client connection is handled independently on its own virtual thread.

Benefits of Using Virtual Threads:

  1. Simplified Concurrency Model: No need for complex concurrency constructs like Executors or thread pools.
  2. Efficient Handling of IO Operations: Virtual threads are particularly beneficial for IO-bound tasks where threads often spend a lot of time waiting.
  3. Improved Scalability: Applications can handle a large number of concurrent tasks without the overhead associated with a similar number of platform threads.

Stopping a Virtual Thread:

  • Natural Completion: The most common and recommended way for a thread (virtual or traditional) to stop is by naturally completing its execution. Once the run() method (or lambda expression) finishes, the thread will terminate.
  • Interruption: If you need to stop a virtual thread prematurely, you can interrupt it, just like a regular thread. However, this requires the thread’s code to handle interruptions properly.

In the example of handling client connections with a virtual thread, each virtual thread will stop itself after it has completed handling its client connection :

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class VirtualThreadExample {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        while (true) {
            Socket clientSocket = serverSocket.accept();

            // Creating a virtual thread for each client connection
            Thread virtualThread = Thread.startVirtualThread(() -> handleClient(clientSocket));

            // Example of interrupting a thread (in real-world, condition for interrupt would be more sophisticated)
            if (shouldInterruptThread()) {
                virtualThread.interrupt();
            }
        }
    }

    private static boolean shouldInterruptThread() {
        // Implement logic to decide whether to interrupt the thread
        // For example, based on some external condition or signal
        return false; // Placeholder
    }

    private static void handleClient(Socket clientSocket) {
        try {
            // Logic to interact with the client
            System.out.println("Handling client: " + clientSocket);

            // Check for interruption
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Thread was interrupted, stopping handling the client.");
                return;
            }

            // Continue handling the client...
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Project Valhala

The project valhala brings significant enhancements to the Java programming language and the Java Virtual Machine (JVM). It aims to introduce value types and improve the way Java handles memory, with the goal of increasing performance and reducing memory overhead, particularly for complex, data-heavy applications.

Value Types (Inline Classes without references): behave like primitives

  • Traditional Java objects are reference types, which means variables hold references to objects that are stored in the heap. Value types, on the other hand, are intended to be a new kind of type that is not a reference. They are intended to be more memory-efficient and faster in certain scenarios, particularly where large arrays of objects are used.
  • Value Types, as proposed in Project Valhalla for Java, are a fundamental shift in how Java handles data types. They are intended to provide a way to define types that behave like primitives in terms of efficiency and memory layout but can be used like objects in terms of encapsulation and methods. This concept is quite different from the traditional Java objects, which are reference types.

Understanding Value Types:

  1. Efficient Memory Usage: Unlike objects, value types are intended to be allocated on the stack (when used in a method scope) or stored directly in fields (when used as part of another object). This direct storage avoids the overhead of heap allocation and garbage collection.
  2. Immutable: Value types are immutable. Once created, their state cannot be changed, much like Java’s primitive types (int, float, etc.).
  3. No Identity: They do not have an identity, which means that their equality is based on the values of their fields, not on their identity in memory. There are no references or pointers to value types, and they do not have a separate object header like regular objects.
  4. Syntax and Usage: They are expected to be defined similarly to classes but with some restrictions due to their nature (e.g., immutability, no identity).

Example: Complex Numbers

Let’s take an example of a ComplexNumber class. In traditional Java, each instance of this class is a reference type.

public class ComplexNumber {
    private final double real;
    private final double imaginary;

    public ComplexNumber(double real, double imaginary) {
        this.real = real;
        this.imaginary = imaginary;
    }

    // Getters and other methods...
}

Creating an array of ComplexNumber objects involves storing references to these objects in the heap, which can be memory-inefficient:

ComplexNumber[] complexNumbers = new ComplexNumber[1000];
for (int i = 0; i < complexNumbers.length; i++) {
    complexNumbers[i] = new ComplexNumber(Math.random(), Math.random());
}

Using Value Types

With value types, you could define ComplexNumber as a value type. This is a hypothetical example, as the actual syntax and capabilities may vary:

public value class ComplexNumber {
    public double real;
    public double imaginary;
}

Now, when you create an array of ComplexNumber, each instance is stored inline in the array, just like an array of primitives. This means the memory layout is contiguous, and there’s no overhead of object headers or references.

ComplexNumber[] complexNumbers = new ComplexNumber[1000];
for (int i = 0; i < complexNumbers.length; i++) {
    complexNumbers[i] = new ComplexNumber(Math.random(), Math.random());
}


Certainly! Let’s consider a more concrete example to illustrate how value types could be used in Java and how they could be stored inline in an array, as envisioned by Project Valhalla. Please note that as of my last knowledge update in April 2023, value types were not yet part of the standard Java release, so the syntax and features may evolve.

Example: Complex Numbers

Let’s take an example of a ComplexNumber class. In traditional Java, each instance of this class is a reference type.

public class ComplexNumber { private final double real; private final double imaginary; public ComplexNumber(double real, double imaginary) { this.real = real; this.imaginary = imaginary; } // Getters and other methods... }

Creating an array of ComplexNumber objects involves storing references to these objects in the heap, which can be memory-inefficient:

ComplexNumber[] complexNumbers = new ComplexNumber[1000]; for (int i = 0; i < complexNumbers.length; i++) { complexNumbers[i] = new ComplexNumber(Math.random(), Math.random()); }

Using Value Types

With value types, you could define ComplexNumber as a value type:

public value class ComplexNumber { public double real; public double imaginary; }

Now, when you create an array of ComplexNumber, each instance is stored inline in the array, just like an array of primitives. This means the memory layout is contiguous, and there’s no overhead of object headers or references.

ComplexNumber[] complexNumbers = new ComplexNumber[1000]; for (int i = 0; i < complexNumbers.length; i++) { complexNumbers[i] = new ComplexNumber(Math.random(), Math.random()); }

In this scenario, complexNumbers is an array where each ComplexNumber is stored directly within the array’s memory structure, rather than as a separate heap object. This inline storage reduces memory consumption and can lead to performance improvements, especially when processing large arrays.

Impact on Java Memory Model:

  • Memory Efficiency: The array of ComplexNumber value types uses less memory than an array of ComplexNumber objects because there’s no overhead for object headers or references.
  • Performance Gains: Accessing the elements of the array is faster due to the contiguous memory layout, improving cache locality.
  • No Garbage Collection: These value types, when used in a method scope and stored on the stack, are not subject to garbage collection, reducing GC overhead.

Project Leyden

Project Leyden aimed at addressing a long-standing challenge in the Java ecosystem: startup time and memory footprint. The project’s primary focus is to introduce the concept of static images to the Java platform.
How It Works:

The process involves ahead-of-time (AOT) compilation, which compiles Java bytecode into native code before the application is run. This contrasts with the traditional Just-In-Time (JIT) compilation approach of the JVM, where bytecode is compiled into native code at runtime.

Key Objectives of Project Leyden:

  1. Static Images:
    • Concept: The idea is to create precompiled and optimized runtime images of Java applications. These images would be generated ahead-of-time (AOT) and would include all necessary components of the application and the runtime (like classes, libraries, and parts of the JVM itself).
    • Benefits: By compiling Java code ahead-of-time, startup times can be significantly reduced, as the JVM would have less work to do at runtime. This approach also reduces the memory footprint, as the static images are optimized for the specific application.
  2. Faster Startup Time:
    • Focus: One of the primary goals is to improve the startup time of Java applications, which is a critical factor in cloud and serverless computing environments where rapid scaling and efficient resource usage are essential.
  3. Reduced Memory Footprint:
    • Focus: By optimizing the runtime image for each specific application, the memory usage can be more efficient, which is especially beneficial in environments where resources are constrained or costly.
  4. Compatibility with Existing Java Ecosystem:
    • Goal: Project Leyden aims to achieve these objectives while maintaining compatibility with the existing Java ecosystem, ensuring that current Java applications can benefit from these improvements without significant changes.