https://horstmann.com/unblog/2023-09-19/index.html
Java 21: The Nice, The Meh, and the ... Momentous
When Java 17 was released in 2021 as a "long term support"
version, I wrote an article dissecting its features and came to
the conclusion that it had a few nice features, but none that
were compelling reasons to upgrade. Except one: tens of thousands
of bug fixes.
Java 21 was released today, as another "long term support"
release. How does it rate on the momentousness scale? Read on for
an unbiased opinion.
.jpeg
The Momentousness Ratings
Every six months, there is a new Java release. Ever so often
(currently, every two years), Oracle labels a release as "long term
support", and Java users wonder whether they should upgrade. In
theory, other JDK distributors could offer "long term support" for
other releases, but it seems everyone is following Oracle's lead.
Should you upgrade?
Here are the major features of Java 21. I omit preview and incubator
features (which you are surely not going to use in production), JVM
internals, highly specialized features such as this one, and
deprecations.
Feature Example Momentousness Why care?
rating
Employee e = . . .;
String description = switch (e) {
case Executive exec when exec.getTitle().length() >= 20 ->
"An executive with an impressive title";
Pattern case Executive __ -> "An executive"; It's better than chains of if/else/else with instanceof. Do you
matching for case Manager m -> { Nice do that often? The JDK source has over 5 million LOC with about
switch m.setBonus(10000); a thousand instanceof preceded by else.
yield "A manager who just got a bonus";
}
default -> "A lowly employee with a salary of " + e.getSalary();
};
String description = switch (p)
{
case Point(var x, var y) when x == 0 && y == 0 -> "origin";
Record case Point(var x, var __) when x == 0 -> "on x-axis"; Nice How many records are in your codebase? (The Java 21 API has two
Patterns case Point(var __, var y) when y == 0 -> "on y-axis"; .)
default -> "not on either axis";
};
List words = ...;
Sequenced String lastWord = words.getLast(); Nice Good to have, but you wouldn't upgrade for that.
Collections for (String word : words.reversed()) System.out.println(word);
try { No more async gobbledygook!
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
for (URL url : getImageURLs(response.body())) { client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
Virtual saveImage(getImage(url)); .thenApply(HttpResponse::body)
threads } Momentous .thenApply(this::getImageURLs)
} .thenCompose(this::getImages)
catch (...) { ... } .thenAccept(this::saveImages)
.exceptionally(this::ohNoes);
"Hello, World!".splitWithDelimiters
Miscellaneous ("\\pP\\s*", -1) Meh Good that the API keeps evolving in small ways, but the changes
new methods // ["Hello", ", ", "World", "!", ""] are pretty minor.
Over 10,000 Bug JDK-8054022 HttpURLConnection timeouts with Expect: 100-Continue Count me in! Unless you are sure that none of them might impact you,
bug fixes and no chunking shouldn't you upgrade?
Let's look at these features in more detail.
Virtual Threads
Virtual threads are a big deal. Similar to generics, lambda
expressions, and modules, they solve a major problem for which the
language has otherwise no good alternative. If you have the problem
that they are designed to solve, you will have a powerful motivation
to upgrade.
Here is the problem. If you write applications that process many more
concurrent requests than available platform threads, you currently
have two unappealing choices:
* Use a synchronous programming style and accept that throughput is
limited by the number of platform threads
* Use an asynchronous or "reactive" programming style
What is wrong with an asynchronous programming style? You have to
structure your program as chunks of callbacks. You need library
support for sequencing, branches, loops, and exception handling,
instead of using the features that are built into Java. Debugging is
more challenging since the debugger cannot show you a complete
execution history when it stops at a breakpoint. Not convinced? Make
one of your junior programmers read through the documentation of
Project Reactor and then assign a simple task, such as loading a web
page and then loading all images in it.
Of course, virtual threads are not appropriate for all concurrent
programming. They only work for tasks that spend most of their time
waiting for network I/O. This is the situation in many business
applications where much of the request processing consists of calls
to the database and external services.
Interestingly, there is very little to learn in order to use virtual
threads. You just use them like regular threads. In most scenarios,
you simply configure your application framework to invoke your
business logic on virtual threads, and watch throughput increase.
One idiom is worth learning. To run multiple tasks in parallel, use a
local instance of ExecutorService:
try (var service = Executors.newVirtualThreadPerTaskExecutor()) {
Future f1 = service.submit(callable1);
Future f2 = service.submit(callable2);
result = combine(f1.get(), f2.get());
}
Obtaining the result with get is a blocking call, but so what,
blocking is cheap with virtual threads.
Structured Concurrency, a preview feature in Java 21, simplifies
error handling and makes it easier to harvest the results of multiple
concurrent requests.
There are a few caveats:
* In the past, a thread pool didn't just throttle the incoming
requests but also the concurrent resources that your app
consumed. If you now accept many more incoming requests, you may
need other ways to manage resource consumption.
* One resource that deserves particular attention is thread locals.
With many more threads than before, do you really want many more
thread locals? Or are there more appropriate mechanisms to
achieve whatever you wanted to achieve with thread locals? Your
framework provider needs to think this through, and if you
actively use thread locals, so should you. A lighter-weight
alternative is in preview.
* Virtual threads do not yet work well with blocking calls inside
synchronized methods or blocks. The remedy is to rewrite the
offending code with java.util.concurrent locks. Be sure that the
providers of your framework, database driver, and so on, update
their code to work well with virtual threads. Quite a few already
did.
Pattern Matching
Many functional languages have some form of pattern matching that
makes it convenient to work with "algebraic data types", which in
Java are implemented with sealed hierarchies and record classes.
Java has chosen to extend the syntax for instanceof and switch for
pattern matching, in order to leverage existing programmer knowledge.
These extensions have been in preview until Java 20 and are now in
their final form.
Are you using sealed hierarchies and records in your code base? Then
pattern matching is appealing. Here is an example, a simple JSON
hierarchy:
sealed interface JSONValue permits JSONArray, JSONObject, JSONPrimitive {}
final class JSONArray extends ArrayList implements JSONValue {}
final class JSONObject extends HashMap implements JSONValue {}
sealed interface JSONPrimitive extends JSONValue
permits JSONNumber, JSONString, JSONBoolean, JSONNull {}
final record JSONNumber(double value) implements JSONPrimitive {}
final record JSONString(String value) implements JSONPrimitive {}
enum JSONBoolean implements JSONPrimitive {
FALSE, TRUE;
}
enum JSONNull implements JSONPrimitive {
INSTANCE;
}
.png
Now you can process JSON values like this:
JSONPrimitive p = . . .;
double value = switch (p) {
case JSONString(var v) when v.matches("-?(0|[1-9]\\d*)(\\.\\d+)?([eE][+-]?\\d+)?") ->
Double.parseDouble(v);
case JSONString __ -> Double.NaN;
case JSONNumber(var v) -> v;
case JSONBoolean.TRUE -> 1;
case JSONBoolean.FALSE, JSONNull.INSTANCE -> 0;
}
Note the following:
* This is a switch expression that yields a value
* The compiler checks that the switch is exhaustive
* The pattern JSONString(var v) binds the variable v to the
component of the record
* The when clause restricts a match to a Boolean condition
* With JEP 445, you will be able to use case JSONString _, with a
single underscore, to indicate that you do not need the variable
binding. But that is still a preview feature.
* Since Java 14, you can have multiple constants in a single case
All this is certainly nicer than the instanceof and casting that one
might do right now with Jackson. But you might want to hold off
switching to a new JSON hierarchy until Java gives us value classes.
In general, pattern matching is more useful in contexts that are
designed for pattern matching. Today's use cases are perhaps not all
that compelling, but it is an investment in the future.
Sequenced Collections
When you have a Collection, how do you get the first element? With a
List, it's list.get(0), but in general, you'd call
collection.iterator().next(). Except with a stack or queue it is
peek, with a deque getFirst, and the SortedSet interface has first.
And what about the last element?
And how do you visit the elements in reverse order? Deque and
NavigableSet have a handy descendingIterator. For lists, you iterate
backwards, starting from the last element.
JEP 431 cleans up this situation with a SequencedCollection
interface. It has these methods:
E getFirst();
E getLast();
void addFirst(E);
void addLast(E);
E removeFirst();
E removeLast();
SequencedCollection reversed();
The first six methods are the same as in the Deque interface, which
is now a subinterface.
There is also a SequencedSet, where reversed yiels a set, and a
SequencedMap, with methods to get and put the first and last entry,
and with sequenced views for the keys, values, and entries.
This figure, by Stuart Marks, shows the change in the collections
hierarchy.
.png
TL;DR Reverse iteration over a list, deque, tree set, or tree map is
now more uniform. Getting the first and laste element too. That's
nice. Obviously not momentous.
Should You Upgrade?
When Java 17 was released, I opined that none of its features were
momentous enough to warrant upgrading, and one was downright ugly.
Still, upgrading was a no-brainer: tens of thousands of bug fixes.
Of course you should upgrade again to Java 21. Because, lots of bug
fixes.
And this time there is a truly momentous feature: virtual threads. If
you are contemplating the use of reactive programming, or you are
already unhappily doing so, you definitely want to check them out.
Also Nice
Oracle now has an online "playground" for testing Java snippets.
Check it out!
Comments powered by Talkyard.
* More Entries
* RSS Feed .png