Compact Source Files & Instance Main Methods
No more class wrapper or public static void main(String[] args). Write simple programs with minimal ceremony.
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } }
// That's the entire file! void main() { println("Hello, World!"); }
Flexible Constructor Bodies
You can now execute statements before super() or this() — as long as you don't access this.
class Employee extends Person { Employee(String rawName) { // ❌ Cannot validate before super() // if (rawName == null) throw ... // Must call super() FIRST super(validate(rawName)); } // Workaround: static helper method private static String validate(String n) { if (n == null) throw new IllegalArgumentException(); return n.strip(); } }
class Employee extends Person { Employee(String rawName) { // ✅ Validate BEFORE super() if (rawName == null) { throw new IllegalArgumentException( "Name cannot be null" ); } String cleaned = rawName.strip(); // Now call super super(cleaned); } }
Module Import Declarations
Import all exported packages of a module with a single statement. Drastically reduces import boilerplate.
import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; // 6 import statements just to get started
import module java.base; // ✅ All public types from java.base // are now available: List, Map, Path, // Files, Stream, Collectors, etc.
import module java.base; void main() { List<String> names = List.of("Alice", "Bob", "Charlie"); Map<Integer, List<String>> grouped = names.stream() .collect(Collectors.groupingBy(String::length)); println(grouped); // {3=[Bob], 5=[Alice], 7=[Charlie]} }
Primitive Types in Pattern Matching
Pattern matching with instanceof and switch now supports all primitive types — no more manual casting.
static String classify(Object obj) { if (obj instanceof Integer i) { return "Integer: " + i; } else if (obj instanceof Double d) { return "Double: " + d; } return "Unknown"; // ❌ No primitive support }
static String classify(Object obj) { return switch (obj) { case int i -> "int: " + i; case double d -> "double: " + d; case long l -> "long: " + l; default -> "other"; }; // ✅ Primitives work directly! }
static void process(Object obj) { if (obj instanceof int i) { println("Got a primitive int: " + i); } // Also works with guards if (obj instanceof int i && i > 100) { println("Large int: " + i); } }
Scoped Values (Finalized)
An immutable, thread-safe alternative to ThreadLocal — designed for virtual threads and structured concurrency.
// ❌ Mutable, leak-prone, per-thread copy static final ThreadLocal<String> USER = new ThreadLocal<>(); void handleRequest() { USER.set("alice"); try { doWork(); } finally { USER.remove(); // easy to forget! } } void doWork() { String u = USER.get(); // "alice" USER.set("bob"); // ⚠ mutated! }
// ✅ Immutable, auto-cleaned, efficient static final ScopedValue<String> USER = ScopedValue.newInstance(); void handleRequest() { ScopedValue .where(USER, "alice") .run(() -> doWork()); // auto-cleaned when scope ends } void doWork() { String u = USER.get(); // "alice" // Cannot mutate! Immutable by design }
Structured Concurrency (Finalized)
Treat a group of concurrent subtasks as a single unit — if one fails, cancel the rest automatically.
// ❌ Manual thread management ExecutorService exec = Executors.newFixedThreadPool(2); Future<User> f1 = exec.submit( () -> fetchUser(id)); Future<Order> f2 = exec.submit( () -> fetchOrder(id)); // If fetchUser fails, fetchOrder // keeps running wastefully User user = f1.get(); Order order = f2.get(); exec.shutdown();
// ✅ Structured - auto-cancellation Response handle(int id) throws Exception { try (var scope = new StructuredTaskScope .ShutdownOnFailure()) { var user = scope.fork( () -> fetchUser(id)); var order = scope.fork( () -> fetchOrder(id)); scope.join() .throwIfFailed(); return new Response( user.get(), order.get()); } // If either fails, the other is // cancelled immediately! }
Compact Object Headers
Object headers shrunk from 96–128 bits to 64 bits. Huge memory savings for apps with millions of small objects.
// An empty Object in memory: // Java 21 (default): 128 bits header = 16 bytes // [ Mark Word (64 bits) | Klass Pointer (64 bits) ] // Java 25 (compact): 64 bits header = 8 bytes // [ Combined Mark + Klass (64 bits) ] // Real-world impact example: // 10 million small objects (e.g., records in a cache) // Before: 10M × 16 bytes = 160 MB in headers alone // After: 10M × 8 bytes = 80 MB in headers // Savings: 80 MB → ~50% reduction! // Enable at JVM level: // java -XX:+UseCompactObjectHeaders MyApp
Key Derivation Function API (KDF)
A new standard API for deriving cryptographic keys — needed for TLS, password hashing, and modern security protocols.
import javax.crypto.KDF; import javax.crypto.SecretKey; import javax.crypto.spec.HKDFParameterSpec; void deriveKey() throws Exception { // Create a KDF instance using HKDF-SHA256 KDF kdf = KDF.getInstance("HKDF-SHA256"); // Define extraction parameters byte[] salt = "random-salt".getBytes(); byte[] ikm = "input-key-material".getBytes(); byte[] info = "context-info".getBytes(); // Extract-then-Expand in one step HKDFParameterSpec params = HKDFParameterSpec .extractThenExpand(salt, ikm, info, 32); // Derive a 256-bit AES key SecretKey derivedKey = kdf.deriveKey("AES", params); println("Key algo: " + derivedKey.getAlgorithm()); // Output: Key algo: AES }