Released September 2025 · Long-term support

Java 25 Features

Java 25 is the latest long-term-support release. It makes Java simpler to start with compact source files and module imports, more powerful with scoped values and flexible constructors, and cheaper to run with compact object headers and AOT caching. Learn each feature with clear explanations and runnable examples.

  • Compact Sourcevoid main() without a class
  • Scoped ValuesA better ThreadLocal
  • Flexible ConstructorsCode before super()
  • Faster RuntimeCompact headers, AOT cache
Feature 01

Compact Source Files and Instance main

Java 25 (JEP 512) finalised compact source files. A file can contain just a void main() method, with no class declaration, no static and no String[] args. Every package of java.base is imported automatically, and the new IO class gives simple console input and output. It is perfect for beginners and small scripts.

  • void main() is a valid entry point; static and args are optional
  • java.base (List, Map, Path, LocalDate...) is imported automatically
  • IO.println, IO.print and IO.readln for console I/O
  • Grows naturally into a normal class when the program gets bigger
HelloCompact.java
// A compact source file: no class, no static, no String[] args.
// All of java.base is imported automatically, and IO prints text.

void main() {
    String name = "Asha";
    IO.println("Hello, " + name + "!");

    List<String> topics = List.of("Records", "Patterns", "Virtual threads");
    for (String topic : topics) {
        IO.println("- " + topic);
    }
    IO.println("Topics: " + topics.size());
}
Run it
$ java HelloCompact.java
Hello, Asha!
- Records
- Patterns
- Virtual threads
Topics: 3
Feature 02

Module Import Declarations

With import module java.base; (JEP 511) you import every package a module exports, without listing them one by one. Your own code does not need to be a module. If a simple name exists in two imported packages, add a normal single-type import to choose one.

  • import module M; imports all packages exported by M
  • Also includes modules that M requires transitively
  • Single-type imports win over module imports, which resolves clashes
  • Compact source files get import module java.base automatically
ModuleImportDemo.java
// One line imports every package that a module exports
import module java.base;   // java.util, java.time, java.nio.file, ...
import module java.sql;    // java.sql, javax.sql (and java.xml, java.logging)

// Some simple names exist in more than one package, so name the one
// you want: java.util.Date vs java.sql.Date, and
// java.sql.Timestamp vs java.security.Timestamp
import java.util.Date;
import java.sql.Timestamp;

public class ModuleImportDemo {
    public static void main(String[] args) {
        List<String> langs = new ArrayList<>(List.of("kotlin", "java"));
        Collections.sort(langs);
        System.out.println("java.util     : " + langs);

        LocalDate release = LocalDate.of(2025, Month.SEPTEMBER, 16);
        System.out.println("java.time     : " + release.getDayOfWeek());

        Path path = Path.of("reports", "2025.csv");
        System.out.println("java.nio.file : " + path.getFileName());

        Timestamp ts = Timestamp.valueOf("2025-09-16 10:00:00");
        System.out.println("java.sql      : " + ts);

        Date date = new Date(0);
        System.out.println("Date type     : " + date.getClass().getName());
    }
}
Output
java.util     : [java, kotlin]
java.time     : TUESDAY
java.nio.file : 2025.csv
java.sql      : 2025-09-16 10:00:00.0
Date type     : java.util.Date
Feature 03

Flexible Constructor Bodies

Before Java 25, super(...) had to be the first statement in a constructor. Now (JEP 513) you can validate arguments, compute values and even assign fields before calling it. This fails fast on bad input and fixes the classic bug where an overridden method, called from the parent constructor, saw uninitialised fields.

  • Validate arguments before the parent constructor runs
  • Assign the subclass's own fields before super(...)
  • You still cannot use this to read fields or call methods before super(...)
  • Works for records' canonical and non-canonical constructors too
FlexibleConstructorDemo.java
public class FlexibleConstructorDemo {
    static class Employee {
        final String name;

        Employee(String name) {
            this.name = name;
            describe(); // calls the subclass version
        }

