Skip to content

Java 25 (September 2025)

Java 25 Status

Release Information

Release Date: September 2025 End of Support: September 2033 Theme: Refinements & Developer Experience

Overview

Java 25 is the latest LTS release, building on the foundations of Java 21 with refinements to pattern matching, constructor flexibility, and developer productivity. It continues to make Java more expressive while maintaining backward compatibility.


Primitive Types in Patterns

The Problem

Pattern matching only worked with reference types, not primitives.

Before/After

// Could only match reference types
Object value = 42;

if (value instanceof Integer i) { // Works
    System.out.println(i * 2);
}

// But primitive int? No pattern matching
int number = getValue();
if (number > 0 && number < 100) {
    // Can't use patterns
}
// Primitives in patterns
Object value = 42;

String result = switch (value) {
    case int i when i > 100 -> "Large integer: " + i;  // (1)
    case int i when i > 0 -> "Small positive: " + i;
    case int i -> "Zero or negative: " + i;
    case double d -> "Double: " + d;
    case String s -> "String: " + s;
    default -> "Other";
};

// Direct primitive switching with guards
int score = getScore();
String grade = switch (score) {
    case int s when s >= 90 -> "A";
    case int s when s >= 80 -> "B";
    case int s when s >= 70 -> "C";
    case int s when s >= 60 -> "D";
    default -> "F";
};
  1. Direct int pattern, not Integer

Extended Primitive Patterns

// In instanceof
if (value instanceof int i && i > 0) {
    System.out.println("Positive int: " + i);
}

// Nested in record patterns
record Measurement(double value, String unit) {}

String format(Object obj) {
    return switch (obj) {
        case Measurement(double v, String u) when v > 1000 ->
            "%.2e %s".formatted(v, u);  // Scientific notation
        case Measurement(double v, String u) ->
            "%.2f %s".formatted(v, u);  // Normal
        default -> obj.toString();
    };
}

Flexible Constructor Bodies

The Problem

super() or this() MUST be the first statement in constructors, even when you need to validate/compute arguments.

Before/After

public class BankAccount {
    private final String accountId;
    private final double initialBalance;

    public BankAccount(String accountId, double balance) {
        // Can't validate BEFORE calling super!
        super(); // Must be first

        // Validation happens after super - too late if parent
        // does something with invalid data
        if (accountId == null || accountId.isBlank()) {
            throw new IllegalArgumentException("Invalid account ID");
        }
        if (balance < 0) {
            throw new IllegalArgumentException("Balance cannot be negative");
        }

        this.accountId = accountId;
        this.initialBalance = balance;
    }

    // Workaround: static factory methods
    public static BankAccount create(String id, double balance) {
        validate(id, balance);  // Validate first
        return new BankAccount(id, balance);
    }
}
public class BankAccount {
    private final String accountId;
    private final double initialBalance;

    public BankAccount(String accountId, double balance) {
        // Validation BEFORE super() - now allowed!
        if (accountId == null || accountId.isBlank()) {
            throw new IllegalArgumentException("Invalid account ID");
        }
        if (balance < 0) {
            throw new IllegalArgumentException("Balance cannot be negative");
        }

        // Compute values before super
        var normalizedId = accountId.trim().toUpperCase();

        super();  // Can be called after validation/computation

        this.accountId = normalizedId;
        this.initialBalance = balance;
    }
}

Rules

public class Child extends Parent {
    private final String value;

    public Child(String input) {
        // These are allowed BEFORE super():
        // - Validation and argument checking
        // - Local variable declarations and assignments
        // - Static method calls
        // - Pure computations

        if (input == null) {
            throw new IllegalArgumentException("Input required");
        }
        var processed = input.trim();

        // These are NOT allowed before super():
        // - Accessing 'this' (including fields)
        // - Calling instance methods
        // - Accessing super members

        super(processed);  // Now call super with processed value

        this.value = processed;  // OK after super()
    }
}

Compact Source Files (Simple Main)

The Problem

Even "Hello World" requires class declaration and public static void main(String[] args).

Before/After

// HelloWorld.java - minimum viable program
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

// Simple script - still needs boilerplate
public class Script {
    public static void main(String[] args) {
        var name = args.length > 0 ? args[0] : "World";
        System.out.println("Hello, " + name + "!");
    }
}
// HelloWorld.java - just the code!
void main() {
    System.out.println("Hello, World!");
}

// Script.java - with implicit args access
void main(String[] args) {
    var name = args.length > 0 ? args[0] : "World";
    System.out.println("Hello, " + name + "!");
}

// Can include other methods and fields
String greeting = "Hello";

void main() {
    greet("World");
}

void greet(String name) {
    System.out.println(greeting + ", " + name + "!");
}

How It Works

// What you write:
void main() {
    System.out.println("Hello!");
}

// What Java sees (implicit wrapper):
final class HelloWorld {
    static final HelloWorld $instance = new HelloWorld();

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

    public static void main(String[] args) {
        $instance.main();
    }
}

Automatic Imports

// These are implicitly imported in simple source files:
// - java.base module exports
// - Static import of System.out, System.err, System.in

void main() {
    println("No need to write System.out!");  // Works!
    var input = readln("Enter name: ");       // Console input
}

