Back to blog

Java 25: New APIs and Libraries

javajava-25jdkapibackend
Java 25: New APIs and Libraries

So far in this series, we have looked at language changes and JVM and performance improvements in Java 25. In this post, we shift focus to the standard library side of the release.

Java 25 adds several meaningful new APIs, ranging from concurrency utilities that are now permanently part of the platform, to new cryptography APIs that fill long-standing gaps in the standard library.

The key additions are:

  • JEP 506: Scoped Values (Final)
  • JEP 510: Key Derivation Function API (Final)
  • JEP 502: Stable Values (Preview)
  • JEP 470: PEM Encodings of Cryptographic Objects (Preview)
  • JEP 508: Vector API (Tenth Incubator)

Not all of these are ready for production adoption in the same way. We will cover stability, practical use cases, and what you should evaluate first.

Quick Classification

Before going deep, here is the most useful breakdown by stability:

Final — available in production without flags

  • Scoped Values (java.lang.ScopedValue)
  • Key Derivation Function API (javax.crypto.KDF)

Preview — requires --enable-preview at compile and runtime

  • Stable Values (java.lang.StableValue)
  • PEM Encodings (java.security.PEMEncoder / PEMDecoder)

Incubator — requires --add-modules jdk.incubator.vector

  • Vector API (jdk.incubator.vector)

The final features are the most actionable for most teams. The preview and incubator features are worth tracking but not yet for standard production use.

1. Scoped Values (Final)

JEP 506 delivers Scoped Values as a permanently finalized API in Java 25. This is one of the most practically important additions in the release for backend developers.

The Problem

ThreadLocal was designed for a world of platform threads, typically with one thread per request. That model worked fine for years, but it has three serious problems when you combine it with virtual threads:

  • Mutability: Any code with a reference to the key can call set() at any time, making state hard to reason about.
  • Cleanup: You must manually call remove(), or you risk memory leaks in thread pools.
  • Cost at scale: When virtual threads fork child tasks, each inherits a copy of the thread-local map, which is expensive when you have hundreds of thousands of concurrent threads.

What Scoped Values Solve

ScopedValue is designed exactly for the "pass read-only context down the call stack" use case. A scoped value is:

  • immutable once bound,
  • automatically cleaned up when the scope exits,
  • cheaply inherited by child virtual threads via StructuredTaskScope.

The API

// Declare a scoped value — typically static final
static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();

Binding and running:

ScopedValue.where(REQUEST_ID, req.getId())
           .where(CURRENT_USER, authenticate(req))
           .run(this::processRequest);

Reading from anywhere in the call tree:

String reqId = REQUEST_ID.get();      // throws if not bound
User user = CURRENT_USER.orElse(null); // safe fallback

Server Framework Pattern

Here is what request-scoped context looks like with ScopedValue:

public class RequestHandler {
    private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
    private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
 
    public void handle(HttpRequest req) {
        ScopedValue.where(REQUEST_ID, req.getId())
                   .where(CURRENT_USER, authenticate(req))
                   .run(this::processRequest);
        // after run() returns, REQUEST_ID and CURRENT_USER are unbound
        // no cleanup code needed
    }
 
    private void processRequest() {
        String reqId = REQUEST_ID.get();
        User user = CURRENT_USER.get();
        // pass to services, repositories, loggers — they can read the same values
    }
}

Propagation to Child Virtual Threads

When you use StructuredTaskScope, scoped values are automatically inherited by forked subtasks:

private static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance();
 
ScopedValue.where(TRACE_ID, "abc-123").run(() -> {
    try (var scope = StructuredTaskScope.open()) {
        scope.fork(() -> callServiceA()); // TRACE_ID.get() == "abc-123"
        scope.fork(() -> callServiceB()); // TRACE_ID.get() == "abc-123"
        scope.join();
    }
});
// TRACE_ID is unbound here — no remove() call required

Re-Binding in Nested Scopes

A scoped value can be re-bound for a nested scope. The outer binding is automatically restored when the inner scope exits:

ScopedValue.where(USER, "alice").run(() -> {
    // USER.get() == "alice"
    ScopedValue.where(USER, "sudo").run(() -> doAdminAction());
    // USER.get() == "alice" again — automatically restored
});

ScopedValue vs ThreadLocal

