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