Java Evolution — Part 8

Java 18 (March 22, 2022), Java 19 (September 20, 2022), and Java 20 (March 21, 2023) were non-LTS bridge releases between the Java 17 and Java 21 LTS versions. Their most important role was incubating and previewing the Project Loom features — virtual threads and structured concurrency — that would be finalised in Java 21.


UTF-8 by Default (Java 18)

JEP 400 made UTF-8 the default charset for the Java standard APIs. Before Java 18, the default charset was platform-dependent — UTF-8 on Linux and macOS, but Windows-1252 (or another Windows code page) on Windows. This caused subtle, hard-to-reproduce bugs when code was developed on one OS and deployed on another.

// Before Java 18 — charset depended on the OS
FileWriter writer = new FileWriter("output.txt"); // platform charset!
FileReader reader = new FileReader("input.txt"); // platform charset!
// Java 18+ — always UTF-8 by default
FileWriter writer = new FileWriter("output.txt"); // UTF-8
FileReader reader = new FileReader("input.txt"); // UTF-8
// Explicit charset still works and is recommended for clarity
FileWriter writer = new FileWriter("output.txt", StandardCharsets.UTF_8);

This change is a correctness improvement for the vast majority of modern applications. If your application explicitly relied on a non-UTF-8 platform charset, you can restore the old behaviour with -Dfile.encoding=COMPAT.


Simple Web Server (Java 18)

JEP 408 added a minimal HTTP file server to the JDK, launchable from the command line. It is designed for prototyping, testing, and quick demonstrations — not production use.

Terminal window
# Serve the current directory on port 8000
jwebserver
# Specify a directory and port
jwebserver -p 9090 -d /path/to/files
# Output
# Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
# Serving /path/to/files and subdirectories on 127.0.0.1 port 9090
# URL http://127.0.0.1:9090/

You can also create a simple file server programmatically:

var server = SimpleFileServer.createFileServer(
new InetSocketAddress(8080),
Path.of("/var/www/html"),
SimpleFileServer.OutputLevel.VERBOSE
);
server.start();

Code Snippets in Javadoc (Java 18)

JEP 413 introduced the @snippet tag for Javadoc, replacing the old <pre>{@code ...}</pre> pattern with a dedicated, IDE-friendly syntax that supports syntax highlighting and external file references.

/**
* Demonstrates the use of {@code Stream.toList()}.
*
* {@snippet :
* List<String> names = Stream.of("Alice", "Bob", "Charlie")
* .filter(s -> s.length() > 3)
* .toList(); // @highlight substring="toList"
* }
*/
public void example() {}

Virtual Threads (Preview — Java 19 & 20)

Project Loom is the most significant concurrency improvement in Java’s history. Its centrepiece is virtual threads — lightweight threads managed by the JVM rather than the OS.

The Problem with Platform Threads

Traditional Java threads (platform threads) map 1:1 to OS threads. OS threads are expensive:

  • Each consumes approximately 1–2 MB of stack memory
  • Context switching between OS threads is slow
  • A typical server can sustain only a few thousand concurrent platform threads

This forces developers to use asynchronous, non-blocking programming models (reactive, callbacks, CompletableFuture) to achieve high concurrency — code that is notoriously difficult to write, read, and debug.

Virtual Threads

Virtual threads are managed by the JVM. They are extremely lightweight — millions can exist simultaneously — and they are mounted onto a small pool of carrier (platform) threads. When a virtual thread blocks (e.g., waiting for I/O), it is unmounted from its carrier thread, which is then free to run another virtual thread.

// Java 19 preview — creating virtual threads
Thread vt = Thread.ofVirtual().start(() -> {
System.out.println("Running in virtual thread: " + Thread.currentThread());
});
// Using the Executors factory
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> {
// Each task runs in its own virtual thread
Thread.sleep(Duration.ofMillis(100));
return "done";
});
}
} // executor.close() waits for all tasks to complete