ThreadLocalScopedValue
MutabilityMutable via set() anywhereImmutable once bound
CleanupManual remove()Automatic on scope exit
Virtual thread costCopies entire map per threadShared reference, no copy
Child thread inheritanceInherited-and-copiedInherited directly via scope
SecurityAny holder of the key can writeRead-only after binding

Production Advice

ScopedValue is finalized in Java 25 and ready for production use. The most natural migration targets are:

  • per-request user identity and security context,
  • distributed tracing correlation IDs,
  • tenant IDs in multi-tenant applications,
  • logger MDC replacement in virtual-thread-heavy systems.

2. Key Derivation Function API (Final)

JEP 510 adds a standard Key Derivation Function (KDF) API to the javax.crypto package. This fills a real gap in the Java standard library.

What a KDF Is

A Key Derivation Function takes some source secret — a shared secret from Diffie-Hellman, a passphrase, or a random seed — and derives one or more cryptographically strong, independent keys from it.

The most common modern algorithm is HKDF, which works in two steps:

  1. Extract: Condenses the input keying material into a uniform pseudorandom key.
  2. Expand: Stretches that key into multiple purpose-specific derived keys using context labels.

HKDF is the key derivation mechanism inside TLS 1.3, Signal Protocol, Noise Protocol, and many other modern systems.

The Problem Before Java 25

Java had no standard KDF API. Developers had to either:

  • use SecretKeyFactory with awkward workarounds,
  • import BouncyCastle,
  • or write their own HMAC-based derivation.

None of those options were satisfying for teams that preferred staying within the standard library.

The API

The main class is javax.crypto.KDF, modeled after Cipher and MessageDigest:

KDF hkdf = KDF.getInstance("HKDF-SHA256");

Java 25 supports three variants: HKDF-SHA256, HKDF-SHA384, and HKDF-SHA512.

Full Extract-then-Expand (the most common pattern)

KDF hkdf = KDF.getInstance("HKDF-SHA256");
 
byte[] ikm  = sharedSecret;                      // input keying material
byte[] salt = secureRandomSalt;                  // recommended: 32 bytes for SHA-256
byte[] info = "myapp|v1|encryption".getBytes();  // context label
 
AlgorithmParameterSpec spec = HKDFParameterSpec.ofExtract()
    .addIKM(ikm)
    .addSalt(salt)
    .thenExpand(info, 32); // derive 32 bytes → AES-256 key
 
SecretKey aesKey = hkdf.deriveKey("AES", spec);

Deriving Multiple Independent Keys

A common pattern is to derive an encryption key and a MAC key from the same shared secret:

KDF hkdf = KDF.getInstance("HKDF-SHA256");
 
// Step 1: extract a pseudorandom key
byte[] prk = hkdf.deriveData(
    HKDFParameterSpec.ofExtract()
        .addIKM(sharedSecret)
        .addSalt(salt)
        .extractOnly()
);
SecretKey prkKey = new SecretKeySpec(prk, "Generic");
 
// Step 2: expand into two independent keys with different context labels
SecretKey encKey = hkdf.deriveKey("AES",
    HKDFParameterSpec.expandOnly(prkKey, "encryption-key".getBytes(), 32));
 
SecretKey macKey = hkdf.deriveKey("HmacSHA256",
    HKDFParameterSpec.expandOnly(prkKey, "authentication-key".getBytes(), 32));

The two keys are cryptographically independent — knowing one tells you nothing about the other.

When to Use deriveData vs deriveKey

// Use deriveKey when you need a SecretKey object (for use with Cipher, Mac, etc.)
SecretKey aesKey = hkdf.deriveKey("AES", spec);
 
// Use deriveData when you need raw bytes (for non-key purposes)
byte[] rawBytes = hkdf.deriveData(spec);

Where This Matters for Backend Developers

  • Deriving session keys from a Diffie-Hellman shared secret.
  • Key wrapping: deriving a key-encryption key from a master secret.
  • Protocol implementation that follows TLS 1.3 or Noise Protocol patterns.
  • Secure token generation where independent keys are needed per purpose.

Note on PBKDF2

PBKDF2 is not accessed via the new KDF class. It remains in SecretKeyFactory for backward compatibility. The KDF API is forward-looking toward modern algorithms like HKDF.

3. Stable Values (Preview)

JEP 502 introduces StableValue, a new lazy initialization primitive in the java.lang package. It is preview in Java 25, which means it requires --enable-preview and may still change before finalization.

The Problem

