What’s new in Java 25 (for Developers)
Java 25 lands with a strong mix of language quality-of-life, better startup, powerful profiling, and practical APIs.
This post is a hands-on tour: short sections, runnable code, and concrete scenarios where you’ll use each feature soon. Each snippet is intentionally minimal so you can copy, adapt, and run quickly.
Legend
- ✅ Final: stable, production-ready
- 🧪 Preview: requires
--enable-preview
🚀 Java 25 highlights
- ✅ Flexible constructor bodies (JEP 513)
- 🧩 Primitive patterns in
instanceof
andswitch
(JEP 507) - 📦 Compact programs & module imports (JEP 512 + JEP 511)
- 🔄 Structured concurrency + scoped values (JEP 505 + JEP 506)
- 🔐 KDF API and PEM encoding (JEP 510 + JEP 470)
- 📊 New JFR profiling tools (JEP 520 + JEP 509)
- ⚡ Faster startup via AOT cache (JEP 514 + JEP 515)
Language & syntax upgrades
JEP 513: Flexible constructor bodies (✅ final)
👉 JEP 513
What it is: You can run statements before calling super(...)
or this(...)
. A new constructor “prologue” lets you validate inputs and initialize your own fields safely before delegating up the hierarchy.
When you’ll use it: Fail-fast validation and establishing subclass integrity before a superclass might call overridable methods.
Before (previous approach: had to call super
first, often pushing validation into a factory or doing it after super
, risking partially-initialized state if superclass used overridable methods):
// BEFORE (pre-Java 25)
class Person {
Person(String name) { init(); }
void init() { /* might call overridable methods */ }
}
class Employee extends Person {
private String payrollId; // cannot set before super
Employee(String name, String payrollId) {
super(name); // must be first line
if (payrollId == null || !payrollId.matches("EMP-\\d{4}")) {
throw new IllegalArgumentException("Invalid payrollId");
}
this.payrollId = payrollId; // set only after super
}
}
Limitations before:
- Validation runs only after
super
, so superclass code might observe incomplete subclass state. - Workarounds: static factories, two-phase init, or duplicated validation earlier.
After (Employee payroll ID validated and initialized before Person’s constructor):
// src/main/java/com/loiane/people/Employee.java
package com.loiane.people;
class Person {
final String name;
Person(String name) { this.name = name; log("constructed: " + name); }
void log(String msg) { System.out.println(msg); }
}
class Employee extends Person {
final String payrollId;
Employee(String name, String payrollId) {
// Prologue: validate & set subclass state before calling super
if (payrollId == null || !payrollId.matches("EMP-\\d{4}"))
throw new IllegalArgumentException("Invalid payrollId");
this.payrollId = payrollId; // allowed before super
super(name); // delegation after establishing invariants
log("ready: " + this.payrollId);
}
public static void main(String[] args) {
new Employee("Ana", "EMP-1234");
}
}
What’s happening here?
- Validation + invariant setup happen in the prologue (before
super
). - Field assignment to
payrollId
is allowed early. - Subclass state exists before superclass logic runs.
JEP 507: Primitive patterns in instanceof and switch (🧪 third preview)
👉 JEP 507
What it is: Pattern matching now handles primitive types in instanceof
and switch
. You can safely test and bind narrowed values without manual range checks; switch
supports boolean
, long
, float
, and double
too.
When you’ll use it: Safer numeric conversions, expressive parsing, and cleaner business rules.
Before (manual casting + separate branches: more boilerplate and risk of accidental widening):
// BEFORE (pre-Java 25)
public class TierLegacy {
static String tierFor(Object usage) {
if (usage instanceof Integer) {
int i = (Integer) usage;
if (i < 1) return "FREE";
if (i < 1_000) return "STARTER";
if (i < 10_000) return "PRO";
return "ENTERPRISE";
} else if (usage instanceof Long) {
long l = (Long) usage;
if (l < 10_000) return "PRO"; else return "ENTERPRISE";
} else if (usage instanceof Double) {
double d = (Double) usage;
int asInt = (int) d; // silent truncation risk
if (asInt == d) {
return tierFor(asInt); // re-box to Integer call
}
return d < 1_000.0 ? "STARTER" : "PRO";
}
return "UNKNOWN";
}
}
Limitations before:
- Repetition of casts and variable declarations.
- Potential silent truncation when narrowing
double
toint
. - No pattern guard inline with
switch
expression.
After (pricing tiers with precise narrowing and guards):
// src/main/java/com/loiane/billing/Tier.java
package com.loiane.billing;
public class Tier {
static String tierFor(Object usage) {
if (usage instanceof int i) {
return switch (i) {
case int v when v < 1 -> "FREE";
case int v when v < 1_000 -> "STARTER";
case int v when v < 10_000 -> "PRO";
case int v -> "ENTERPRISE";
};
}
if (usage instanceof long l) {
return switch (l) {
case long v when v < 10_000 -> "PRO";
case long v -> "ENTERPRISE";
};
}
if (usage instanceof double d) {
// Bind only when d converts exactly to int (no silent truncation)
if (d instanceof int exact) {
return tierFor(exact);
}
return d < 1_000.0 ? "STARTER" : "PRO";
}
return "UNKNOWN";
}
public static void main(String[] args) {
System.out.println(tierFor(0)); // FREE
System.out.println(tierFor(999)); // STARTER
System.out.println(tierFor(9_999)); // PRO
System.out.println(tierFor(10_000L)); // ENTERPRISE (long path)
System.out.println(tierFor(42.0)); // STARTER (double exact -> int)
}
}
What’s happening here?
instanceof int exact
binds only on exact conversion (no silent loss).switch
overlong
and guards on values make intent clear.
How to run (preview):
javac --enable-preview --release 25 -d out src/main/java/com/loiane/billing/Tier.java
java --enable-preview -cp out com.loiane.billing.Tier
Compact programs and cleaner imports
JEP 512: Compact source files + instance main (✅ final)
👉 JEP 512
What it is: You can write a file with just methods/fields; the compiler wraps it in an implicit class. void main()
can be an instance method. New java.lang.IO
makes console I/O easy.
When you’ll use it: Katas, scripts, teaching, quick demos.
Before (explicit class + static main required):
// BEFORE (pre-Java 25)
public class HelloClassic {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
After (simplified compact source Hello World):
// Name this file: Hello.java (compact source)
void main() {
IO.println("Hello, World!");
}
What’s happening here?
- No class declaration;
void main()
is the entry point. IO.println
(java.lang.IO) avoids boilerplate.
Run:
java Hello.java
JEP 511: Module import declarations (✅ final)
👉 JEP 511
What it is: import module <name>;
imports all exported packages of a module (and transitives), even from classpath code.
When you’ll use it: Prototyping and learning; quickly pulling in broad APIs (e.g., java.se
in modular code).
Code (HTTP + JSON parsing with fewer imports):
// src/main/java/com/loiane/http/FetchTodo.java
package com.loiane.http;
import module java.base;
import module java.net.http;
public class FetchTodo {
public static void main(String[] args) throws Exception {
var client = java.net.http.HttpClient.newHttpClient();
var req = java.net.http.HttpRequest.newBuilder(URI.create("https://jsonplaceholder.typicode.com/todos/1")).build();
var res = client.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
}
}
What’s happening here?
- Two module imports bring in all needed
java.net.http
types; we still qualify types for clarity. - If an ambiguity arises, add a specific import to resolve it.
Concurrency and context
JEP 506 + JEP 505: Scoped values (✅ final) + Structured concurrency (🧪 fifth preview)
What it is: Scoped values are a safer, inheritable alternative to ThreadLocal
. Structured concurrency gives a lifecycle for task groups.
When you’ll use it: Propagating request IDs, auth, or locale across virtual threads with clear cancellation.
Code (correlation-id across a task group):
// src/main/java/com/loiane/observability/Trace.java
package com.loiane.observability;
import java.lang.ScopedValue;
import java.util.concurrent.StructuredTaskScope; // preview
public class Trace {
static final ScopedValue<String> CORR_ID = ScopedValue.newInstance();
static void log(String msg) { System.out.println("[" + CORR_ID.orElse("-") + "] " + msg); }
public static void main(String[] args) throws Exception {
var id = java.util.UUID.randomUUID().toString();
ScopedValue.where(CORR_ID, id).run(() -> {
try (var scope = new StructuredTaskScope<Void>()) { // simplified example
var a = scope.fork(() -> { log("load customer"); return null; });
var b = scope.fork(() -> { log("load orders"); return null; });
scope.join();
log("done");
}
});
}
}
What’s happening here?
ScopedValue.where(CORR_ID, id).run(...)
binds the correlation ID for the whole task scope.- Virtual threads (if used) inherit the scoped value without leaks.
Run (preview for structured concurrency):
javac --enable-preview --release 25 -d out src/main/java/com/loiane/observability/Trace.java
java --enable-preview -cp out com.loiane.observability.Trace
Security building blocks
JEP 510: KDF API (✅ final) with HKDF
👉 JEP 510
What it is: Standard javax.crypto.KDF
for deriving keys; includes HKDF and parameter specs.
When you’ll use it: Session keys, HPKE/KEM flows, keys-from-seed.
Code (derive AES key material for an export):
// src/main/java/com/loiane/crypto/HkdfDemo.java
package com.loiane.crypto;
import javax.crypto.*;
import javax.crypto.spec.HKDFParameterSpec;
import java.security.spec.AlgorithmParameterSpec;
public class HkdfDemo {
public static void main(String[] args) throws Exception {
byte[] ikm = "seed-key-material".getBytes();
byte[] salt = "salt".getBytes();
byte[] info = "export-label".getBytes();
KDF hkdf = KDF.getInstance("HKDF-SHA256");
AlgorithmParameterSpec params = HKDFParameterSpec
.ofExtract().addIKM(ikm).addSalt(salt)
.thenExpand(info, 32);
SecretKey key = hkdf.deriveKey("AES", params);
System.out.println("Key algorithm: " + key.getAlgorithm());
System.out.println("Key length: " + key.getEncoded().length);
}
}
JEP 470: PEM encodings (🧪 preview)
👉 JEP 470
What it is: First-class APIs to read/write PEM and DER with optional encryption.
When you’ll use it: Import/export keys without third-party libs; cleanly handle PEM envelopes.
Code (write a simple PEM record of bytes):
// src/main/java/com/loiane/crypto/PemWrite.java
package com.loiane.crypto;
import java.nio.file.*;
import java.util.HexFormat;
import jdk.security.pem.*; // preview API
public class PemWrite {
public static void main(String[] args) throws Exception {
byte[] payload = HexFormat.of().parseHex("DEADBEEF");
var rec = PEMEncoder.encode("TEST-DATA", payload);
Files.writeString(Path.of("test-data.pem"), rec);
}
}
Run (preview):
javac --enable-preview --release 25 -d out src/main/java/com/loiane/crypto/*.java
java --enable-preview -cp out com.loiane.crypto.PemWrite
Profiling and observability
JEP 520: JFR method timing & tracing (✅ final)
👉 JEP 520
What it is: Time and trace specific methods by bytecode instrumentation: no code changes needed. Configure via CLI, config files, or JMX.
When you’ll use it: Pinpoint slow initializers, hotspots in frameworks, or leaks in resource lifecycles.
Try it (trace HashMap::resize
while running your app):
java '-XX:StartFlightRecording:jdk.MethodTrace#filter=java.util.HashMap::resize,filename=recording.jfr' -jar yourapp.jar
jfr print --events jdk.MethodTrace --stack-depth 20 recording.jfr
Programmatic JMX (remote on-demand tracing) is also supported.
JEP 509: JFR CPU-time profiling (experimental, Linux)
👉 JEP 509
What it is: Accurate CPU-time profiles using the Linux CPU timer; attributes native time back to Java frames.
When you’ll use it: Production profiling with low overhead; prioritize CPU-bound work.
Quick start:
java -XX:StartFlightRecording=jdk.CPUTimeSample#enabled=true,filename=cpu.jfr -jar yourapp.jar
jfr view cpu-time-hot-methods cpu.jfr
Startup and warmup
JEP 514: One-step AOT cache creation (✅ final)
👉 JEP 514
What it is: -XX:AOTCacheOutput=app.aot
runs a training invocation and creates the cache in one step. Use -XX:AOTCache=app.aot
in production.
Scenario: CI step trains startup (typical HTTP route + deserialization), publishes the cache artifact. Production pods mount app.aot
for consistently fast cold starts.
# Train and create cache (one-shot)
java -XX:AOTCacheOutput=app.aot -jar yourapp.jar --train
# Use cache in prod
java -XX:AOTCache=app.aot -jar yourapp.jar
JEP 515: Ahead-of-time method profiling (✅ final)
👉 JEP 515
What it is: Profiles from training are stored in the AOT cache so the JIT optimizes earlier on startup.
Tip: For split infra, set create-only options via JDK_AOT_VM_OPTIONS
if training and creation happen on different instances.
Notes
- Preview language features (primitive patterns, structured concurrency) require
--enable-preview
to compile and run. - JFR additions enable both sampling (CPU-time) and precise timing/tracing; combine with JMC for deep analysis.
- Leyden AOT workflows (AOTCacheOutput + method profiling) improve cold starts and reduce warmup time.
- Keep an eye on preview/incubator JEPs; details (method names, minor API shapes) can still shift slightly until final.
Use the features that fit your use case today, and keep the previews on your radar: they’re already useful and getting close to final.
References
Happy Coding!