Post

Java 18 to Java 21 Migration Guide — Features, Code Examples, Checklist

Complete Java 18 to Java 21 LTS migration guide for developers. Covers virtual threads, pattern matching, record patterns, sequenced collections, sealed classes — with official JEP links, Spring Boot compatibility, code examples, and step-by-step migration checklist.

Java 18 to Java 21 Migration Guide — Features, Code Examples, Checklist

Overview

 Details
TargetJava 21 (LTS)
Source VersionsJava 18, 19, 20
Release DateSeptember 19, 2023
LTS SupportOracle Premier Support until September 2028
Previous LTSJava 17 (September 2021)
Official Release NotesJDK 21 — OpenJDK
Oracle Migration GuideMigration Guide — Java SE 21

Why Migrate to Java 21?

Java 21 is the first Long-Term Support release since Java 17. Versions 18, 19, and 20 were short-term releases (6 months of support each). Migrating to 21 gives you:

  • Years of security patches and support
  • All finalized features that were in preview across 18–20
  • Framework compatibility (Spring Boot 3.2+, Quarkus 3.6+, Micronaut 4.2+)
  • Performance improvements in GC, JIT, and startup time

Java 18 (March 2022) — Key Features

Official release page: JDK 18 — OpenJDK

Simple Web Server — JEP 408

A built-in, zero-dependency HTTP file server for prototyping and testing.

1
jwebserver --port 8080 --directory /path/to/files

Programmatic usage:

1
2
3
4
5
6
var server = SimpleFileServer.createFileServer(
    new InetSocketAddress(8080),
    Path.of("/tmp/www"),
    OutputLevel.INFO
);
server.start();

UTF-8 by Default — JEP 400

Charset.defaultCharset() now returns UTF-8 on all operating systems. No more platform-specific charset surprises.

If your code relied on platform-specific default charset (e.g., Windows-1252), you may need to explicitly specify charsets where needed.

Code Snippets in JavaDoc — JEP 413

Replace verbose <pre>{@code ...}</pre> with cleaner syntax:

1
2
3
4
5
6
7
/**
 * Example usage:
 * {@snippet :
 *     var list = List.of("one", "two", "three");
 *     list.forEach(System.out::println);
 * }
 */

Java 19 (September 2022) — Preview Features

Official release page: JDK 19 — OpenJDK

Virtual Threads (Preview) — JEP 425

First look at Project Loom’s lightweight threads:

1
2
3
Thread.startVirtualThread(() -> {
    System.out.println("Running on: " + Thread.currentThread());
});

Structured Concurrency (Incubator) — JEP 428

Manages multiple concurrent tasks as a single unit of work — improves error handling and cancellation.

Record Patterns (Preview) — JEP 405

Deconstruct records directly in pattern matching:

1
2
3
4
5
record Point(int x, int y) {}

if (obj instanceof Point(int x, int y)) {
    System.out.println("x = " + x + ", y = " + y);
}

Java 20 (March 2023) — Refinement Cycle

Official release page: JDK 20 — OpenJDK

Java 20 refined preview features from 19. No new finalized features, but important iterations:

FeatureJEPStatus in 20
Virtual ThreadsJEP 436Second Preview
Record PatternsJEP 432Second Preview
Pattern Matching for SwitchJEP 433Fourth Preview
Scoped ValuesJEP 429Incubator

If you adopted preview features in 19, check for API changes. Refinements between previews can be subtle but breaking.


Java 21 (September 2023) — The LTS Destination

Cloud Hosting Everything comes together in Java 21

Official release page: JDK 21 — OpenJDK

Finalized Features Summary

FeatureJEPCategory
Virtual ThreadsJEP 444Concurrency
Record PatternsJEP 440Language
Pattern Matching for SwitchJEP 441Language
Sequenced CollectionsJEP 431Collections
Generational ZGCJEP 439GC
Key Encapsulation Mechanism APIJEP 452Security

Virtual Threads — JEP 444

The headline feature. Lightweight threads managed by the JVM, not the OS.

1
2
3
4
5
6
7
8
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i ->
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        })
    );
}

Traditional threads cost ~1MB stack each. Virtual threads cost only a few KB. Use simple blocking I/O without worrying about thread pool exhaustion.

Migration action: Replace Executors.newFixedThreadPool() in I/O-heavy services with Executors.newVirtualThreadPerTaskExecutor().

Further reading:


Record Patterns — JEP 440

Deconstruct record values directly in instanceof and switch:

1
2
3
4
5
6
7
record Address(String city, String country) {}
record Customer(String name, Address address) {}

// Nested deconstruction
if (customer instanceof Customer(String name, Address(String city, var country))) {
    System.out.println(name + " lives in " + city);
}

Pattern Matching for Switch — JEP 441

