Java 26 arrives in March 2026 as the latest feature release in the six-month cadence. It is not an LTS release (that honour belongs to JDK 25), but it pushes the language and platform further toward a cleaner, more expressive Java. This article walks through every noteworthy change, shows real code, and gives you a practical guide for applying JDK 26 safely on your enterprise projects.
Table of New Features
| # | Feature | JEP | Status | Impact |
|---|---|---|---|---|
| 1 | Primitive Types in Patterns, instanceof, and switch | JEP 488 | Preview (3rd) | High |
| 2 | Flexible Constructor Bodies | JEP 492 | Finalized | Medium |
| 3 | Module Import Declarations | JEP 494 | Finalized | Medium |
| 4 | Unnamed Classes and Instance Main Methods | JEP 495 | Finalized | High |
| 5 | Null-Restricted and Nullable Value Types | JEP 496 | Preview (1st) | High |
| 6 | Value Classes | JEP 497 | Preview (1st) | High |
| 7 | Structured Concurrency | JEP 499 | 4th Preview | Medium |
| 8 | Scoped Values | JEP 487 | Finalized | Medium |
| 9 | Vector API | JEP 489 | 9th Incubator | Low–Medium |
| 10 | Stream Gatherers | JEP 485 | Finalized (since JDK 24) | Medium |
| 11 | Ahead-of-Time Class Loading & Linking | JEP 483 | Finalized (since JDK 24) | Low |
| 12 | Deprecations & Removals | — | Various | Varies |
Legend: Finalized — production-ready, no
--enable-previewneeded. Preview — requires--enable-preview; the API/semantics may still change.
Feature Deep-Dives with Code Examples
1. Primitive Types in Patterns (JEP 488 — 3rd Preview)
Pattern matching now supports all primitive types, closing the gap that existed since instanceof patterns were introduced. This eliminates tedious manual casting and improves type safety in switch expressions.
// Before JDK 26 — manual branching and casting
Object value = getSensorReading(); // returns Integer, Long, Double, or String
if (value instanceof Integer i) {
System.out.println("int sensor: " + i);
} else if (value instanceof Long l) {
System.out.println("long sensor: " + l);
} else if (value instanceof Double d) {
System.out.println("double sensor: " + d);
} else {
System.out.println("unknown: " + value);
}
// JDK 26 — primitive patterns in switch (--enable-preview)
String description = switch (getSensorReading()) {
case int i -> "integer reading: %d".formatted(i);
case long l -> "long reading: %d".formatted(l);
case double d -> "double reading: %.3f".formatted(d);
case String s -> "label: " + s;
default -> "unrecognised";
};
System.out.println(description);
Numeric range guards work naturally alongside primitive patterns:
int statusCode = fetchHttpStatus(); // primitive int
String category = switch (statusCode) {
case int c when c >= 100 && c < 200 -> "Informational";
case int c when c >= 200 && c < 300 -> "Success";
case int c when c >= 300 && c < 400 -> "Redirection";
case int c when c >= 400 && c < 500 -> "Client Error";
case int c when c >= 500 -> "Server Error";
default -> "Unknown";
};
instanceof with primitives replaces > 0-style checks in validation:
// Compile-time narrowing check — only matches when value fits in int
if (longValue instanceof int i) {
processIntRange(i); // safe, i is already narrowed
}
2. Flexible Constructor Bodies (JEP 492 — Finalized)
Constructors can now execute statements before the explicit super() or this() call, as long as those statements do not access the instance being constructed. This removes a longstanding restriction that forced awkward static helper methods.
// Before JDK 26 — workaround via static factory
public class PositiveInteger extends Number {
private final int value;
// Previously you had to move validation into a static helper
private static int validated(int v) {
if (v <= 0) throw new IllegalArgumentException("Must be positive: " + v);
return v;
}
public PositiveInteger(int value) {
super(validated(value)); // static helper required
this.value = value;
}
}
// JDK 26 — statements allowed before super()
public class PositiveInteger extends Number {
private final int value;
public PositiveInteger(int value) {
if (value <= 0) { // ✅ runs before super()
throw new IllegalArgumentException("Must be positive: " + value);
}
super(value);
this.value = value;
}
}
Builder-style delegation also benefits:
public class AuditedList<E> extends ArrayList<E> {
private final String owner;
public AuditedList(String owner, Collection<? extends E> initial) {
// Log before delegating — no static helper needed
AuditLog.record("Creating list for " + owner + " with " + initial.size() + " items");
super(initial);
this.owner = owner;
}
}
3. Module Import Declarations (JEP 494 — Finalized)
A single import module statement imports all exported packages of a module, reducing boilerplate at the top of files that rely heavily on a single module.
// Before — verbose single-type imports
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
// JDK 26 — single module import
import module java.base;
public class OrderProcessor {
public Map<String, List<String>> groupByCategory(Stream<Order> orders) {
return orders.collect(
Collectors.groupingBy(
Order::category,
Collectors.mapping(Order::id, Collectors.toList())
)
);
}
}
Note: On-demand imports (
import module) have lower precedence than single-type imports, so you can always override a specific class without conflict.
4. Unnamed Classes and Instance Main Methods (JEP 495 — Finalized)
The “launch protocol” is now fully streamlined. Small programs and scripts no longer need a surrounding class declaration or a public static void main(String[] args) signature.
// Traditional entry point — still valid
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, JDK 26!");
}
}
// JDK 26 — unnamed class with instance main
void main() {
System.out.println("Hello, JDK 26!");
}
Script-style data pipeline — ideal for tooling and automation:
import module java.base;
void main() {
var result = Stream.of("Alice", "Bob", "Charlie")
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.toList();
result.forEach(System.out::println);
}
Top-level fields and helpers are supported in unnamed classes:
import module java.base;
static final int MAX_RETRIES = 3;
void main() {
for (int i = 0; i < MAX_RETRIES; i++) {
if (tryConnect()) break;
System.out.println("Retry " + (i + 1));
}
}
boolean tryConnect() {
// ... connection logic ...
return false;
}
5. Null-Restricted and Nullable Value Types (JEP 496 — 1st Preview)
This is the first step toward Project Valhalla’s null safety story. You can annotate a value type to declare whether null is permissible at the type level.
// Requires --enable-preview
// Null-restricted: the field can never be null
value class Point {
double! x; // '!' means null-restricted
double! y;
Point(double x, double y) {
this.x = x;
this.y = y;
}
}
// Nullable: can hold null — explicitly opted in
Point? maybeOrigin = null; // '?' means nullable
Point! origin = new Point(0.0, 0.0); // never null
// Point! bad = null; // compile-time error
API design with null-restricted types:
public interface CoordinateService {
Point! findById(long id); // guaranteed non-null result
Point? findByName(String name); // may return null (not found)
}
Status: First preview — expect the syntax and exact semantics to evolve before finalisation.
6. Value Classes (JEP 497 — 1st Preview)
Value classes are the foundation of Project Valhalla. They have no identity (no == reference equality, no synchronized) and can be inlined by the JVM for flat memory layouts — eliminating object header overhead in arrays and generics.
// Requires --enable-preview
value class Money {
private final long amount; // in cents
private final String currency;
public Money(long amount, String currency) {
this.amount = amount;
this.currency = Objects.requireNonNull(currency);
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount + other.amount, this.currency);
}
public String formatted() {
return "%s %.2f".formatted(currency, amount / 100.0);
}
}
// Usage
Money price = new Money(1999, "USD");
Money tax = new Money(160, "USD");
Money total = price.add(tax);
System.out.println(total.formatted()); // USD 21.59
Difference from records: records still have identity; value classes do not. Use value classes when you want JVM-level inlining and never need == or synchronized.
// Record — has identity
record Coordinate(double x, double y) {}
// Value class — no identity, JVM may inline in arrays
value class Coordinate2D {
double x;
double y;
}
// In large numerical arrays, Coordinate2D[] avoids boxing overhead
Coordinate2D[] grid = new Coordinate2D[1_000_000];
7. Structured Concurrency (JEP 499 — 4th Preview)
Still in preview but increasingly production-like, Structured Concurrency treats a set of related tasks as a single unit of work. If any subtask fails, sibling tasks are cancelled automatically.
import java.util.concurrent.StructuredTaskScope;
record UserDetails(User user, List<Order> orders, List<Address> addresses) {}
UserDetails fetchUserDetails(long userId) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var userFuture = scope.fork(() -> userService.findById(userId));
var ordersFuture = scope.fork(() -> orderService.findByUser(userId));
var addressFuture = scope.fork(() -> addressService.findByUser(userId));
scope.join() // wait for all three
.throwIfFailed(); // propagate the first failure
return new UserDetails(
userFuture.get(),
ordersFuture.get(),
addressFuture.get()
);
}
}
Timeout with structured cancellation:
UserDetails fetchWithTimeout(long userId) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var userFuture = scope.fork(() -> userService.findById(userId));
var ordersFuture = scope.fork(() -> orderService.findByUser(userId));
scope.joinUntil(Instant.now().plusSeconds(5))
.throwIfFailed();
return new UserDetails(userFuture.get(), ordersFuture.get(), List.of());
}
}
8. Scoped Values (JEP 487 — Finalized)
ScopedValue is the modern, thread-safe replacement for ThreadLocal in virtual-thread-heavy applications. A value is bound for a defined scope and is automatically unbound when that scope exits.
import java.lang.ScopedValue;
public class RequestContext {
public static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
public static final ScopedValue<TraceId> CURRENT_TRACE = ScopedValue.newInstance();
public Response handle(HttpRequest req) {
User user = authenticate(req);
TraceId trace = TraceId.generate();
return ScopedValue.where(CURRENT_USER, user)
.where(CURRENT_TRACE, trace)
.call(() -> processRequest(req));
}
private Response processRequest(HttpRequest req) {
// Deep in the call stack — no parameter drilling needed
User u = CURRENT_USER.get();
TraceId t = CURRENT_TRACE.get();
AuditLog.record(t, u, req.path());
return routeRequest(req);
}
}
Why not
ThreadLocal?ThreadLocalleaks memory with virtual threads (millions of threads, each with its own copy).ScopedValueis bound per-scope, not per-thread, and has zero ongoing cost once the scope exits.
9. Stream Gatherers (JEP 485 — Finalized since JDK 24)
Custom intermediate stream operations are now first-class citizens. Gatherers lets you compose reusable pipeline stages beyond what the built-in API provides.
import java.util.stream.Gatherers;
// Sliding window of size 3
List<String> events = List.of("login", "view", "add-to-cart", "checkout", "payment");
events.stream()
.gather(Gatherers.windowSliding(3))
.forEach(window -> System.out.println("Window: " + window));
// Output:
// Window: [login, view, add-to-cart]
// Window: [view, add-to-cart, checkout]
// Window: [add-to-cart, checkout, payment]
Custom gatherer — batch by size with flush:
import java.util.stream.Gatherer;
public static <T> Gatherer<T, ?, List<T>> batchOf(int size) {
return Gatherer.ofSequential(
() -> new ArrayList<T>(),
(buffer, element, downstream) -> {
buffer.add(element);
if (buffer.size() == size) {
downstream.push(new ArrayList<>(buffer));
buffer.clear();
}
return true;
},
(buffer, downstream) -> {
if (!buffer.isEmpty()) downstream.push(new ArrayList<>(buffer));
}
);
}
// Usage
LongStream.rangeClosed(1, 10)
.boxed()
.gather(batchOf(3))
.forEach(batch -> System.out.println("Batch: " + batch));
// Output:
// Batch: [1, 2, 3]
// Batch: [4, 5, 6]
// Batch: [7, 8, 9]
// Batch: [10]
10. Ahead-of-Time Class Loading & Linking (JEP 483 — Finalized since JDK 24)
Startup time for large Spring/Jakarta applications can be significantly reduced by caching the class-loading and linking phase between runs.
# Step 1 — training run: generate the AOT cache
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf \
-jar myapp.jar --spring.main.web-environment=false
# Step 2 — production run: replay from cache
java -XX:AOTMode=on -XX:AOTConfiguration=app.aotconf \
-jar myapp.jar
Typical Spring Boot startup improvement: 30–50 % faster cold starts with no code changes.
Important Considerations for Enterprise Projects
Compatibility & Standards
| Topic | Guidance |
|---|---|
| LTS vs Feature Release | JDK 26 is not an LTS. Plan upgrades to JDK 25 (LTS, Sept 2025) for long-term support. Use 26 for non-critical services or as a stepping stone. |
| Bytecode Level | --release 26 targets bytecode version 70.0. Build tools must support it. |
| Preview Features | Features marked Preview require --enable-preview at compile and runtime. Never ship preview code without explicit approval; APIs can change. |
| Security Patches | Only the most recent two feature releases receive security patches. Stay current or move to LTS. |
Dependency & Framework Compatibility
// Check your core dependency matrix before upgrading
Spring Boot 3.5+ → full JDK 26 support (Spring Framework 6.3+)
Jakarta EE 11+ → compatible
Hibernate ORM 7.0+ → compatible
Quarkus 3.10+ → compatible
Micronaut 4.8+ → compatible
Lombok → requires 1.18.36+ for JDK 26 annotation processing
MapStruct 1.6+ → compatible
Reflection & Encapsulation
JDK 26 continues tightening access to JDK internals. Libraries or legacy code that rely on --add-opens hacks may break.
# Identify problematic accesses BEFORE upgrading
java --version # confirm current JDK
jdeprscan --release 26 myapp.jar # scan for deprecated APIs
java --illegal-access=warn -jar myapp.jar # log reflective access warnings
Virtual Threads & Structured Concurrency
If you are adopting virtual threads (stable since JDK 21), combine them with ScopedValue and StructuredTaskScope for safe, observable concurrency:
// Spring Boot + virtual threads + scoped values
@Configuration
public class VirtualThreadConfig {
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandler() {
return handler -> handler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
}
}
// Service layer — scoped values propagate safely across virtual threads
@Service
public class OrderService {
public List<Order> fetchUserOrders() {
User user = RequestContext.CURRENT_USER.get(); // safe in virtual threads
return orderRepo.findByUserId(user.id());
}
}
Memory & GC
- ZGC (default in server-class VMs) and G1 both support the generational modes improved in JDK 25/26.
- Value classes (preview) promise heap density gains once finalised — plan to revisit heap sizing after adoption.
- Remove any
-XX:+UseParallelGCflags inherited from JDK 8 and re-benchmark on ZGC.
Tooling Readiness
| Tool | Minimum Version for JDK 26 |
|---|---|
| Maven | 3.9.6+ |
| Gradle | 8.11+ |
| IntelliJ IDEA | 2025.1+ |
| Eclipse | 2025-06 |
| VS Code Java Extension | 1.38+ |
| SonarQube | 10.8+ |
| Checkstyle | 10.21+ |
| SpotBugs | 4.8+ |
Migration Checklist
Use this checklist to systematically upgrade a production service from JDK 21 / 17 → JDK 26.
Phase 1 — Assessment
- Inventory dependencies — run
jdeprscan --release 26and review the report. - Check framework versions — ensure Spring Boot, Quarkus, or Micronaut supports JDK 26.
- Audit
--add-opens/--add-exportsflags — each one is a risk; research alternatives. - Review
sun.*andcom.sun.*usages — these are increasingly inaccessible. - Identify
ThreadLocalusages — plan migration toScopedValuewhere applicable. - Scan for removed APIs — check the JDK migration guide for all removals since your current version.
Phase 2 — Build Pipeline Update
- Update
pom.xml/build.gradle— set<java.version>26</java.version>(orsourceCompatibility = 26). - Update Maven/Gradle plugins —
maven-compiler-plugin 3.13+,gradle 8.11+. - Add compiler flags for preview features (only if you adopt them):
<!-- Maven --> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.13.0</version> <configuration> <release>26</release> <compilerArgs> <arg>--enable-preview</arg> </compilerArgs> </configuration> </plugin>// Gradle tasks.withType(JavaCompile).configureEach { options.compilerArgs += ['--enable-preview'] } tasks.withType(Test).configureEach { jvmArgs '--enable-preview' } - Update CI/CD pipelines — point
JAVA_HOME/java-versionin GitHub Actions, Jenkins, etc.# GitHub Actions - uses: actions/setup-java@v4 with: java-version: '26' distribution: 'temurin'
Phase 3 — Code Migration
- Replace
ThreadLocalwithScopedValue— start with request-scoped contexts. - Adopt unnamed classes for utility scripts, test helpers, and quick prototypes.
- Refactor complex
instanceofchains to pattern-matchingswitch. - Replace static factory workarounds with flexible constructor bodies where cleaner.
- Adopt module imports (
import module java.base) in classes with manyjava.*imports. - Enable AOT caching for services where startup time matters:
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -jar myapp.jar - Profile with JFR after migration to catch performance regressions:
java -XX:StartFlightRecording=duration=60s,filename=recording.jfr -jar myapp.jar
Phase 4 — Validation & Go-Live
- Run full test suite on JDK 26 and confirm 0 failures.
- Execute load tests — validate GC behaviour and latency under peak load.
- Smoke-test preview features (if adopted) with
-ea -XX:+EnableDynamicAgentLoadingwhere needed. - Review logs for
WARNING: Illegal reflective accessmessages. - Update Docker base images:
FROM eclipse-temurin:26-jre-alpine COPY target/myapp.jar /app/myapp.jar ENTRYPOINT ["java", "-jar", "/app/myapp.jar"] - Tag the release and keep JDK 25 LTS available as a rollback target.
Summary
JDK 26 is a rich feature release that finalises several long-running preview features and introduces exciting first previews from Project Valhalla. Here are the key takeaways:
✅ Finalized & safe for production
• Flexible Constructor Bodies
• Module Import Declarations
• Unnamed Classes & Instance Main Methods
• Scoped Values
• Stream Gatherers (since JDK 24)
• AOT Class Loading (since JDK 24)
🔬 Preview — great to experiment, not for production
• Primitive Patterns (3rd preview)
• Null-Restricted Value Types (1st preview)
• Value Classes (1st preview)
• Structured Concurrency (4th preview)
For enterprise teams: adopt the finalized features immediately, explore previews in non-critical services, and target JDK 25 (LTS) as your production baseline. JDK 26 is the perfect release to validate your upgrade path and get hands-on experience with Valhalla before it lands in an LTS.
Happy coding! ☕