What’s new in Java 25 (for Developers)

06 Sep 2025
9 mins read

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.

placeholder

Legend

  • Final: stable, production-ready
  • 🧪 Preview: requires --enable-preview

🚀 Java 25 highlights

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:

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?

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:

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?

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?

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?

Concurrency and context

JEP 506 + JEP 505: Scoped values (✅ final) + Structured concurrency (🧪 fifth preview)

👉 JEP 505 👉 JEP 506

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?

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

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!