Java has several options for deferred initialization, and none of them are ideal:

  • static final fields: eagerly initialized at class-load time.
  • Double-checked locking with volatile: error-prone and prevents JIT constant-folding.
  • The "holder class" pattern: only works for static fields.

StableValue aims to give you lazy, thread-safe, at-most-once initialization that the JVM can treat similarly to a final field once set.

Core Concepts

A StableValue<T> is a container that can be set exactly once. After being set, the JVM can constant-fold reads of it the same way it does for final fields.

The most common entry point is the supplier factory, which wraps lazy initialization cleanly:

public class Component {
    // computed once on first access, then constant-foldable
    private final Supplier<Logger> logger =
        StableValue.supplier(() -> Logger.getLogger(Component.class));
 
    public void process() {
        logger.get().info("Process started");
    }
}

Using StableValue Directly

For cases where you need more control:

public class Component {
    private final StableValue<Config> config = StableValue.of();
 
    private Config getConfig() {
        return config.orElseSet(() -> Config.load());
        // orElseSet is thread-safe: supplier runs at most once
    }
}

Key methods on StableValue<T>:

T orElseSet(Supplier<? extends T> supplier) // compute and set if unset (at-most-once)
boolean trySet(T contents)                  // set if unset; returns false if already set
void setOrThrow(T contents)                 // set; throws if already set
T orElseThrow()                             // get; throws if not set
T orElse(T other)                           // get or default
boolean isSet()                             // check if set

Memoized Functions

StableValue also provides higher-level factory methods for common memoization patterns:

// Memoized IntFunction: caches results for inputs in [0, size)
IntFunction<Integer> powers =
    StableValue.intFunction(10, n -> 1 << n);
 
int four = powers.apply(2); // computed once, then cached
int eight = powers.apply(3);
 
// Lazy list: elements computed on first access
List<String> labels =
    StableValue.list(5, i -> "item-" + i);
 
// Lazy map: values computed on first access for a predefined key set
Map<String, Config> configs =
    StableValue.map(Set.of("dev", "staging", "prod"), env -> Config.load(env));

Recursive Memoization

Because the memoized functions know the full input range upfront, they support safe recursion:

final class Fibonacci {
    private static final int MAX = 46;
    private static final IntFunction<Integer> FIB =
        StableValue.intFunction(MAX, Fibonacci::fib);
 
    public static int fib(int n) {
        return n < 2 ? n : FIB.apply(n - 1) + FIB.apply(n - 2);
    }
}

Why It Is Still Preview

StableValue touches subtle JVM optimization territory. The JEP team wants more feedback on:

  • the exact API surface before finalizing it,
  • edge cases in the JIT constant-folding guarantees,
  • and how it composes with value classes from Project Valhalla.

So while the idea is compelling, treat this as something to evaluate and experiment with rather than a default production pattern.

The JVM Optimization Angle

The reason StableValue matters beyond just "lazy initialization" is the JVM treatment. When a StableValue stored in a static final field is set, the JIT can treat subsequent reads as if reading a final field — enabling the same constant-folding optimizations that final enjoys, without requiring the value to be set at construction time. That is the key difference from volatile.

4. PEM Encodings of Cryptographic Objects (Preview)

JEP 470 adds a standard PEM API to java.security. It is a preview feature in Java 25, so it requires --enable-preview.

What PEM Is

PEM (Privacy-Enhanced Mail) is the text encoding format you see everywhere in real security work:

-----BEGIN CERTIFICATE-----
MIIBkTCB+wIJ...
-----END CERTIFICATE-----

PEM files appear in TLS certificate chains, private key files, CSRs, and SSH keys. Despite being ubiquitous, Java had no standard API for reading or writing them. The common workaround was to use BouncyCastle or write manual Base64 stripping code.

The API

Two new classes, both immutable and thread-safe:

PEMEncoder — converts Java cryptographic objects to PEM text:

PEMEncoder encoder = PEMEncoder.of();
 
// Encode a private key
String privateKeyPem = encoder.encodeToString(keyPair.getPrivate());
 
// Encode a public key
String publicKeyPem = encoder.encodeToString(keyPair.getPublic());
 
// Encode a certificate
String certPem = encoder.encodeToString(x509Cert);
 
// Encrypt a private key with a password
PEMEncoder encryptingEncoder = PEMEncoder.of().withEncryption("passphrase".toCharArray());
String encryptedPem = encryptingEncoder.encodeToString(keyPair.getPrivate());

