New features from Java 18 to 21
A quick tour through new, production-ready features when LTS-upgrading from Java 17 to Java 21.
Note that this article highlights just those features that will be relevant for most developers - many more improvements have been made apart from these.
All articles in this series:
Code Snippets in Java API Documentation
Instead of using a combination of <pre>
and @code
for including source code in JavaDoc, this can now be done with the following syntax:
/**
* Example for an inline snippet:
*
* {@snippet :
* System.out.println("Hello!");
* }
*
*/
Alternatively, the actual snippet can be provided in a separate file, which is then referenced like this:
/**
* Example for an external snippet:
*
* {@snippet file="SomeSnippet.java"}
*
*/
Some advantages and features of these snippets are:
- IDEs and other tools can easily recognize source code snippets.
- Automatic syntax highlighting.
- Property files and languages other than Java are supported.
- Highlighting specific parts (e.g., as bold or italic) is supported.
- Linking specific parts to their API doc is supported.
- Hardly any need to escape special characters, especially with external snippets.
- Different parts of an external snippet can be referenced using “regions” within this snippet.
HashMap.newHashMap() / HashSet.newHashSet()
To create a new HashMap or HashSet that can hold (for example) up to 42 elements without being resized, we can now use
HashMap.newHashMap(42);
HashSet.newHashSet(42);
When compared to the following
new HashMap(42);
new HashSet(42);
these constructors result in data structures that will be resized as soon as 75% of the specified initial capacity are occupied, which might not be what the developer actually intended.
Sequenced Collections
Three new interfaces have been added to the java.util
package to more consistently express data structures whose elements
have a defined encounter order:
interface SequencedCollection<E> extends Collection<E> {
E getFirst();
E getLast();
void addFirst(E); // optional
void addLast(E); // optional
E removeFirst(); // optional
E removeLast(); // optional
SequencedCollection<E> reversed();
}
interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
SequencedSet<E> reversed();
}
interface SequencedMap<K,V> extends Map<K,V> {
Entry<K, V> firstEntry();
Entry<K, V> lastEntry();
Entry<K, V> pollFirstEntry(); // optional
Entry<K, V> pollLastEntry(); // optional
V putFirst(K, V); // optional
V putLast(K, V); // optional
SequencedMap<K,V> reversed();
SequencedSet<K> sequencedKeySet();
SequencedCollection<V> sequencedValues();
SequencedSet<Entry<K,V>> sequencedEntrySet();
}
Record Patterns
Pattern matching for records is capable of directly assigning the record’s components to variables:
if (obj instanceof Point(int x, int y)) {
System.out.println(x+y);
}
Instead of explicitly stating the component types, you can let the compiler infer them via var
:
if (obj instanceof Point(var x, var y)) {
System.out.println(x + y);
}
This “disaggregation” (or “destructuring”) of records also works for nested components:
if (obj instanceof ColoredPoint(Point(int x, int y), Color c)) {
System.out.println(x + y);
System.out.println(c);
}
Apart from instanceof
, record patterns can also be used in switch statements and expressions (see below).
Pattern Matching for Switch
The case
labels of switch statements (and switch expressions) can hold patterns:
switch (obj) {
case Integer i -> System.out.println("int: " + i);
case String s -> System.out.println("str: " + s);
default -> System.out.println("obj: " + obj);
}
Moreover, null
is also allowed for case
labels:
switch (str) {
case null -> System.out.println("Null!");
case "Foo" -> System.out.println("Foo!");
default -> System.out.println("Something else!");
}
The default
case does not match null
. Without the case null
, a null value for str
would throw a
NullPointerException. However, it is possible to combine the null
and default
case if they require the same handling:
switch (str) {
case "Foo" -> System.out.println("Foo!");
case null, default -> System.out.println("Null or something else!");
}
Pattern case labels can be further refined with non-constant guards via when
clauses:
switch (str) {
case String s
when s.equalsIgnoreCase("foo") -> System.out.println("Foo!");
case String s
when s.equalsIgnoreCase("bar") -> System.out.println("Bar!");
default -> System.out.println("Some other string!");
}
It is not possible to provide multiple when
guards to a single case
label. Instead, the case
label must be duplicated as in the example above.
As soon as a switch statement uses any of the newly introduced features
- pattern labels
- null labels
- switch over something other than the switch “legacy” types (four primitives and their wrappers, String, enums)
the compiler checks the cases for exhaustiveness, which might require you to add a default
case.
Virtual Threads
Types of threads:
- Operating system (OS) thread: relatively expensive and scarce
- Platform thread: a
java.lang.Thread
that- is a thin wrapper around an OS thread (1:1 and hence, just as expensive and scarce)
- occupies the OS thread even while waiting for something else (like I/O)
- Virtual thread: a
java.lang.Thread
that- gets mounted to a platform thread (“carrier”) whenever it has work to do (i.e., while it performs calculations on the CPU)
- gets unmounted from a platform thread whenever it blocks / waits for something (with certain exceptions), making room for other virtual threads that have something to do
Hence, a large number of virtual threads can be mapped to a small number of platform threads. Instead of occupying precious OS threads with idle, waiting platform threads, the hardware usage can be optimized by assigning busy threads, and un-assigning idle virtual threads. For typical server applications with frequent blocking operations (database or backend access) and a high number of concurrent requests, using virtual threads can significantly increase the overall throughput (i.e., the number of requests that can be processed per second).
There are different ways for creating virtual threads (instead of platform threads):
When running your Java application within a server or framework, consult its documentation on how to enable virtual threads.
In Spring Boot, for example,
virtual threads can be enabled
via the property spring.threads.virtual.enabled
(beginning with version 3.2).
The method Thread.isVirtual() can be used to check whether the current thread is a virtual thread.
Be sure to follow certain guidelines to prevent some caveats that might arise with virtual threads:
- Prefer simple, synchronous code (thread-per-request style) over asynchronous, “reactive” style.
- Because the latter is more complex, harder to debug, and does not benefit from virtual threads.
- Never pool virtual threads.
- Because virtual threads are cheap and plentiful.
- Don’t use virtual thread pools to limit the number of concurrent accesses to a limited resource (such as an easily overloaded backend).
- Because virtual threads should not be pooled (see above).
- Typically, use Semaphore instead.
- Database connection pools do not require an additional semaphore, because they already serve as a semaphore themselves.
- Don’t cache expensive reusable objects in Thread-local variables.
- Because this only makes sense with pooled threads that are actually reused (which is not the case for virtual threads).
- Scoped values (JEP 506 / Java 25) might be a suitable alternative in certain cases.
- For checking the locations where a virtual thread sets a thread-local value at runtime, use the system property
jdk.traceVirtualThreadLocals
, which triggers a stack trace whenever this happens.
- Be aware that virtual threads cannot be unmounted from their carrier thread in the following cases:
- When it executes a native method or a foreign function.
- When it executes code inside a synchronized block or method (until Java 24, where this was solved with JEP 491).
Math.clamp()
The static method
returns its first parameter if it is within the given min/max bounds. Otherwise, if the value is out-of-bounds, it returns the bound that is closer to the value. There are also overloads for double and float.
StringBuilder/StringBuffer.repeat()
The methods
insert the given character sequence for the given number of times (so you don’t have to use Apache Commons StringUtils for this any more).
String.indexOf()
The methods
- String.indexOf(String str, int beginIndex, int endIndex)
- String.indexOf(int ch, int beginIndex, int endIndex)
return the index of the first parameter within the bounds given by the other two parameters, and throw a StringIndexOutOfBoundsException
if the given bounds are invalid.
String.splitWithDelimiters()
The method
splits a string into an array of substrings using the given regex as delimiter, with the given limit indicating the number of times the regex shall be applied. When providing a limit of 0, this method behaves exactly like String.split(String regex), which is available since Java 1.4.