Skip to content
yisusvii Blog
Go back

Java JDK 26: New Features, Code Examples & Enterprise Migration Guide

Suggest Changes

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

#FeatureJEPStatusImpact
1Primitive Types in Patterns, instanceof, and switchJEP 488Preview (3rd)High
2Flexible Constructor BodiesJEP 492FinalizedMedium
3Module Import DeclarationsJEP 494FinalizedMedium
4Unnamed Classes and Instance Main MethodsJEP 495FinalizedHigh
5Null-Restricted and Nullable Value TypesJEP 496Preview (1st)High
6Value ClassesJEP 497Preview (1st)High
7Structured ConcurrencyJEP 4994th PreviewMedium
8Scoped ValuesJEP 487FinalizedMedium
9Vector APIJEP 4899th IncubatorLow–Medium
10Stream GatherersJEP 485Finalized (since JDK 24)Medium
11Ahead-of-Time Class Loading & LinkingJEP 483Finalized (since JDK 24)Low
12Deprecations & RemovalsVariousVaries

Legend: Finalized — production-ready, no --enable-preview needed. 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? ThreadLocal leaks memory with virtual threads (millions of threads, each with its own copy). ScopedValue is 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

TopicGuidance
LTS vs Feature ReleaseJDK 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 FeaturesFeatures marked Preview require --enable-preview at compile and runtime. Never ship preview code without explicit approval; APIs can change.
Security PatchesOnly 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

Tooling Readiness

ToolMinimum Version for JDK 26
Maven3.9.6+
Gradle8.11+
IntelliJ IDEA2025.1+
Eclipse2025-06
VS Code Java Extension1.38+
SonarQube10.8+
Checkstyle10.21+
SpotBugs4.8+

Migration Checklist

Use this checklist to systematically upgrade a production service from JDK 21 / 17 → JDK 26.

Phase 1 — Assessment

Phase 2 — Build Pipeline Update

Phase 3 — Code Migration

Phase 4 — Validation & Go-Live


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! ☕


Suggest Changes
Share this post on:

Previous Post
Installing OpenClaw Securely on Ubuntu (Step-by-Step Guide)
Next Post
TensorFlow in 2026: Key Applications and the Best Alternatives