New Features in JDK 21: Virtual Threads, Sequenced Collections, String Templates, Record Patterns, and Switch Pattern Matching
The article introduces JDK 21’s latest long‑term‑support features—including virtual threads, sequenced collections, preview string templates, record patterns, and switch pattern matching—explaining their concepts, benefits, and providing concrete Java code examples for each.
JDK 21, released on 19 September 2023, is the newest long‑term‑support version of Java and brings several language enhancements that simplify concurrency, collection handling, string manipulation, and pattern matching.
1. Virtual Threads
Virtual threads behave like regular Java threads but are not bound 1:1 to OS threads; they use an M:N mapping via a carrier thread pool (ForkJoinPool). When a virtual thread blocks, it is detached from the carrier, allowing the carrier to run other virtual threads.
Carrier thread pool is ForkJoinPool
Key advantages include higher throughput, better availability, and reduced memory consumption.
Creating a virtual thread:
Thread.ofVirtual().start(Runnable);
Thread.ofVirtual().unstarted(Runnable);
Thread.ofVirtual().start(); // starts immediately
Thread.ofVirtual().unstarted(); // creates without startingCreating an ExecutorService that uses virtual threads:
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> System.out.println(Thread.currentThread().getName()));
executor.shutdown();2. Sequenced Collections
Sequenced collections provide a defined encounter order, allowing access to the first and last elements and iteration in reverse order. Methods such as addFirst , addLast , getFirst , removeLast , and reversed are default implementations.
public interface SequencedCollection
extends Collection
{
default void addFirst(E e) { ... }
default void addLast(E e) { ... }
default E getFirst() { ... }
default E getLast() { ... }
default E removeFirst() { ... }
default E removeLast() { ... }
default SequencedCollection
reversed();
}Example with ArrayList (which now implements the interface):
ArrayList
list = new ArrayList<>();
list.add(1); // [1]
list.addFirst(0); // [0, 1]
list.addLast(2); // [0, 1, 2]
System.out.println(list.getFirst()); // 0
System.out.println(list.getLast()); // 2
System.out.println(list.reversed()); // [2, 1, 0]SequencedSet
Ordered sets such as LinkedHashSet implement SequencedSet , providing the same first/last operations.
interface SequencedSet
extends Set
, SequencedCollection
{
SequencedSet
reversed();
}Sample usage:
SequencedSet
values = new LinkedHashSet<>();
values.add("one");
values.addFirst("zero");
System.out.println(values); // [zero, one]
SequencedSet
rev = values.reversed();
System.out.println(rev); // [one, zero]
System.out.println(values.equals(rev)); // trueSequencedMap
Maps with a defined encounter order (e.g., LinkedHashMap ) implement SequencedMap , offering methods like firstEntry , lastEntry , putFirst , and reversed .
interface SequencedMap
extends Map
{
SequencedMap
reversed();
SequencedSet
sequencedKeySet();
SequencedCollection
sequencedValues();
SequencedSet
> sequencedEntrySet();
Entry
firstEntry();
Entry
lastEntry();
Entry
pollFirstEntry();
Entry
pollLastEntry();
V putFirst(K k, V v);
V putLast(K k, V v);
}Example:
SequencedMap
myMap = new LinkedHashMap<>();
myMap.put("one",1);
myMap.put("two",2);
System.out.println(myMap.firstEntry()); // one=1
System.out.println(myMap.lastEntry()); // two=2
myMap.putFirst("zero",0);
System.out.println(myMap); // {zero=0, one=1, two=2}
SequencedMap
rev = myMap.reversed();
System.out.println(rev); // {two=2, one=1, zero=0}
System.out.println(myMap.equals(rev)); // true3. String Templates (Preview)
String templates, enabled with --enable-preview , provide concise interpolation using the STR or FMT processors. They replace verbose concatenation, StringBuilder , and String.format approaches.
package com.mina.stringtemplates;
import static java.util.FormatProcessor.FMT;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class StringTemplateExamples {
public static String greeting(String firstName, String lastName) {
return STR."Hello! Good morning \{firstName} \{lastName}";
}
public static String multiplyWithArithmeticExpressions(int a, int b) {
return STR."\{a} times \{b} = \{a * b}";
}
public static String multiplyFloatingNumbers(double a, double b) {
return FMT."%.2f\{a} times %.2f\{b} = %.2f\{a * b}";
}
public static String getErrorResponse(int httpStatus, String errorMessage) {
return STR."""
{"httpStatus": \{httpStatus}, "errorMessage": "\{errorMessage}"}
""";
}
public static String getCurrentDate() {
return STR."Today's date: \{LocalDate.now().format(DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"))}";
}
}4. Record Patterns
Record patterns allow matching a record type and extracting its components in a single step, improving readability of type checks.
package com.mina.recordpattern;
public class RecordPatternExample {
public static String getTransactionType(Transaction transaction) {
return switch (transaction) {
case null -> throw new IllegalArgumentException("Transaction cannot be null.");
case Transaction(String type, double amount) when type.equals("Deposit") && amount > 0 -> "Deposit";
case Transaction(String type, _) when type.equals("Withdrawal") -> "Withdrawal";
default -> "Unknown transaction type";
};
}
record Transaction(String type, double amount) {}
}5. Switch Pattern Matching
Pattern matching for switch was previewed in Java 17 and became a permanent feature in Java 21, allowing cases to be guarded with when clauses and to match against sealed hierarchies.
package com.mina.switchpatternmatching;
import com.mina.switchpatternmatching.SwitchPatternMatchingExample.Transaction.Deposit;
import com.mina.switchpatternmatching.SwitchPatternMatchingExample.Transaction.Withdrawal;
public class SwitchPatternMatchingExample {
public static String getTransactionType(Transaction transaction) {
return switch (transaction) {
case null -> throw new IllegalArgumentException("Transaction can't be null.");
case Deposit d when d.getAmount() > 0 -> "Deposit"; // guarded pattern
case Withdrawal w -> "Withdrawal";
default -> "Unknown transaction type";
};
}
sealed class Transaction permits Deposit, Withdrawal {
private double amount;
public Transaction(double amount) { this.amount = amount; }
public double getAmount() { return amount; }
final class Withdrawal extends Transaction { public Withdrawal(double amount) { super(amount); } }
final class Deposit extends Transaction { public Deposit(double amount) { super(amount); } }
}
}These additions make Java code more expressive, concise, and performant, especially for concurrent applications and modern API designs.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.