        void describe() {
            System.out.println("Employee " + name);
        }
    }

    static class Manager extends Employee {
        final int reports;

        Manager(String name, int reports) {
            // Java 25: code before super(...) to validate and prepare
            if (reports < 0) {
                throw new IllegalArgumentException("reports < 0");
            }
            String cleanName = name.strip();
            // Fields can be set before super(), so describe() sees them
            this.reports = reports;
            super(cleanName);
        }

        @Override
        void describe() {
            // Before Java 25, reports would still be 0 here
            System.out.println("Manager " + name + " with "
                    + reports + " reports");
        }
    }

    public static void main(String[] args) {
        new Employee("Ravi");
        new Manager("  Asha  ", 5);
        try {
            new Manager("Meera", -1);
        } catch (IllegalArgumentException e) {
            // Rejected before the Employee constructor ever ran
            System.out.println("Rejected: " + e.getMessage());
        }
    }
}
Output
Employee Ravi
Manager Asha with 5 reports
Rejected: reports < 0
Feature 04

Scoped Values

Scoped values (JEP 506, final in Java 25) are a safer, faster alternative to ThreadLocal. A value is bound for the duration of a call and is visible to every method called inside it, without passing it as a parameter. The binding is immutable and ends automatically, which suits virtual threads and request-scoped data such as the current user.

  • ScopedValue.where(KEY, value).run(...) or .call(...)
  • Read with KEY.get() anywhere inside the scope
  • Immutable and automatically unbound, so nothing leaks between requests
  • Cheap with millions of virtual threads, unlike ThreadLocal
ScopedValueDemo.java
public class ScopedValueDemo {
    // An immutable value shared with code called inside a scope
    static final ScopedValue<String> USER = ScopedValue.newInstance();

    public static void main(String[] args) throws Exception {
        // Bind a value only while run() executes
        ScopedValue.where(USER, "asha").run(() -> handleRequest());

        // Each request gets its own binding, even in parallel threads
        Thread t1 = Thread.ofVirtual().start(() ->
                ScopedValue.where(USER, "ravi").run(() -> handleRequest()));
        t1.join();

        // Rebinding for a nested call; the outer value comes back after
        ScopedValue.where(USER, "admin").run(() -> {
            System.out.println("outer : " + USER.get());
            ScopedValue.where(USER, "guest")
                    .run(() -> System.out.println("nested: " + USER.get()));
            System.out.println("outer : " + USER.get());
        });

        // call() returns a result from the scope
        int length = ScopedValue.where(USER, "meera")
                .call(() -> USER.get().length());
        System.out.println("length of user name: " + length);

        // Outside any scope the value is not bound
        System.out.println("bound now? " + USER.isBound());
    }

    static void handleRequest() {
        // No parameter needed: read the value bound by the caller
        System.out.println("Handling request for " + USER.get());
    }
}
Output
Handling request for asha
Handling request for ravi
outer : admin
nested: guest
outer : admin
length of user name: 5
bound now? false
Feature 05

Unnamed Variables and Patterns

Finalised in Java 22 (JEP 456), the underscore _ marks a variable or pattern component that is intentionally unused. It makes code clearer and stops warnings about unused variables in loops, catch blocks, lambdas and record patterns.

  • Record patterns: Order(_, _, int qty)
  • Loops and catch blocks: for (Order _ : orders), catch (Exception _)
  • Lambdas: (_, value) -> ...
  • Several _ can appear in the same scope
UnnamedDemo.java
import java.util.List;
import java.util.Map;

public class UnnamedDemo {
    record Order(String id, String item, int qty) { }