The Key Insight

Virtual threads allow you to write simple, synchronous, blocking code and get the scalability of asynchronous code. You do not need to change your programming model — just use virtual threads instead of platform threads.

// Simple blocking code that scales to millions of concurrent requests
// with virtual threads — no callbacks, no reactive chains
void handleRequest(HttpExchange exchange) throws IOException {
// This blocks the virtual thread, not the OS thread
String data = fetchFromDatabase(exchange.getRequestURI());
String processed = callExternalApi(data);
exchange.sendResponseHeaders(200, processed.length());
exchange.getResponseBody().write(processed.getBytes());
}

Virtual threads were finalised in Java 21.


Structured Concurrency (Incubator — Java 19 & 20)

Structured Concurrency (JEP 428/437) is a companion to virtual threads. It treats a group of related concurrent tasks as a single unit of work, with a well-defined lifecycle.

The core idea: if a task spawns subtasks, all subtasks must complete (or be cancelled) before the spawning task completes. This mirrors the structure of sequential code and makes concurrent code easier to reason about.

// Java 19 incubator — structured concurrency
Response handle(Request request) throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<UserInfo> userFuture = scope.fork(() -> fetchUser(request.userId()));
Future<OrderList> ordersFuture = scope.fork(() -> fetchOrders(request.userId()));
Future<Preferences> prefsFuture = scope.fork(() -> fetchPreferences(request.userId()));
scope.join(); // wait for all three
scope.throwIfFailed(); // propagate any failure
return new Response(
userFuture.resultNow(),
ordersFuture.resultNow(),
prefsFuture.resultNow()
);
}
}

With ShutdownOnFailure, if any subtask fails, the scope cancels the remaining subtasks immediately. This prevents resource leaks and avoids waiting for tasks whose results are no longer needed.

Structured Concurrency was finalised in Java 25.


Record Patterns (Preview — Java 19 & 20)

JEP 405/432 extended pattern matching to support record patterns — deconstructing record components directly in instanceof and switch.

record Point(int x, int y) {}
record ColoredPoint(Point point, Color color) {}
// Java 19 preview — record pattern in instanceof
Object obj = new ColoredPoint(new Point(1, 2), Color.RED);
if (obj instanceof ColoredPoint(Point(int x, int y), Color c)) {
System.out.printf("x=%d, y=%d, color=%s%n", x, y, c);
}
// Record patterns in switch
String describe(Object o) {
return switch (o) {
case Point(int x, int y) -> "Point at (%d,%d)".formatted(x, y);
case ColoredPoint(Point(int x, int y), var c) -> "Colored point at (%d,%d)".formatted(x, y);
default -> "Unknown";
};
}

Record patterns were finalised in Java 21.


Pattern Matching for switch — Continued Refinement

Java 19 and 20 continued refining pattern matching for switch with guarded patterns using the when keyword:

// Java 20 preview — guarded patterns
String classify(Object obj) {
return switch (obj) {
case Integer i when i < 0 -> "negative integer";
case Integer i when i == 0 -> "zero";
case Integer i -> "positive integer";
case String s when s.isBlank() -> "blank string";
case String s -> "non-blank string: " + s;
default -> "other";
};
}

Summary

Java 18–20 were deliberately focused on incubating the Project Loom features. The virtual threads preview in Java 19 was arguably the most anticipated Java feature in a decade.

FeatureVersionKey Benefit
UTF-8 by Default18Consistent charset across all platforms
Simple Web Server18Quick file serving for prototyping
@snippet in Javadoc18Better code examples in documentation
Virtual Threads (Preview)19 & 20Millions of lightweight threads; simple blocking code
Structured Concurrency (Incubator)19 & 20Safe, leak-free concurrent task management
Record Patterns (Preview)19 & 20Deconstruct records in patterns
Guarded Patterns (when)20Conditional case arms in switch