PEMDecoder — converts PEM text back to Java objects:

PEMDecoder decoder = PEMDecoder.of();
 
// Typed decode — you know what type to expect
PublicKey pub = decoder.decode(pemText, PublicKey.class);
PrivateKey priv = decoder.decode(privPemText, PrivateKey.class);
X509Certificate cert = decoder.decode(certPemText, X509Certificate.class);
 
// Decrypt an encrypted private key
PEMDecoder decryptingDecoder = PEMDecoder.of().withDecryption("passphrase".toCharArray());
PrivateKey key = decryptingDecoder.decode(encryptedPem, PrivateKey.class);

Auto-Detection with Pattern Matching

If you do not know the type upfront:

DEREncodable obj = PEMDecoder.of().decode(unknownPem);
 
switch (obj) {
    case X509Certificate cert ->
        System.out.println("Subject: " + cert.getSubjectX500Principal());
    case PrivateKey key ->
        System.out.println("Algorithm: " + key.getAlgorithm());
    case PublicKey key ->
        System.out.println("Public key type: " + key.getAlgorithm());
    case PEMRecord record ->
        System.out.println("Unknown PEM type: " + record.type());
}

What Types Are Supported

PEM headerJava type
CERTIFICATEX509Certificate
X509 CRLX509CRL
PUBLIC KEYPublicKey
PRIVATE KEYPrivateKey
ENCRYPTED PRIVATE KEYEncryptedPrivateKeyInfo (or PrivateKey with decryption)
Unknown typesPEMRecord

Why This Matters

Loading an OpenSSL-generated RSA private key was previously two to three libraries or fifty lines of manual Base64 parsing. The new API reduces it to one method call, using only the standard library. For developers building TLS-aware systems, custom certificate handling, or key management tooling, this is a significant quality-of-life improvement.

5. Vector API (Tenth Incubator)

JEP 508 is the tenth incubator iteration of the Vector API. It is accessed via the jdk.incubator.vector module and requires --add-modules jdk.incubator.vector.

The Vector API provides a way to express SIMD (Single Instruction, Multiple Data) computations that compile to hardware vector instructions like AVX2 and AVX-512 on x64, or NEON and SVE on ARM.

Why It Is Still Incubating

The Vector API is deliberately held back from preview status because it needs Project Valhalla. Vector types should be value types — stack-allocated, no object identity, no null — to reach their full performance potential. Until Valhalla's value class support becomes available, the final API design cannot be locked down.

What Changed in the Tenth Iteration

Three meaningful additions in Java 25:

  1. VectorShuffle supports MemorySegment: You can load and store shuffle patterns directly to and from off-heap memory, enabling tighter integration with BLAS-style routines.
  2. Implementation uses the FFM API: The internal implementation now links to native math libraries via the Foreign Function & Memory API instead of custom C++ JVM code.
  3. Float16 auto-vectorization: Addition, subtraction, multiplication, division, and fused multiply-add on Float16 values are now auto-vectorized on x64 CPUs that support it, which is useful for ML inference workloads.

How It Works

The core abstraction is a VectorSpecies, which combines an element type with a vector width:

import jdk.incubator.vector.*;
 
// SPECIES_PREFERRED picks the widest vector the JVM can use efficiently
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;

On a CPU with AVX-512, SPECIES_PREFERRED may process 16 floats per instruction. On AVX2, it is 8. On a CPU with no vector support, it falls back to scalar.

SIMD Array Multiplication

static float[] vectorMultiply(float[] a, float[] b) {
    float[] result = new float[a.length];
    int i = 0;
    int upperBound = SPECIES.loopBound(a.length);
 
    // vectorized loop — processes SPECIES.length() elements per iteration
    for (; i < upperBound; i += SPECIES.length()) {
        FloatVector va = FloatVector.fromArray(SPECIES, a, i);
        FloatVector vb = FloatVector.fromArray(SPECIES, b, i);
        va.mul(vb).intoArray(result, i);
    }
 
    // scalar tail
    for (; i < a.length; i++) {
        result[i] = a[i] * b[i];
    }
    return result;
}

Dot Product with Fused Multiply-Add