    public static void main(String[] args) {
        List<Order> orders = List.of(
                new Order("A1", "Book", 2), new Order("A2", "Pen", 5));

        // _ in a record pattern: ignore components you don't need
        int total = 0;
        for (Order order : orders) {
            if (order instanceof Order(_, _, int qty)) {
                total += qty;
            }
        }
        System.out.println("Total quantity: " + total);

        // _ for a variable that is never used
        int count = 0;
        for (Order _ : orders) {
            count++;
        }
        System.out.println("Orders: " + count);

        // _ for an unused exception and lambda parameter
        String input = "12x";
        try {
            Integer.parseInt(input);
        } catch (NumberFormatException _) {
            System.out.println("Not a number: " + input);
        }
        Map<String, Integer> stock = Map.of("Book", 3);
        stock.forEach((_, qty) -> System.out.println("In stock: " + qty));
    }
}
Output
Total quantity: 7
Orders: 2
Not a number: 12x
In stock: 3
Feature 06

Stream Gatherers

Java 24 (JEP 485) added Stream.gather(...), which does for intermediate operations what collect does for terminal ones. The Gatherers class provides ready-made operations for fixed and sliding windows, running totals and concurrent mapping, and you can write your own.

  • windowFixed(n) and windowSliding(n) for batches and moving averages
  • scan for running results, fold for a single result
  • mapConcurrent(n, fn) maps on virtual threads, keeping order
  • Implement Gatherer to build your own operations
GathererDemo.java
import java.util.List;
import java.util.stream.Gatherers;
import java.util.stream.Stream;

public class GathererDemo {
    public static void main(String[] args) {
        List<Integer> prices = List.of(100, 102, 98, 105, 110, 107);

        // windowFixed: split into groups of n
        System.out.println("windowFixed(2)  : " + prices.stream()
                .gather(Gatherers.windowFixed(2)).toList());

        // windowSliding: overlapping windows, e.g. moving averages
        List<String> movingAvg = prices.stream()
                .gather(Gatherers.windowSliding(3))
                .map(w -> w.stream().mapToInt(i -> i).average().orElse(0))
                .map(avg -> "%.1f".formatted(avg))
                .toList();
        System.out.println("moving average  : " + movingAvg);

        // scan: running results, like a running total
        System.out.println("running total   : " + Stream.of(1, 2, 3, 4)
                .gather(Gatherers.scan(() -> 0, Integer::sum)).toList());

        // fold: combine everything into one result
        System.out.println("fold (concat)   : " + Stream.of("a", "b", "c")
                .gather(Gatherers.fold(() -> "", String::concat))
                .findFirst().orElse(""));

        // mapConcurrent: run a mapping on up to n virtual threads
        System.out.println("mapConcurrent(4): " + Stream.of(1, 2, 3, 4, 5)
                .gather(Gatherers.mapConcurrent(4, n -> n * n)).toList());
    }
}
Output
windowFixed(2)  : [[100, 102], [98, 105], [110, 107]]
moving average  : [100.0, 101.7, 104.3, 107.3]
running total   : [1, 3, 6, 10]
fold (concat)   : abc
mapConcurrent(4): [1, 4, 9, 16, 25]
Feature 07

Foreign Function and Memory API

The FFM API (JEP 454, final in Java 22) replaces JNI for most uses. You can look up a C function, call it through a MethodHandle, and allocate native memory that an Arena frees safely. No C glue code or header files are needed.

  • Linker finds and calls native functions
  • Arena controls the lifetime of native memory
  • MemorySegment reads and writes memory safely
  • Run with --enable-native-access=ALL-UNNAMED to avoid warnings
FfmDemo.java
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;

public class FfmDemo {
    public static void main(String[] args) throws Throwable {
        // Find the C function strlen in the standard C library
        Linker linker = Linker.nativeLinker();
        MethodHandle strlen = linker.downcallHandle(
                linker.defaultLookup().find("strlen").orElseThrow(),
                FunctionDescriptor.of(ValueLayout.JAVA_LONG,
                        ValueLayout.ADDRESS));

        // An arena frees its native memory when it is closed
        try (Arena arena = Arena.ofConfined()) {
            MemorySegment text = arena.allocateFrom("Hello from Java 25");
            long length = (long) strlen.invokeExact(text);
            System.out.println("strlen() returned " + length);

            // Off-heap memory without Unsafe or JNI
            MemorySegment numbers = arena.allocate(ValueLayout.JAVA_INT, 5);
            for (int i = 0; i < 5; i++) {
                numbers.setAtIndex(ValueLayout.JAVA_INT, i, i * i);
            }
            System.out.print("off-heap ints   :");
            for (int i = 0; i < 5; i++) {
                System.out.print(" "
                        + numbers.getAtIndex(ValueLayout.JAVA_INT, i));
            }
            System.out.println();
        }
    }
}
Output
strlen() returned 18
off-heap ints   : 0 1 4 9 16
Feature 08