Scoped Values

Interview Topic

Understand why this is better than ThreadLocal for virtual threads.

The Problem

ThreadLocal has issues: memory leaks, expensive with virtual threads, inherited values can be surprising.

Before/After

// ThreadLocal - problematic with virtual threads
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();

void handleRequest(Request request) {
    currentUser.set(authenticate(request));
    try {
        processRequest();
    } finally {
        currentUser.remove();  // Easy to forget = memory leak!
    }
}

void processRequest() {
    User user = currentUser.get();  // Access in nested call
    // ...
}

// Problems:
// 1. Must remember to remove() - leaks if forgotten
// 2. Mutable - any code can set()
// 3. Expensive copying for InheritableThreadLocal
// 4. Unbounded lifetime
// ScopedValue - designed for virtual threads
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();

void handleRequest(Request request) {
    User user = authenticate(request);

    // Scoped execution - clean, safe, efficient
    ScopedValue.where(CURRENT_USER, user)
        .run(() -> processRequest());  // (1)

    // Automatically "unset" after run() completes
    // No cleanup needed, no memory leaks possible
}

void processRequest() {
    User user = CURRENT_USER.get();  // Access in nested call

    // Can also check if bound
    if (CURRENT_USER.isBound()) {
        // ...
    }
}
  1. Value is immutable within scope, automatically cleaned up

Structured Concurrency with Scoped Values

private static final ScopedValue<RequestContext> CTX = ScopedValue.newInstance();

Response handleRequest(Request request) {
    var context = new RequestContext(request);

    return ScopedValue.where(CTX, context).call(() -> {
        // Context available in all child tasks
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            var userFuture = scope.fork(() -> fetchUser());  // Has CTX
            var orderFuture = scope.fork(() -> fetchOrders()); // Has CTX

            scope.join().throwIfFailed();

            return new Response(userFuture.get(), orderFuture.get());
        }
    });
}

User fetchUser() {
    var ctx = CTX.get();  // Automatically inherited from parent
    return userService.findById(ctx.userId());
}

Comparison

Aspect ThreadLocal ScopedValue
Mutability Mutable (set() anytime) Immutable within scope
Cleanup Manual (remove()) Automatic
Memory safety Leak-prone Leak-proof
Virtual threads Expensive inheritance Cheap inheritance
Rebinding Yes Only in nested scope

Key Derivation Function API

The Problem

Deriving cryptographic keys (PBKDF2, HKDF) required external libraries or verbose code.

After (Java 25+)

// PBKDF2 - derive key from password
KeyDerivation kdf = KeyDerivation.getInstance("PBKDF2WithHmacSHA256");

char[] password = "user-password".toCharArray();
byte[] salt = SecureRandom.getInstanceStrong().generateSeed(16);

SecretKey key = kdf.deriveKey("AES", new PBKDFParameterSpec(
    password,
    salt,
    310_000,  // iterations
    256       // key length in bits
));

// HKDF - derive multiple keys from master key
KeyDerivation hkdf = KeyDerivation.getInstance("HKDF-SHA256");

SecretKey masterKey = // ... from key agreement
byte[] info = "encryption-key".getBytes();

SecretKey derivedKey = hkdf.deriveKey("AES", new HKDFParameterSpec(
    masterKey,
    info,
    256
));

Compact Object Headers (JEP 450)

The Improvement

Reduces object header size from 128 bits to 64 bits on 64-bit systems.

┌─────────────────────────────────────────┐
│           BEFORE (128 bits)              │
├─────────────────────────────────────────┤
│  Mark Word (64 bits)                     │
│  - Hash code, GC age, lock state         │
├─────────────────────────────────────────┤
│  Class Pointer (64 bits)                 │
│  - Pointer to class metadata             │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│           AFTER (64 bits)                │
├─────────────────────────────────────────┤
│  Combined Header (64 bits)               │
│  - Compressed class ptr + mark bits      │
└─────────────────────────────────────────┘

Benefits

  • 10-20% memory reduction for object-heavy applications
  • Better CPU cache utilization
  • Improved GC performance
  • No code changes required

Module Import Declarations

Simplified Module Imports

// Import all public types from a module
import module java.sql;

// Instead of individual imports:
// import java.sql.Connection;
// import java.sql.DriverManager;
// import java.sql.ResultSet;
// import java.sql.SQLException;
// import java.sql.Statement;
// ...

public class DatabaseExample {
    void query() throws SQLException {
        Connection conn = DriverManager.getConnection(url);
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("SELECT * FROM users");
    }
}

Quick Reference

// Primitive patterns
switch (value) {
    case int i when i > 0 -> "positive";
    case int i -> "non-positive";
    default -> "not an int";
}

// Flexible constructors
public MyClass(String input) {
    if (input == null) throw new IllegalArgumentException();
    super();  // Can be after validation now
}

// Compact source files
void main() {
    println("Hello, World!");
}

// Scoped Values
ScopedValue.where(CONTEXT, value).run(() -> {
    doWork();  // CONTEXT.get() works here
});

// Key Derivation
KeyDerivation.getInstance("PBKDF2WithHmacSHA256")
    .deriveKey("AES", params);