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.
Overview
| Details | |
|---|---|
| Target | Java 21 (LTS) |
| Source Versions | Java 18, 19, 20 |
| Release Date | September 19, 2023 |
| LTS Support | Oracle Premier Support until September 2028 |
| Previous LTS | Java 17 (September 2021) |
| Official Release Notes | JDK 21 — OpenJDK |
| Oracle Migration Guide | Migration 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:
| Feature | JEP | Status in 20 |
|---|---|---|
| Virtual Threads | JEP 436 | Second Preview |
| Record Patterns | JEP 432 | Second Preview |
| Pattern Matching for Switch | JEP 433 | Fourth Preview |
| Scoped Values | JEP 429 | Incubator |
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
Everything comes together in Java 21
Official release page: JDK 21 — OpenJDK
Finalized Features Summary
| Feature | JEP | Category |
|---|---|---|
| Virtual Threads | JEP 444 | Concurrency |
| Record Patterns | JEP 440 | Language |
| Pattern Matching for Switch | JEP 441 | Language |
| Sequenced Collections | JEP 431 | Collections |
| Generational ZGC | JEP 439 | GC |
| Key Encapsulation Mechanism API | JEP 452 | Security |
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
| Framework | Minimum Version for Java 21 | Reference |
|---|---|---|
| Spring Boot | 3.2+ | Spring Boot 3.2 Release Notes |
| Spring Framework | 6.1+ | Spring Framework 6.1 |
| Hibernate | 6.4+ | Hibernate ORM 6.4 |
| Quarkus | 3.6+ | Quarkus |
| Micronaut | 4.2+ | Micronaut |
| Lombok | 1.18.30+ | Lombok Changelog |
| JUnit 5 | 5.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
| # | Step | Details |
|---|---|---|
| 1 | Update build tool | Set source and target to 21 in pom.xml or build.gradle |
| 2 | Update CI/CD image | Use eclipse-temurin:21-jdk or equivalent |
| 3 | Run full build | Fix removed/deprecated API usage |
| 4 | Check SecurityManager | Deprecated for removal — plan alternatives |
| 5 | Adopt virtual threads | Replace fixed thread pools in I/O-heavy services |
| 6 | Refactor instanceof chains | Use switch pattern matching |
| 7 | Use SequencedCollection | Where you access first/last elements |
| 8 | Remove –enable-preview | For features now finalized |
| 9 | Update frameworks | Spring Boot 3.2+, Hibernate 6.4+ |
| 10 | Update libraries | Lombok, Byte Buddy, ASM compatibility |
| 11 | Run tests & benchmarks | Verify 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
| Resource | Link |
|---|---|
| OpenJDK 21 Project Page | openjdk.org/projects/jdk/21 |
| Oracle Java SE 21 Docs | docs.oracle.com/en/java/javase/21 |
| Oracle Migration Guide | Java SE 21 Migration Guide |
| Java Almanac (Diff Tool) | javaalmanac.io/jdk/21 |
| Baeldung — Java 21 Features | baeldung.com/java-lts-21-new-features |
| Spring Boot 3.2 Release Notes | GitHub Wiki |
| Virtual Threads — Oracle Blog | blogs.oracle.com |
| JDK Download (Temurin) | adoptium.net |
Related Posts
- Java Collections + Lambda Expressions — Practical Guide with Code Examples — Master streams, filtering, sorting, and grouping with real before/after examples.
- Setting Up a Developer Blog with Jekyll Chirpy — SEO, Analytics, Comments, and Pageviews — How this blog was built, step by step.