What's new in Java 26 (for Developers)
Java 26 arrives with pattern matching hitting its stride, concurrency moving toward finalization, and pragmatic APIs for startup optimization and cryptography. After the heavy language work in Java 25, Java 26 focuses on hardening existing previews and delivering missing library primitives.
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.

Note: Java 26 is not an LTS release. It receives updates for 6 months (until September 2026). For long-term support, use Java 21 LTS or Java 25 LTS.
Legend
- [FINAL] — stable, production-ready
- [PREVIEW] — requires
--enable-preview- [INCUBATOR] — experimental, may change significantly
Java 26 highlights
- [PREVIEW] Primitive types in patterns anywhere (JEP 530 – fourth preview, landing soon)
- [PREVIEW] Structured concurrency maturity (JEP 525 – sixth preview, approaching final)
- [PREVIEW] Lazy constants for startup boost (JEP 526 – second preview)
- [PREVIEW] PEM cryptography APIs (JEP 524 – second preview)
- [FINAL] AOT cache with any GC – including ZGC (JEP 516)
- [FINAL] HTTP/3 for the HTTP Client (JEP 517)
- [FINAL] G1 GC throughput gains (JEP 522)
- [FINAL] Final fields getting teeth (JEP 500 – warnings today, denials later)
- [INCUBATOR] Vector compute API (JEP 529 – eleventh incubator)
Language & pattern matching
JEP 530: Primitive types in patterns, instanceof, and switch [PREVIEW]
What it is: Pattern matching now works uniformly across all primitive types. You can use instanceof int x, bind narrowed values safely in switch, and check conversions without silent loss.
When you’ll use it: JSON parsing with type coercion, safe numeric range checks, expressing business rules with guards.
Before (manual casting, repetitive type checks):
// BEFORE (Java 25)
public class PriceAnalyzer {
static String category(Object price) {
if (price instanceof Integer) {
int p = (Integer) price;
return p < 100 ? "BUDGET" : p < 500 ? "MID" : "PREMIUM";
}
if (price instanceof Double) {
double d = (Double) price;
// Silent truncation risk if cast to int
if (d > 1000.0) return "PREMIUM";
return "MID";
}
if (price instanceof Long) {
long l = (Long) price;
return l > 10_000 ? "PREMIUM" : "BUSINESS";
}
return "UNKNOWN";
}
}
After (primitive patterns with safe narrowing):
// src/main/java/com/loiane/pricing/Category.java
package com.loiane.pricing;
public class PriceAnalyzer {
static String category(Object price) {
return switch (price) {
case int p when p < 100 -> "BUDGET";
case int p when p < 500 -> "MID";
case int p -> "PREMIUM";
case long l when l > 10_000 -> "PREMIUM";
case long l -> "BUSINESS";
// Only bind d to int x if conversion is exact (no silent loss)
case double d when d instanceof int x ->
"EXACT_INT: " + category(x);
case double d when d > 1000.0 -> "PREMIUM";
case double d -> "MID";
default -> "UNKNOWN";
};
}
public static void main(String[] args) {
System.out.println(category(50)); // BUDGET
System.out.println(category(300)); // MID
System.out.println(category(800)); // PREMIUM
System.out.println(category(15_000L)); // PREMIUM
System.out.println(category(500.0)); // EXACT_INT
System.out.println(category(500.5)); // MID
}
}
What’s happening here?
case int pbinds exactly to int values;LongandDoublehave their own cases.d instanceof int xchecks if doubledconverts exactly to int (no precision loss); only then bind it.- Guards (
when) let you express range logic inline.
Run (preview):
javac --enable-preview --release 26 -d out src/main/java/com/loiane/pricing/Category.java
java --enable-preview -cp out com.loiane.pricing.PriceAnalyzer
Concurrency at scale
JEP 525: Structured concurrency [PREVIEW]
What it is: Task-based concurrency with automatic cancellation and error propagation. Fork related subtasks, join them as a unit; if one fails, the rest are cancelled automatically.
When you’ll use it: Web request handlers, parallel I/O operations, workflows with clear success/failure boundaries.
Before (manual cancellation, thread leaks on failure, complex error handling):
// BEFORE (Java 25, using ExecutorService)
public class OrderService {
static class OrderData {
String customer;
List<String> items;
String address;
}
static OrderData fetchOrder(ExecutorService executor) throws Exception {
Future<String> customerFuture = executor.submit(() -> {
Thread.sleep(100); // simulate I/O
return "Alice";
});
Future<List<String>> itemsFuture = executor.submit(() -> {
Thread.sleep(150);
return List.of("Laptop", "Mouse");
});
Future<String> addressFuture = executor.submit(() -> {
Thread.sleep(200);
return "123 Main St";
});
// Problem: if customerFuture.get() throws, itemsFuture and addressFuture
// keep running (thread leak). Manual cancellation needed everywhere.
String customer = customerFuture.get();
List<String> items = itemsFuture.get();
String address = addressFuture.get();
return new OrderData(customer, items, address);
}
}
After (structured, automatic error handling and cancellation):
// src/main/java/com/loiane/orders/OrderService.java
package com.loiane.orders;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Joiner;
import java.util.List;
public class OrderService {
static class OrderData {
String customer;
List<String> items;
String address;
OrderData(String customer, List<String> items, String address) {
this.customer = customer;
this.items = items;
this.address = address;
}
}
static OrderData fetchOrder() throws Exception {
try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())) {
var customerTask = scope.fork(() -> {
Thread.sleep(100);
return "Alice";
});
var itemsTask = scope.fork(() -> {
Thread.sleep(150);
return List.of("Laptop", "Mouse");
});
var addressTask = scope.fork(() -> {
Thread.sleep(200);
return "123 Main St";
});
scope.join(); // Wait for all; if any fails, cancel others automatically
return new OrderData(
customerTask.get(),
itemsTask.get(),
addressTask.get()
);
}
// Scope closed: all subtasks guaranteed to have finished
}
public static void main(String[] args) throws Exception {
var order = fetchOrder();
System.out.println("Customer: " + order.customer);
System.out.println("Items: " + order.items);
System.out.println("Address: " + order.address);
}
}
What’s happening here?
StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())creates a scope where all subtasks must succeed.fork()launches each async task (virtual thread, by default in Java 21+).join()waits for all subtasks; if any fail, others are cancelled immediately.try-with-resources ensures the scope closes and cleanup happens.
Run (preview):
javac --enable-preview --release 26 -d out src/main/java/com/loiane/orders/OrderService.java
java --enable-preview -cp out com.loiane.orders.OrderService
Startup optimization
JEP 526: Lazy constants [PREVIEW]
What it is: Deferred immutability. Create a LazyConstant<T> that initializes on first access, with at-most-once semantics even under concurrent calls. The JVM treats it as a true constant after initialization, enabling optimizations.
When you’ll use it: Application startup—defer expensive initialization (loggers, repositories, config) until needed.
Before (eager initialization or double-checked locking, both problematic):
// BEFORE
public class Application {
// Eager: logger created even if app never logs
private static final Logger LOGGER = Logger.getLogger("com.app");
// Or: double-checked locking (verbose, not constant-folded)
private static volatile Logger logger;
static Logger getLogger() {
if (logger == null) {
synchronized (Application.class) {
if (logger == null) {
logger = Logger.getLogger("com.app");
}
}
}
return logger;
}
}
After (lazy constant with performance of eager):
// src/main/java/com/loiane/app/Application.java
package com.loiane.app;
import java.lang.LazyConstant;
import java.util.logging.Logger;
public class Application {
// Initialize lazily on first call; JVM constant-folds after init
private static final LazyConstant<Logger> LOGGER =
LazyConstant.of(() -> Logger.getLogger("com.loiane.app"));
static void logInfo(String msg) {
LOGGER.get().info(msg);
}
public static void main(String[] args) {
logInfo("App started"); // First call: logger created
logInfo("Processing"); // Subsequent: instant (constant-folded)
}
}
For lists, use List.ofLazy():
// src/main/java/com/loiane/app/Pool.java
package com.loiane.app;
import java.util.List;
public class Pool {
static class Worker {
String name;
Worker(String name) { this.name = name; }
}
// Pool of 10 workers, each created on first access
static final List<Worker> WORKERS = List.ofLazy(10,
index -> new Worker("worker-" + index));
public static void main(String[] args) {
System.out.println(WORKERS.get(0).name); // Lazy init
System.out.println(WORKERS.get(0).name); // Instant (cached)
}
}
What’s happening here?
LazyConstant.of(supplier)creates a wrapper; calls supplier on first.get(), never again.List.ofLazy(size, indexedSupplier)creates a fixed-size list with lazy elements.- The JVM optimizes accesses after initialization is complete.
Run (preview):
javac --enable-preview --release 26 -d out src/main/java/com/loiane/app/*.java
java --enable-preview -cp out com.loiane.app.Application
Cryptography & security
JEP 524: PEM encodings of cryptographic objects [PREVIEW]
What it is: Standard APIs for PEM encoding/decoding of keys, certificates, and CRLs. Encode to PEM for storage/transport; decode from PEM back to Java crypto objects. Optionally encrypt private keys.
When you’ll use it: Key management, certificate loading, securely sharing keys between systems.
Before (manual Base64, boilerplate, no standard):
// BEFORE (Java 25)
import java.security.KeyPairGenerator;
import java.security.KeyPair;
import java.util.Base64;
import java.nio.file.*;
public class KeyMgmt {
static void exportKey(KeyPair kp) throws Exception {
byte[] privateKeyBytes = kp.getPrivate().getEncoded();
String base64 = Base64.getEncoder().encodeToString(privateKeyBytes);
StringBuilder pem = new StringBuilder();
pem.append("-----BEGIN PRIVATE KEY-----\n");
// Manually format to 64-char lines
for (int i = 0; i < base64.length(); i += 64) {
int end = Math.min(i + 64, base64.length());
pem.append(base64, i, end).append("\n");
}
pem.append("-----END PRIVATE KEY-----\n");
Files.writeString(Path.of("key.pem"), pem);
}
}
After (clean PEM encoder/decoder with optional encryption):
// src/main/java/com/loiane/crypto/KeyMgmt.java
package com.loiane.crypto;
import java.security.*;
import java.security.spec.*;
import java.nio.file.*;
import java.security.PEMEncoder;
import java.security.PEMDecoder;
public class KeyMgmt {
// Generate a keypair
static KeyPair generateKeyPair() throws Exception {
var gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(2048);
return gen.generateKeyPair();
}
// Export keypair to PEM (no encryption)
static void exportKeyPair(KeyPair kp, Path path) throws Exception {
String pem = PEMEncoder.of().encodeToString(kp);
Files.writeString(path, pem);
System.out.println("Exported: " + path.getFileName());
}
// Export private key with password encryption
static void exportEncrypted(KeyPair kp, char[] password, Path path)
throws Exception {
var encoder = PEMEncoder.of().withEncryption(password);
String pem = encoder.encodeToString(kp.getPrivate());
Files.writeString(path, pem);
System.out.println("Exported encrypted: " + path.getFileName());
}
// Import from PEM
static KeyPair importKeyPair(Path path, char[] password) throws Exception {
String pem = Files.readString(path);
var decoder = PEMDecoder.of().withDecryption(password);
// Decoder infers type from PEM header
return (KeyPair) decoder.decode(pem);
}
public static void main(String[] args) throws Exception {
var kp = generateKeyPair();
exportKeyPair(kp, Path.of("public-private.pem"));
exportEncrypted(kp, "secret123".toCharArray(), Path.of("private-encrypted.pem"));
// Re-import
var restored = importKeyPair(
Path.of("private-encrypted.pem"),
"secret123".toCharArray()
);
System.out.println("Restored private key: " + restored.getPrivate().getAlgorithm());
}
}
What’s happening here?
PEMEncoder.of()creates an encoder;.encodeToString()returns PEM text directly.withEncryption(password)enables password-based encryption for private keys.PEMDecoder.of().withDecryption(password)auto-detects key type from PEM header; decrypts if needed.
Run (preview):
javac --enable-preview --release 26 -d out src/main/java/com/loiane/crypto/KeyMgmt.java
java --enable-preview -cp out com.loiane.crypto.KeyMgmt
JEP 500: Prepare to make final mean final [FINAL]
What it is: Deep reflection (e.g., Field.set() on final fields) now issues a warning. In a future Java release, it will throw an exception by default. This paves the way for true immutability and JVM optimizations.
When you’ll use it: Review warnings in your logs and libraries. Use --enable-final-field-mutation=ALL-UNNAMED in production if you must, but plan to refactor.
Code (detect final field mutation):
// src/main/java/com/loiane/finals/FinalTest.java
package com.loiane.finals;
import java.lang.reflect.Field;
public class FinalTest {
static class Config {
final String env = "prod";
}
public static void main(String[] args) throws Exception {
Config cfg = new Config();
System.out.println("Before: " + cfg.env);
// Reflection: mutate the final field
Field f = Config.class.getDeclaredField("env");
f.setAccessible(true);
f.set(cfg, "dev"); // Warning issued in Java 26
System.out.println("After: " + cfg.env); // Prints "dev"
}
}
Run and watch the warning:
javac -d out src/main/java/com/loiane/finals/FinalTest.java
java -cp out com.loiane.finals.FinalTest
# Output:
# WARNING: Final field env in class com.loiane.finals.FinalTest$Config
# has been mutated by class com.loiane.finals.FinalTest.main ...
If you need to suppress the warning (temporarily):
java --illegal-final-field-mutation=allow -cp out com.loiane.finals.FinalTest
But plan to migrate serialization libraries to sun.reflect.ReflectionFactory instead.
Performance & GC
[FINAL]
JEP 516: Ahead-of-time object caching with any GC [FINAL]
What it is: AOT cache now works with ZGC, not just G1/Serial/Parallel. Uses a GC-agnostic format (logical indices) so the same cache works across GCs.
When you’ll use it: Container cold-starts. Train with any GC; use in production with Z, G1, or anything else.
Workflow:
# Training: collect class-loading and object data
java -XX:AOTCacheOutput=app.aot -jar yourapp.jar
# Production: reuse the same cache with ZGC
java -XX:+UseZGC -XX:AOTCache=app.aot -jar yourapp.jar
# or with G1 (the default)
java -XX:AOTCache=app.aot -jar yourapp.jar
Spring Boot example (from JDK docs): 41% startup improvement on PetClinic (21,000 classes pre-loaded).
JEP 522: G1 GC: improve throughput by reducing synchronization [FINAL]
What it is: G1’s write barriers simplified; dual card table removes contention between app and GC optimizer threads. Net: 5–15% throughput gain on object-heavy workloads; pause times slightly lower.
When you’ll use it: Nothing to do! Improvements are automatic. Monitor with -XX:+PrintGCDetails if interested.
Benchmark snippet (from release notes):
# Before: heavy mutation → GC thread contention
java -XX:+UseG1GC -XX:+PrintGCDetails -jar cpu-intensive-app.jar
# After Java 26: same flags, lower GC overhead
java -XX:+UseG1GC -XX:+PrintGCDetails -jar cpu-intensive-app.jar
# Observe: fewer GC pauses, higher throughput
JEP 517: HTTP/3 for the HTTP Client API [FINAL]
What it is: HttpClient now supports HTTP/3 (over QUIC). Benefits: faster handshakes, no head-of-line blocking, resilient in high-loss networks. Opt-in; defaults to HTTP/2 for compatibility.
When you’ll use it: When serving from HTTP/3-capable endpoints (modern CDNs, cloud load balancers). Backward-compatible; existing code unchanged.
Code (explicit HTTP/3 opt-in):
// src/main/java/com/loiane/http/Http3Client.java
package com.loiane.http;
import java.net.http.*;
import java.net.URI;
public class Http3Client {
public static void main(String[] args) throws Exception {
// Default HTTP/2 client
var client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();
var req = HttpRequest.newBuilder(
URI.create("https://example.com/api/data"))
.GET()
.build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + resp.statusCode());
System.out.println("Body: " + resp.body());
// HTTP/3 client: opt-in
var h3Client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_3)
.build();
var h3Resp = h3Client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP/3 Status: " + h3Resp.statusCode());
// If server doesn't support HTTP/3, transparently falls back to HTTP/2
}
}
Vector compute (early incubation)
JEP 529: Vector API [INCUBATOR]
What it is: Express vector operations (SIMD) that compile reliably to AVX, NEON, or SVE. Build on VectorSpecies to define shape/size; HotSpot C2 compiles to hardware vector instructions.
When you’ll use it: Machine learning, cryptography, numeric computing, array hashing. Not ready for general use yet (still incubating, awaiting Project Valhalla); used in JDK internals.
Code sketch (vector addition):
// src/main/java/com/loiane/compute/VectorSum.java
package com.loiane.compute;
import jdk.incubator.vector.*;
public class VectorSum {
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
static float[] addVectors(float[] a, float[] b) {
float[] c = new float[a.length];
int i = 0;
// Vectorized loop
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);
FloatVector vc = va.add(vb);
vc.intoArray(c, i);
}
// Scalar tail
for (; i < a.length; i++) {
c[i] = a[i] + b[i];
}
return c;
}
public static void main(String[] args) {
float[] a = {1.0f, 2.0f, 3.0f, 4.0f};
float[] b = {5.0f, 6.0f, 7.0f, 8.0f};
float[] c = addVectors(a, b);
for (float v : c) System.out.println(v);
}
}
Compile (add-modules for incubator):
javac --add-modules jdk.incubator.vector -d out \
src/main/java/com/loiane/compute/VectorSum.java
java --add-modules jdk.incubator.vector -cp out com.loiane.compute.VectorSum
On CPUs supporting AVX-512 (e.g., Xeon), HotSpot generates vectorized instructions; graceful fallback on older CPUs.
Platform cleanup
JEP 504: Remove the Applet API [FINAL]
What it is: java.applet, javax.swing.JApplet, and related classes completely removed. Applets haven’t worked in browsers for 10+ years.
Remember banking apps in the early 2000s? All powered by Applets!
When you’re affected: Only if you maintain legacy code using applets. Migration path:
- UI containers: Use AWT components or Swing directly.
- Audio playback: Use
javax.sound.SoundClip(added in Java 25).
If you see compile errors, you’re modernizing. This is progress.
What you can start using today
Here’s a pragmatic roadmap for adopting Java 26 features in your projects:
Production-ready (no flags required) [FINAL]
Start using these immediately in any Java 26 project:
HTTP/3 support (JEP 517): Add a single line to opt-in; existing code unaffected. Zero risk.
var client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_3) .build();AOT cache with any GC (JEP 516): Improves cold-start by 30–40%. Add a training step in your CI; push
app.aotto production images. Pure win.java -XX:AOTCacheOutput=app.aot -jar app.jar --train java -XX:AOTCache=app.aot -jar app.jar # ProductionG1 GC improvements (JEP 522): Automatic. Nothing to do; enjoy 5–15% throughput gain on object-heavy workloads.
Final field warnings (JEP 500): Review warnings in logs; coordinate with dependency upgrades. No immediate action needed unless using deep reflection on final fields.
Safe to adopt in dev/test [PREVIEW]
These features are stable and landing soon. Use in development and testing; plan for production when finalized:
- Primitive type patterns (JEP 530): Fourth preview; clear path to finalization. Adopt in new code and refactor eager codebases:
if (value instanceof int x) { ... } switch (price) { case long l when l > 10_000 -> "PREMIUM"; ... }Add to build pipeline:
javac --enable-preview --release 26. - Structured concurrency (JEP 525): Sixth preview; proven in real applications. Use for all new concurrent code:
try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())) { var t1 = scope.fork(() -> ...); var t2 = scope.fork(() -> ...); scope.join(); return new Result(t1.get(), t2.get()); } Lazy constants (JEP 526): Second preview; stable API. Use to optimize application startup:
static final LazyConstant<Logger> LOGGER = LazyConstant.of(() -> Logger.getLogger("app"));PEM encodings (JEP 524): Second preview; straightforward. Use for cryptographic key/cert handling:
var kp = KeyPairGenerator.getInstance("RSA").generateKeyPair(); String pem = PEMEncoder.of().encodeToString(kp);
Experimental (incubators) — wait for finalization
- Vector API (JEP 529): Eleventh incubator; depends on Project Valhalla. Not recommended for production use yet. Monitor for graduation in Java 27/28.
Migration checklist
If you’re an early adopter targeting Java 26:
Enable previews in your build:
<!-- Maven --> <release>26</release> <args> <arg>--enable-preview</arg> </args>- Update CI/CD:
- Download JDK 26 GA.
- Set up AOT cache generation in training steps (optional but recommended).
- Test with
--enable-previewto catch deprecations early.
- Refactor incrementally:
- New code: use primitive patterns, structured concurrency, lazy constants.
- Legacy code: adopt as time permits; these features are not disruptive.
- Plan for LTS migration:
- Java 26 = 6 months of updates (until September 2026).
- or stay on Java 21 LTS or Java 25 LTS until then.
Notes
- Preview and incubator features require
--enable-previewand/or--add-modules. - Primitive patterns (JEP 530) are on the homestretch: fourth preview suggests finalization in Java 27 or 28.
- Structured concurrency (JEP 525) is similarly mature; plan to use it in new concurrent code today.
- Lazy constants and PEM encodings are in refinement phase; minor API tweaks possible before final.
- Vector API will graduate from incubator once Project Valhalla delivers value classes; not yet recommended for production outside the JDK itself.
- Final field restrictions are a multi-release journey: warnings in 26, denials in a future release. Plan migrations now.
References
Happy Coding!
