DEV Community

Vahram Papazyan for Hyperskill

Posted on

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

🔍 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 extends Person {
    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(name);
    }
}
Enter fullscreen mode Exit fullscreen mode

💡 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

🔍 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
}
Enter fullscreen mode Exit fullscreen mode

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), and
  • 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

🔍 What’s New

You can now use _ as a placeholder in places 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");
}
Enter fullscreen mode Exit fullscreen mode
  • 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.");
}
Enter fullscreen mode Exit fullscreen mode
  • 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("Matched, but ignored both fields.");
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Important: You cannot use _ more than once in the same pattern expression. For example, if (p instanceof Point(_, _)) is not allowed.

💡 Why It Matters

This is a small syntax change with big readability improvements, especially in exhaustive pattern matching or placeholder variables.

⚠️ Considerations

Overusing _ may make the code obscure. Use it where the intention is clear.

Stream Gatherers

✅ Key JEPs

🔍 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);
Enter fullscreen mode Exit fullscreen mode

You can also define a custom gatherer and use it.

💡 Why It Matters

This allows complex grouping, windowing, buffering, and merging logic to be cleanly expressed in the Stream API, e.g., batching or handling sliding windows, making stream pipelines more flexible and expressive.

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

Structured Concurrency

✅ Key JEPs

🔍 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());
    }
}
Enter fullscreen mode Exit fullscreen mode

💡 Why It Matters

  • Makes parallel code easier to reason about.
  • Simplifies error handling, cancellation propagation, and resource management.
  • Promotes better structured and maintainable multithreaded applications.

⚠️ Considerations:
Still under preview (5th round). Developers must migrate gradually and understand ForkJoin-based semantics.

Module Import Declarations

✅ Key JEPs

🔍 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;
Enter fullscreen mode Exit fullscreen mode

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

💡 Why It Matters

  • Simplifies modular development: Especially useful for small programs, scripts, or single-source-file applications.
  • Reduces boilerplate: No need for a separate module-info.java just to use a few modules.
  • Improves clarity: The source file shows all its external module dependencies up front.
  • Enables better tooling: IDEs and compilers can provide better feedback and autocomplete when module dependencies are declared explicitly.

⚠️ Considerations

  • Using one or more module import declarations leads to a risk of name ambiguity due to different packages declaring members with the same simple name

Conclusion

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

Top comments (0)