Table of contents
Text Link

Modern Java Evolution: Part 1 - From Java 22 to 25

Java has undergone a transformative evolution from versions 22 through 25, introducing features that significantly enhance readability, performance, native interop, and structured concurrency. This article breaks down key JEPs from recent releases, grouped by theme, and highlights both their benefits and potential considerations.

🏗️ Flexible Constructor Bodies

✅ Key JEPs
JEP 447: Statements before super(...) (Preview, Java 22)
JEP 482: Flexible Constructor Bodies (Second Preview, Java 23)
JEP 492: Flexible Constructor Bodies (Third Preview, Java 24)

🔍 What’s New
Traditionally, Java required that the first line in a constructor must be a call to super(...). These JEPs progressively relax that restriction, allowing:

  • Preliminary statements (e.g., input validation, logging) before calling super(...).
  • Explicit control flow logic before initializing the superclass.

Here is what it looks like:

class Student {
    private final String name;

    public Student(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Name cannot be null or blank");
        }

        System.out.println("Validated name: " + name);

        super();
        this.name = name;
    }
}

💡 Why It Matters
This aligns Java with more intuitive OOP practices, particularly in constructors, where argument validation is often required. It enables safer, more flexible class hierarchies.

⚠️ Considerations
Developers must still ensure that fields are not accessed before full initialization, preserving constructor safety.

🌐 Foreign Function & Memory API

✅ Key JEPs
JEP 424: Foreign Function & Memory API (First Preview, Java 19)
JEP 454: Foreign Function & Memory API (Stable, Java 22)

🔍 What’s New
This API replaces JNI with a safer, more efficient, and idiomatic way to:

  • Call native (C/C++) libraries.
  • Allocate and manage memory off-heap using MemorySegment, MemoryLayout, and Linker.
try (Arena arena = Arena.ofConfined()) {
    MemorySegment segment = arena.allocate(100);
    // Use memory directly
}

The Foreign Function & Memory API (FFM API) defines classes and interfaces so that client code in libraries and applications can:

  • Control the allocation and deallocation of foreign memory (MemorySegment, Arena, and SegmentAllocator).
  • Manipulate and access structured foreign memory (MemoryLayout and VarHandle).
  • Call foreign functions (Linker, SymbolLookup, FunctionDescriptor, and MethodHandle). The FFM API resides in the java.lang.foreign package of the java.base module.

💡 Why It Matters
Java can now operate with native libraries without boilerplate JNI code. This is huge for performance-sensitive applications like machine learning, image processing, and systems integration.

⚠️ Considerations
Still evolving and requires an understanding of low-level memory and ABI (Application Binary Interface) details. Avoid misuse that could cause memory leaks or segmentation faults.

✨ Unnamed Variables & Patterns

✅ Key JEPs
JEP 443: Unnamed Patterns and Variables (Preview, Java 21)
JEP 456: Unnamed Variables & Patterns (Final, Java 22)

🔍 What’s New
You can now use _ as a placeholder where a variable is syntactically required, but the actual value is not used. Here is what this JEP adds:

  • An unnamed variable, declared by using an underscore character, _ (U+005F), to stand in for the name of the local variable in a local variable declaration statement, or an exception parameter in a catch clause, or a lambda parameter in a lambda expression.
try {
    // some risky code
} catch (IOException _) {
    System.out.println("I/O error occurred");
}

An unnamed pattern variable, declared by using an underscore character to stand in for the pattern variable in a type pattern.

if (obj instanceof String _) {
    System.out.println("It's a string, but we don't care about the value.");
}

An unnamed pattern, denoted by an underscore character, is equivalent to the unnamed type pattern var _. It allows both the type and name of a record component to be elided in pattern matching.

record Point(int x, int y) {}

Point p = new Point(3, 4);

if (p instanceof Point(_, int y)) {
    System.out.println("Y is: " + y);
}

💡 Why It Matters
This small syntax change brings significant readability improvements, especially in exhaustive pattern matching or placeholder variables.

⚠️ Considerations
Overusing _ may make the code obscure. Use it where the intention remains clear.

🔁 Stream Gatherers

✅ Key JEPs
JEP 461: Stream Gatherers (First Preview, Java 22)
JEP 473: Stream Gatherers (Second Preview, Java 23)
JEP 485: Stream Gatherers (Final, Java 24)

