Java 25 (September 2025)¶
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¶
// 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";
};
- Direct
intpattern, notInteger
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()) {
// ...
}
}
- 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);