Preview Features in Java 25

Java 25 also includes preview features that you can try with --enable-preview. Primitive types in patterns let switch and instanceof work with int, double and other primitives, and stable values are set once and then treated like constants by the JVM.

  • Primitive patterns: case int i when i < 0, x instanceof byte b
  • Stable values: lazy initialisation that is safe and fast
  • Structured concurrency: treat related tasks as one unit (fifth preview)
  • Vector API for SIMD computations (incubator)
PreviewDemo.java
// Preview features in Java 25.
// Run with:  java --enable-preview PreviewDemo.java

// Primitive types in patterns (JEP 507)
static String size(int value) {
    return switch (value) {
        case 0 -> "zero";
        case int i when i < 0 -> "negative";
        case int i when i < 100 -> "small";
        default -> "large";
    };
}

// Stable values (JEP 502): set once, then treated as a constant
static final StableValue<String> CONFIG = StableValue.of();

static String config() {
    return CONFIG.orElseSet(() -> {
        IO.println("(loading config once)");
        return "prod";
    });
}

void main() {
    IO.println(size(0) + ", " + size(-4) + ", " + size(42) + ", " + size(500));

    // instanceof with a primitive: does the value fit in a byte?
    int big = 300;
    int small = 42;
    IO.println("300 fits in byte: " + (big instanceof byte));
    if (small instanceof byte b) {
        IO.println("42 as byte: " + b);
    }

    IO.println("config: " + config());
    IO.println("config: " + config());
}
Run it
$ java --enable-preview PreviewDemo.java
zero, negative, small, large
300 fits in byte: false
42 as byte: 42
(loading config once)
config: prod
config: prod
Feature 09

JVM and Platform Changes

Java 25 makes applications cheaper to run. Compact object headers shrink every object, the AOT cache speeds up startup and warm-up, Shenandoah gained a generational mode, and Flight Recorder can profile CPU time and time individual methods.

  • Compact object headers: 8-byte headers on 64-bit platforms
  • AOT cache: record a training run once, then start faster
  • Generational Shenandoah and new JFR profiling events
  • Key Derivation Function API; 32-bit x86 port removed
Commands and notes
# Compact object headers (JEP 519): smaller objects, less heap
java -XX:+UseCompactObjectHeaders -jar app.jar

# AOT cache in one step (JEP 514/515): faster startup and warm-up
java -XX:AOTCacheOutput=app.aot -jar app.jar   # training run
java -XX:AOTCache=app.aot -jar app.jar         # production run

# Generational Shenandoah is now a product feature (JEP 521)
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational App

# JFR: CPU-time profiling (Linux, experimental) and method timing
java -XX:StartFlightRecording:jdk.CPUTimeSample#enabled=true App

# Security: Key Derivation Function API (JEP 510), and since
# Java 24 quantum-resistant ML-KEM and ML-DSA algorithms
# Removed: 32-bit x86 port; Security Manager is permanently disabled
What Comes Next

Beyond Java 25

Java keeps a six-month release cycle, and several long-running OpenJDK projects are shaping its future:

Release / projectWhat it brings
Java 26 (March 2026)HTTP/3 in the HTTP Client, AOT object caching with any GC, Applet API removed
Java 27 (September 2026)Non-LTS release continuing the six-month cadence; see the OpenJDK JDK 27 project page for its JEPs
Project ValhallaValue classes and objects that behave like primitives (in development)
Project LeydenFaster startup and warm-up through ahead-of-time optimisation
Project BabylonCode reflection for GPUs and machine learning (in development)