No more chains of instanceof checks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Before (Java 17 style)
if (obj instanceof String s) {
    process(s);
} else if (obj instanceof Integer i) {
    process(i);
}

// After (Java 21)
switch (obj) {
    case String s  -> process(s);
    case Integer i -> process(i);
    case null      -> handleNull();
    default        -> handleUnknown(obj);
}

Guarded patterns with when:

1
2
3
4
5
6
switch (shape) {
    case Circle c when c.radius() > 10 -> "large circle";
    case Circle c                       -> "small circle";
    case Rectangle r                    -> "rectangle";
    default                             -> "unknown";
}

Oracle Docs: Pattern Matching for switch — Java SE 21


Sequenced Collections — JEP 431

New interfaces: SequencedCollection, SequencedSet, SequencedMap.

1
2
3
4
5
6
7
8
9
// Before — inconsistent
list.get(0);
set.iterator().next();
deque.getFirst();

// After — unified API
collection.getFirst();
collection.getLast();
collection.reversed();

Sealed Classes + Pattern Matching

Combine sealed classes (finalized in 17) with switch pattern matching (finalized in 21):

1
2
3
4
5
6
7
8
9
10
11
12
13
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Triangle(double base, double height) implements Shape {}

double area(Shape shape) {
    return switch (shape) {
        case Circle c    -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.w() * r.h();
        case Triangle t  -> 0.5 * t.base() * t.height();
        // No default needed — compiler knows it's exhaustive
    };
}

Framework Compatibility

FrameworkMinimum Version for Java 21Reference
Spring Boot3.2+Spring Boot 3.2 Release Notes
Spring Framework6.1+Spring Framework 6.1
Hibernate6.4+Hibernate ORM 6.4
Quarkus3.6+Quarkus
Micronaut4.2+Micronaut
Lombok1.18.30+Lombok Changelog
JUnit 55.10+JUnit 5 User Guide

Spring Boot + Virtual Threads

Spring Boot 3.2+ has built-in support for virtual threads. Enable with a single property:

1
2
# application.properties
spring.threads.virtual.enabled=true

This configures the embedded Tomcat/Jetty to use virtual threads for request handling — massive scalability with zero code change.

Reference: Spring Boot Virtual Threads


Migration Checklist

#StepDetails
1Update build toolSet source and target to 21 in pom.xml or build.gradle
2Update CI/CD imageUse eclipse-temurin:21-jdk or equivalent
3Run full buildFix removed/deprecated API usage
4Check SecurityManagerDeprecated for removal — plan alternatives
5Adopt virtual threadsReplace fixed thread pools in I/O-heavy services
6Refactor instanceof chainsUse switch pattern matching
7Use SequencedCollectionWhere you access first/last elements
8Remove –enable-previewFor features now finalized
9Update frameworksSpring Boot 3.2+, Hibernate 6.4+
10Update librariesLombok, Byte Buddy, ASM compatibility
11Run tests & benchmarksVerify GC behavior and performance

Common Pitfalls

1. Virtual Threads + Synchronized Blocks

Virtual threads can get pinned to carrier threads inside synchronized blocks. Prefer ReentrantLock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Avoid with virtual threads
synchronized (lock) {
    blockingIOCall();
}

// Prefer
private final ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    blockingIOCall();
} finally {
    lock.unlock();
}

Deep dive: JEP 491 — Synchronize Virtual Threads without Pinning (targeted for future releases)

2. Preview API Breaking Changes

Features evolve between preview rounds. Don’t rely on preview behavior from 19/20 — always reference the final JEP.

3. Third-Party Library Compatibility

Libraries that manipulate bytecode need updates:

  • Lombok: 1.18.30+
  • Byte Buddy: 1.14.9+
  • ASM: 9.6+

Update these before upgrading the JDK.


Build Tool Configuration

Maven

1
2
3
4
5
<properties>
    <java.version>21</java.version>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
</properties>

Gradle

1
2
3
4
5
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

Dockerfile

1
2
3
4
5
FROM eclipse-temurin:21-jdk-alpine AS build
# build stage

FROM eclipse-temurin:21-jre-alpine
# runtime stage

Further Resources

ResourceLink
OpenJDK 21 Project Pageopenjdk.org/projects/jdk/21
Oracle Java SE 21 Docsdocs.oracle.com/en/java/javase/21
Oracle Migration GuideJava SE 21 Migration Guide
Java Almanac (Diff Tool)javaalmanac.io/jdk/21
Baeldung — Java 21 Featuresbaeldung.com/java-lts-21-new-features
Spring Boot 3.2 Release NotesGitHub Wiki
Virtual Threads — Oracle Blogblogs.oracle.com
JDK Download (Temurin)adoptium.net

This post is licensed under CC BY 4.0 by the author.