static float dotProduct(float[] a, float[] b) {
    FloatVector sum = FloatVector.zero(SPECIES);
    int i = 0;
    int upperBound = SPECIES.loopBound(a.length);
 
    for (; i < upperBound; i += SPECIES.length()) {
        FloatVector va = FloatVector.fromArray(SPECIES, a, i);
        FloatVector vb = FloatVector.fromArray(SPECIES, b, i);
        sum = va.fma(vb, sum); // fused multiply-add: sum += va * vb
    }
 
    float result = sum.reduceLanes(VectorOperators.ADD);
    for (; i < a.length; i++) result += a[i] * b[i];
    return result;
}

Masked Operations

Masks let you apply an operation conditionally per lane:

// ReLU: clamp negative values to 0
static float[] relu(float[] input) {
    float[] output = new float[input.length];
    FloatVector zero = FloatVector.zero(SPECIES);
    int i = 0;
    for (; i < SPECIES.loopBound(input.length); i += SPECIES.length()) {
        FloatVector v = FloatVector.fromArray(SPECIES, input, i);
        v.max(zero).intoArray(output, i);
    }
    for (; i < input.length; i++) output[i] = Math.max(input[i], 0);
    return output;
}

Who Should Care

The Vector API is most relevant for:

  • numerical computing and scientific workloads,
  • ML inference (matrix multiply, embedding lookups),
  • audio and video signal processing,
  • image filters,
  • full-text search scoring functions.

For typical CRUD backend applications, the Vector API is not relevant yet. But for performance-focused Java work in data or ML contexts, it is worth learning even in its incubator state.

Other Library Changes Worth Noting

New Algorithms in MessageDigest

Java 25 adds two new MessageDigest algorithms:

  • SHAKE128-256 — the 256-bit fixed-length variant of SHAKE128 (NIST FIPS 202)
  • SHAKE256-512 — the 512-bit fixed-length variant of SHAKE256 (NIST FIPS 202)

These are extended output functions (XOFs) from the SHA-3 family, used in some modern cryptographic protocols.

TLS 1.2 Hardening

SHA-1 is now disabled by default in TLS 1.2 handshake signatures. This brings Java's default TLS configuration closer to current security recommendations. Most modern deployments will not notice, but applications connecting to legacy systems that still use SHA-1 in TLS handshakes may need to explicitly re-enable it.

TLS Keying Material Exporters

ExtendedSSLSession gains two new methods:

SecretKey exportKeyingMaterialKey(String label, byte[] context, int length);
byte[] exportKeyingMaterialData(String label, byte[] context, int length);

These implement RFC 5705 (Keying Material Exporters for TLS), which some protocol implementations need for deriving shared secrets tied to a specific TLS session.

IO Helper Class

As part of JEP 512 (Compact Source Files), a new java.lang.IO class is available:

IO.println("Hello, World!");
IO.print("no newline");
String line = IO.readln("Enter your name: ");

In compact source files, IO is automatically imported. In regular class files, it is accessible as java.lang.IO. This is a small convenience, mostly relevant for examples and scripts.

Which APIs Should You Evaluate First?

If you want a practical first pass through the Java 25 library additions:

  1. Scoped Values if your team is adopting virtual threads and wants to replace ThreadLocal for request-scoped context.
  2. Key Derivation Function API if you work on any security-sensitive system that needs HKDF — this is immediately useful and stable.
  3. PEM Encodings if you deal with certificates, keys, or TLS configuration and want to remove BouncyCastle from your dependency list for that use case. Note it is still preview.
  4. Stable Values if you care about lazy initialization performance, especially startup time. Track it, but wait for finalization before making it a team standard.
  5. Vector API only if you are doing numerical or ML-adjacent work and are comfortable working with an incubator feature.

Final Thoughts

The new API story in Java 25 follows a clear pattern: filling long-standing gaps in the standard library while continuing to evolve concurrency and security primitives.

Scoped Values and the Key Derivation Function API are the headline additions because they are both finalized and immediately applicable. They solve real problems that Java developers have been working around with external libraries or awkward patterns.

Stable Values and PEM Encodings are compelling ideas that need one more iteration before they are ready for standard production use. Both are worth learning now.

Vector API continues to mature steadily, waiting for Project Valhalla to unblock its final design.

Taken together, the Java 25 standard library additions continue the platform's gradual but consistent movement toward being more complete out of the box.

In the next post in this series, we will look at the deprecations and removals in Java 25 — the features and APIs that are being phased out, and what you should do if your code depends on them.

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.