🔍 What’s New
Stream.gather() introduces a mechanism to support intermediate stream operation that processes the elements of a stream by applying a user-defined entity called a gatherer. With the gather operation, we can build efficient, parallel-ready streams that implement almost any intermediate operation. This gather method is to intermediate operations what Stream::collect(Collector) is to terminal operations.

An example of a built-in gatherer:

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);

List<List<Integer>> grouped = numbers.stream()
        .gather(windowFixed(3))
        .toList();

System.out.println(grouped);

You can also define and use custom gatherers.

💡 Why It Matters
Allows complex grouping, windowing, buffering, and merging logic to be cleanly expressed in the Stream API, making stream pipelines more flexible and expressive.

⚠️ Considerations
It adds complexity. Understanding gatherer mechanics (accumulator, finisher) takes practice.

🧵 Structured Concurrency

✅ Key JEPs
JEP 453: Structured Concurrency (First Preview, Java 21)
JEP 462: Structured Concurrency (Second Preview, Java 22)
JEP 480: Structured Concurrency (Third Preview, Java 23)
JEP 499: Structured Concurrency (Fourth Preview, Java 24)
JEP 505: Structured Concurrency (Fifth Preview, Java 25)

🔍 What’s New
Structured concurrency is an approach to concurrent programming that preserves the natural relationship between tasks and subtasks, which leads to more readable, maintainable, and reliable concurrent code. It treats concurrent tasks as a structured unit of work—bound together by lifecycle and failure policies. It introduces APIs like StructuredTaskScope.

Structured concurrency derives from a simple principle: If a task splits into concurrent subtasks then they all return to the same place, namely the task's code block.

Response handle() throws InterruptedException {
    try (var scope = StructuredTaskScope.open()) {
        Subtask<String> user = scope.fork(() -> findUser());
        Subtask<Integer> order = scope.fork(() -> fetchOrder());

        scope.join();   // Join subtasks, propagating exceptions

        // Both subtasks have succeeded, so compose their results
        return new Response(user.get(), order.get());
    }
}

💡 Why It Matters
Makes parallel code easier to reason about, simplifies error handling and cancellation propagation, and promotes maintainable multithreaded applications.

⚠️ Considerations
Still under preview. Developers should migrate gradually and understand ForkJoin-based semantics.

📦 Module Import Declarations

✅ Key JEPs
JEP 476: Module Import Declarations (Preview, Java 23)
JEP 494: Module Import Declarations (Second Preview, Java 24)
JEP 511: Module Import Declarations (Final, Java 24)

🔍 What’s New
Module import declarations allow a Java source file to declare which modules it depends on — directly in the source code.

Instead of relying solely on the module system (via module-info.java) or command-line options to express module dependencies, developers can now specify module usage within individual .java files using the new import module syntax.

✅ Syntax Example:

import module com.example.graphics;

import com.example.graphics.Image;

This tells the compiler that this file uses types from the com.example.graphics module.

💡 Why It Matters

  • Simplifies modular development, especially for small programs, scripts, and single-source-file applications.
  • Reduces boilerplate by eliminating the need for a separate module-info.java for minor module usage.
  • Improves clarity by showing all external module dependencies up front.
  • Enables better tooling support in IDEs and compilers.

⚠️ Considerations

  • Using multiple module import declarations may lead to name ambiguities if different packages declare members with the same simple name.

Conclusion

From enabling pre-super() logic to improving native interop and concurrency management, these JEPs collectively represent Java’s steady transformation into a modern, expressive, and high-performance platform. While many features are still in preview, developers are encouraged to explore them now to prepare for their production maturity in the near future.

Share this article
Get more articles
like this
Thank you! Your submission has been received!
Oops! Something went wrong.

Create a free account to access the full topic

Wide range of learning tracks for beginners and experienced developers
Study at your own pace with your personal study plan
Focus on practice and real-world experience
Andrei Maftei
It has all the necessary theory, lots of practice, and projects of different levels. I haven't skipped any of the 3000+ coding exercises.
Get more articles like this
Thank you! Your submission has been received!
Oops! Something went wrong.

More on this

No items found.