Java 25 (the new Long Term Support version) was released on September 18th 2025, and with it, some new cool features have been released. Which features are available to help improve my code? In this blog, I will give you a brief introduction to the released features which you can use directly as a developer (previews, experimentals and incubators are not included).

Scoped Values

In the previous LTS, Java 21, Virtual Threads were introduced, which simply put decoupled the use of kernel threads from Java’s Threads. In extension to Structure Concurrency, which is still in preview, they added Scoped Values. Scoped Values are containers that contain immutable values, which are designed to be safely used by your thread or by the child threads and only live as long as your 'scope'. It is preferred over the old ThreadLocal which was mutable and can live as long as the thread exists, even if you reuse that thread for other tasks.

To use ScopedValues, you need to do the following:

Instantiate the ScopedValue container, later on, you can access this by reference to get the value that will be assigned.

  final static ScopedValue<User> LOGGED_IN_USER = ScopedValue.newInstance();

Use ScopedValue.where() to assign the value and with run() you can execute some code or run on a thread where you can have access to the value for as long as code in the run() is executing.

  final User user = authenticateUser(request);
  ScopedValue.where(LOGGED_IN_USER, user)
    .run(() -> restAdapter.processRequest(request));

Within the running code, you can access the ScopedValue as following:

  validate(request, LOGGED_IN_USER.get());

Stream Gatherers

Since Java 8, we have access to the Stream API which gave us a toolset to process a sequence of values. Stream Gatherers are a newly introduced feature that helps 'gathering' some of your sequence and processing them in a batch. This is useful if you want to have more fine-grained control in executing operations in bulk or batch. To use a Stream Gatherer, you can use it seamlessly in your Stream with .gather(…​).

There are 5 built-in Gatherers that can be used (fold, mapConcurrent, windowFixed, WindowSliding, scan), but a custom Gatherer can also be created.

fold

The .fold(initial, folderFn) method is to fold all values of the sequence into an single accumulated value.

 Stream.of("a", "b", "c")
    .gather(Gatherers.fold(() -> "", String::concat))
    .findFirst();
// Result: Optional["abc"]

mapConcurrent

The .mapConcurrent(maxConcurrency, mapper) method is a way to map every value in a concurrent way and not sequential, threads will be managed by the JVM, so you don’t have to. This comes in really handy when you need to do multiple IO calls for every value for example.

 Stream.of("a", "b", "c")
    .gather(Gatherers.mapConcurrent(2, n -> "Result: " + n))
    .toList();
// Result: ["Result: 1", "Result: 2", "Result: 3"]
// (but processed 2 at a time concurrently)

windowFixed

To group elements into separate batches of a fixed size you can use .windowFixed(windowSize). Use-cases for this are bulking database insertions or email batching to make sure the servers aren’t overloaded.

Stream.of(1,2,3,4,5,6)
  .gather(Gatherers.windowFixed(3))
  .toList();
// Result: [[1,2,3], [4,5,6]]

WindowSliding

Similar to windowFixed, you have .windowSliding(windowSize) where its next window is one where the first entry is dropped and appended by the next. This is useful for moving averages, time-series analysis or trend analysis for example.

Stream.of(1,2,3,4,5)
  .gather(Gatherers.windowSliding(3))
  .toList();
// Result: [[1,2,3], [2,3,4], [3,4,5]]

scan

.scan(initial, accumulatorFn) performs the accumulation but also keeps every intermediate result. This is quite useful for progressively building up state, progress tracking or cumulative statistics.

Stream.of(1,2,3,4)
    .gather(Gatherers.scan(0, Integer::sum))
    .toList();
// Result: [0, 1, 3, 6, 10]

Unnamed Variables and Patterns

Since Java 9, the use of an underscore( _ ) as a variable name is no longer supported, being that it is used as a keyword. With Java 22 and on, the underscore can now be used for unnamed variables and Patterns. With this, you can now use an underscore as an unnamed variable if they aren’t needed or used, in addition, this could also be applied to Patterns where the specific variable is not of any relevance to your Pattern. This will enhance the readability of your code since you can show intent of which variables are important and which aren’t.

In the example below, you can clearly see which variable matters for each case.

record Point(int x, int y) {}

static void main() {
    var point = new Point(1, 2);
     var result = switch (point) {
         case Point(int x, _) when x > 0 -> "Right side";
         case Point(int x, _) when x < 0 -> "Left side";
         case Point(_, int y) when y != 0 -> "On Y-axis";
         case Point(_, _) -> "Origin";
     };
    //result = Right side
}

Compact Source Files and Instance Main Methods

Java has many verbose constructs, which for new developers may be overwhelming and for the more experienced developers, sometimes unnecessary. They made some changes to enable writing java code more simply and compactly. They did this by adding Instance Main Methods, Compact Source Files, and adding a new Class in the java.lang package for some basic IO operations.

Instance Main Methods

Before, the entry point of every java program was the public static void main(String[] args) method, fully written out. This is no longer needed and from now you can write the main method as follows:

class HelloWorld {
    void main() {
        System.out.println("Hello, World!");
    }
}

Compact Source Files

In Java, your code is usually encapsulated with the use of classes, packages and modules. These concepts are good, but are not always needed when you are writing a small java program in a single file. In these cases, the compiler will implicitly declare a class whose members are the unenclosed fields and methods.

For example, you can write a simple Java program without a class declaration as follows:

void main() {
    System.out.println("Hello, World!");
}

New class for printing on command line

To ensure creating your first java programs is as simple as possible, you can now use the IO class to print or read from the command line. This will eliminate some boilerplate code like using System.out, InputStreamReader, BufferedRead and IOException

You can now simplify your simple Java program to:

void main() {
    IO.println("Hello, World!");
}

So, some small changes which don’t provide much new functionality, but it’s still nice that we can now write simple things more succinctly in comparison to:

package org.example;

public class SimpleJavaProgram {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

Flexible Constructor Bodies

From now on, it is allowed to use statements in the constructor before the super() or this. Before, constructors of superclasses were always called first (implicit or explicit) upon calling the subclass constructor. This means that after constructing the superclass your subclass constructor could fail for obvious reasons, which potentially could also have side-effects. In response to that, it is now possible to put statements before the superclass constructor as long as you don’t reference the object under construction.

class Corvette extends Car {
    public Corvette(String name, int year) {
        if (year < 1953) {
            throw new IllegalArgumentException("Corvette didn't exist before 1953");
        }
        super(name, year);
    }
}

Markdown Documentation Comments

From Java 23 and on, it is now possible to write JavaDoc with Markdown, there is no reason anymore not to write documentation from now on! ;)

shadow-left