Virtual threads dominated the Java 21 release notes, and several smaller additions arrived alongside them. These are the ones worth knowing about.
String templates — and why you cannot use them
Worth addressing first because you will find articles recommending them. String templates were previewed in Java 21 as a typed alternative to concatenation, then withdrawn — removed entirely in Java 23 after the design was reconsidered.
So: do not adopt them. Java still has no string interpolation, and
formatted remains the answer:
class Demo {
String message(String name, int count) {
return "Hello %s, you have %d orders".formatted(name, count);
}
}
This is a useful illustration of what preview features are for. They ship so that real use can expose design problems before the feature becomes permanent — and occasionally the answer is that it should not.
Sequenced collections
The other substantial API addition, covered in
its own post: getFirst(),
getLast() and reversed() on every ordered collection.
Key encapsulation and better randomness
class Demo {
void run() {
// A stream of random numbers, from a named algorithm
RandomGenerator generator = RandomGenerator.of("Xoshiro256PlusPlus");
System.out.println(generator.nextInt(100));
// Still fine, and now one implementation among many
RandomGenerator legacy = new Random(42);
System.out.println(legacy.ints(5, 0, 10).boxed().toList());
}
}
Java 17 introduced the RandomGenerator interface and Java 21 rounded it out.
Random, SecureRandom and ThreadLocalRandom all implement it, so
code can now take the interface and let the caller choose the algorithm — including several modern
generators that are faster and have better statistical properties than the 1995
Random.
For anything security-related, SecureRandom remains the only correct choice; this
change does not alter that.
Generational ZGC
ZGC, the low-latency collector, gained generational support. Practically: it now handles the very common case of many short-lived objects far more efficiently, which was its main weakness.
java -XX:+UseZGC -XX:+ZGenerational -jar myapp.jar
G1 is still the right default for almost everything. ZGC is worth investigating if you have a large heap and a measured pause-time problem — and "measured" is doing real work in that sentence, because changing collectors on a hunch usually makes things worse.
The foreign function and memory API
Previewing in 21 and standard in 22, this replaces JNI for calling native code and
ByteBuffer for working with off-heap memory. It matters to a narrow audience and it is
worth recognising:
// java.lang.foreign — allocate memory outside the Java heap, deterministically freed
// try (Arena arena = Arena.ofConfined()) {
// MemorySegment segment = arena.allocate(1024);
// segment.set(ValueLayout.JAVA_INT, 0, 42);
// System.out.println(segment.get(ValueLayout.JAVA_INT, 0)); // 42
// } // memory released here, not whenever the GC gets round to it
The two problems it solves are real ones. JNI required writing C glue code and was easy to get
catastrophically wrong; ByteBuffer capped out at 2GB and freed its memory whenever the
collector felt like it. The new API is pure Java, bounded by an Arena whose lifetime you
control, and checked at runtime rather than trusting you.
If you have never needed to call a C library from Java, you can ignore this entirely. If you have, it is a substantial improvement over what came before.
Deprecations worth noticing
- Finalization is deprecated for removal.
Object.finalize()has been discouraged for years; Java 18 formally deprecated it and it can now be disabled with--finalization=disabled. If any of your classes overridefinalize, replace it withAutoCloseableand try-with-resources — the exception handling post covers the pattern. - The security manager is disabled by default. Java 17 deprecated it; from 18 it must be explicitly re-enabled. Almost nothing uses it.
- Dynamic agent loading now warns. Tools that attach to a running JVM — some profilers and mocking libraries — print a warning, and a future release will require an explicit flag.
Unnamed classes and instance main
Also previewed in 21: the ability to write a program without a class declaration or a
static main. It was refined over several releases and finalised in Java 25, so it is
covered in Module Imports and Simple Source Files rather
than here — mentioned only so you know the Java 21 preview version is not the final syntax.
Smaller API additions
class Demo {
void run() {
// Splitting on a character, without regex overhead
System.out.println("a,b,c".split(",").length); // 3
// Emoji and code point awareness
System.out.println("hello".codePoints().count()); // 5
// Character.isEmoji and friends, added in 21
System.out.println(Character.isLetter('a')); // true
}
}
Java 21 also added Math.clamp, which replaces a very common two-call idiom:
class Demo {
void run() {
int value = 150;
int old = Math.max(0, Math.min(100, value)); // the idiom everyone wrote
int now = Math.clamp(value, 0, 100); // Java 21
System.out.println(old + " " + now); // 100 100
}
}
It also throws if the bounds are the wrong way round, which the nested version silently does not —
Math.max(100, Math.min(0, value)) returns nonsense rather than complaining.
What Java 21 is, overall
It is the LTS that made Java competitive on concurrency again. Everything else here is small; virtual threads are the reason the release matters, and they are a genuine change in how server-side Java is written rather than a syntax convenience.
Alongside that, pattern matching for switch and record patterns completed the data-modelling story that records and sealed classes began in 17. Those four features are designed to be used together, which is why they read oddly in isolation and well in combination.
Next
The Java 21 migration guide is next — what to check when moving from 17.