Site icon JVM Advent

Java 17: The Nice, the Meh, and the Ugly

 

The Release Cadence

In the early years of Java, there was a new release every couple of years, and two of them (1.2 and 5) had significant advances of the API and/or language.

Then came a long dry spell. It took five years for Java 7 (meh) and three years each for Java 8 (default methods and streams, nice) and 9 (modules, ugly).

In order to put things on a more predictable footing, Oracle switched to a new release model. Rain or shine, there is a release every 6 months. Releases every three years get long term support, starting with Java 11. That makes Java 17, released in September, the second LTS release. Followed by 23, 29, 35, 41, 47, 53, and so on. Like clockwork.

Except this press release seems to indicate that the next LTS release may be Java 21. Which would actually be fine. A two year LTS cycle is pretty common elsewhere.

So, now that there is a new LTS release, you are probably asking yourself if you should switch to it.

Rah Rah Rah

I’ve been amazed—although I shouldn’t be—at the amount of hype from the usual suspects. Check out this gushing article. Top three features:

  1. Sealed classes
  2. Context-specific deserialization filters
  3. New macOS rendering pipeline

Really? I am supposed to update my apps for that???

Infoworld does no better. Their headline features:

  1. Always-strict floating point semantics
  2. A foreign function and memory API
  3. A uniform API for pseudo random number generators

Sharat Chander has his heart in the right place, starting out with bug fixes and quality improvements. But then he too gets lost in the weeds of minor Java 17 features:

Clearly, there is a need for some unbiased advice. I am going to tell you what has actually changed from the last LTS release (Java 11), rating the features as nice, meh, or ugly. And then I’ll tell you why you should update.

I am not rating “preview features” at all. They give a glimpse of what may come, but the details are subject to change. Interesting for enthusiasts, but not something you would want to touch in production.

I am not giving brownie points for future promise. Sure, switch expressions, instanceof patterns, records, and sealed classes lay the ground work for robust pattern matching in the future. And robust pattern matching will be great once it arrives. In the future. I only rate features for the utility they have today.

Here is an executive summary of the highlights:

Feature Rating Why care?
Records Nice Constructor, accessors, toString, hashCode automatically provided
Switch expressions without fallthrough Nice More compact than if/else, safer than classic switch
Two other switch constructs Ugly I never asked for four kinds of switch
Pattern matching for instanceof Nice No need to cast
Sealed types Nice Can accurately model closed hierarchies
Text blocks Meh Nice for multiline strings, but must still escape \ and some "
Minor API changes Meh Few exciting API improvements in this cycle
Packaging tool Meh Need to run separately on each platform
Alpine port, NUMA-Aware Memory Allocation for G1, JFR Event Streaming, many more YMMV Depends on how badly you need the particular feature
Tens of thousands of bug fixes Count me in! Do you really want to gamble that none of them affect you, or that they are all backported?

What is in the Box?

Let’s break this into three parts: the Java Enhancement Proposals (JEP) that specify new language features or APIs, smaller API changes, and bug fixes.

JEPs

Here are the JEPs for JDK 12 through 17, sorted by category. I dropped previews and incubations that were superseded by later versions.

Language
361: Switch Expressions (Standard)
378: Text Blocks
394: Pattern Matching for instanceof
395: Records
409: Sealed Classes
API
334: JVM Constants API
339: Edwards-Curve Digital Signature Algorithm (EdDSA)
352: Non-Volatile Mapped Byte Buffers
356: Enhanced Pseudo-Random Number Generators
380: Unix-Domain Socket Channels
Tools
392: Packaging Tool
Warnings and Errors
358: Helpful NullPointerExceptions
390: Warnings for Value-Based Classes
396: Strongly Encapsulate JDK Internals by Default
403: Strongly Encapsulate JDK Internals
415: Context-Specific Deserialization Filters
Plumbing
189: Shenandoah: A Low-Pause-Time Garbage Collector (Experimental)
230: Microbenchmark Suite
340: One AArch64 Port, Not Two
341: Default CDS Archives
344: Abortable Mixed Collections for G1
345: NUMA-Aware Memory Allocation for G1
346: Promptly Return Unused Committed Memory from G1
347: Enable C++14 Language Features
349: JFR Event Streaming
350: Dynamic CDS Archives
351: ZGC: Uncommit Unused Memory
353: Reimplement the Legacy Socket API
357: Migrate from Mercurial to Git
364: ZGC on macOS
365: ZGC on Windows
369: Migrate to GitHub
371: Hidden Classes
373: Reimplement the Legacy DatagramSocket API
376: ZGC: Concurrent Thread-Stack Processing
377: ZGC: A Scalable Low-Latency Garbage Collector
379: Shenandoah: A Low-Pause-Time Garbage Collector
382: New macOS Rendering Pipeline
386: Alpine Linux Port
387: Elastic Metaspace
388: Windows/AArch64 Port
391: macOS/AArch64 Port
Deprecation and Removal
306: Restore Always-Strict Floating-Point Semantics
362: Deprecate the Solaris and SPARC Ports
363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
366: Deprecate the ParallelScavenge + SerialOld GC Combination
367: Remove the Pack200 Tools and API
372: Remove the Nashorn JavaScript Engine
374: Disable and Deprecate Biased Locking
381: Remove the Solaris and SPARC Ports
385: Deprecate RMI Activation for Removal
398: Deprecate the Applet API for Removal
407: Remove RMI Activation
410: Remove the Experimental AOT and JIT Compiler
411: Deprecate the Security Manager for Removal
Incubation
383: Foreign-Memory Access API (Second Incubator)
412: Foreign Function & Memory API (Incubator)
414: Vector API (Second Incubator)

Of course, that categorization is a bit subjective. What I call “plumbing” may be boring to me, but exciting and important for you. And anyway, boring isn’t bad. Without a continuous investment in plumbing, the platform crumbles.

API

There are quite a few API changes that aren’t part of a JEP. You can see all changes at Marc Hoffmann’s nifty API Diff site https://javaalmanac.io/jdk/17/apidiff/11/. However, most of them are quite mundane or specialized. My favorite:

var result = Files.lines(Path.of("/usr/share/dict/words")).filter(w -> w.length() > 20).toList()

Yes, that’s toList and not collect(Collectors.toList()).

Bug Fixes

Finally, bug fixes. If you run this query, you get over 32,000 entries. Each one is an entry in the bug database that was addressed in JDK 12—17. Now that’s impressive.

Not all bug reports are bugs. The bug tracker is also used for API changes, Unicode versions, and so on.

Sure, many of those bugs are obscure. But obscure bugs can be incredibly annoying. I recently ran into Bug 8247546. It took me many hours to realize that it wasn’t my code that was at fault. Had I used Java 17, I wouldn’t have had to go through that. Maybe not many people were affected by this particular obscure issue, but every fixed bug affected someone. In my mind, I multiply my pain by 32,000, and it’s clear that bug fixes are a big deal.

Language Features

There are five significant new language features in JDK 17. Let’s look at them one at a time.

Records

Records are plainly nice. When you have an immutable class with getters for every field, it looks neater as a record. List the fields, toss in some methods, and you are done:

record Point(double x, double y) {
   public double distance(Point q) {
      return Math.hypot(x - q.x, y - q.y);
   }
   public Point translate(double dy, double dy) {
      return new Point(x + dx, y + dy);
   }
}

You get accessors x and y as well as toString, equals, and hashCode for free. More info here.

Note that the class must be immutable. And the accessors don’t follow the JavaBeans convention—they are not getX and getY. So you won’t be converting your JPA entity classes into records.

How important is this feature in practice? The generally accepted wisdom is that it’s of the same order of utility as enum. Nice, but by itself probably not a reason for switching to a new JDK.

Sealed Classes

What about sealed classes? If you have an inheritance hierarchy that is inherently bounded, then sealed classes are nice. I like the example of modeling JSON:

A JSONValue is either a JSONObject, JSONArray, or JSONPrimitive. It can never be something else. And there are exactly four kinds of JSONPrimitive:

public sealed class JSONValue permits JSONObject, JSONArray, JSONPrimitive {
   . . .
}

public sealed class JSONPrimitive extends JSONValue
      permits JSONString, JSONNumber, JSONBoolean, JSONNull {
   . . .
}

For now, the compiler checks that nobody declares spurious subclasses. One day, pattern matching over a sealed class will be able to tell that you don’t need a default class. Sure, it’s nice. But how many hierarchies like that do you have in your code?

Pattern Match for instanceof

Nobody likes to write:

if (obj instanceof Woozle) {
   Woozle woozle = (Woozle) obj;
   woozle.aWoozleMethod();
}

That’s now shorter:

if (obj instanceof Woozle woozle) {
   woozle.aWoozleMethod();
}

Nice. But really, how often do you do this? I really, really hope that your code isn’t brimming with instanceof and casts.

Text Blocks

I regularly have SQL or HTML or regex or some other foreign strings in my Java code. And I’d love to be able to paste like this:

   String myForeignString = some delimiter
paste my foreign content here
some delimiter;

I can now do this. The delimiter is """. What if """ occurs in myForeignContent? I am actually not super concerned about that. If you are, there is a simple fix. Use a \" escape.

But that has a really unfortunate side effect. What if you just want a
\? You have to escape it as \\. I regularly have foreign strings with \ and I wish I didn’t have to double-escape them. There are known ways to avoid this hassle, and it irks me that none was adopted.

I am also mildly peeved at another feature—stripping initial white space. Consider this:

   String myNameInABox = """
      +---+
      |Cay|
      +---+"""

Java takes it upon itself to strip the common whitespace prefix, so that this is the same as

   String myNameInABox = """
+---+
|Cay|
+---+"""

There is some accidental complexity, like what about tabs? It doesn’t do any harm because you can always opt not to indent. Still, I wish the complexity budget had instead been expended to give me copy and paste without worrying about backslashes. Hence my overall “meh” rating.

Switch

Do you use switch? I never do any more, because I’ve more than once been bitten by fall-through due to a forgotten break. But maybe I should. The switch statement has one advantage. It is more efficient than a long sequence of if/else when it can be compiled into a jump table. How long does the sequence have to be? Not that long—after ten or so if/else, a jump table is often a performance win.

Of course, many people use switch because it signals switchiness—branching solely on a value. And they live with the break after each branch.

This has now gotten better:

int numConsonants = switch (season) {
   case "Fall" -> 3;
   case "Summer", "Winter" -> 4;
   case "Spring" ->  5;
   default -> -1;
}

Note the arrows. They indicate that the branches are disjoint. Note that the switch is an expression. It’s like a ? : with multiple branches. How do you know it’s an expression? The syntax doesn’t tell you. You have to observe that switch is in an expression position, being on the right hand side of an =. I don’t love that part, but otherwise, these switch expressions are nice. Probably many of the switch statements in your code can be rewritten into this form. If in fact you commonly use switch.

However, along the way, two more forms of switch were introduced: a switch statement without fallthrough, and a switch expression with fallthrough. That’s a lot of accidental complexity, and on more than one occasion, I’ve been amazed that Java luminaries were entirely unaware of that—see this survey. My advice: The new expression no-fallthrough switch is nice (and would have been even nicer if it hadn’t been called switch). But stay away from the ugly siblings.

Should You Move to the Latest JDK?

If you are currently using JDK 11, I don’t think you’ll be bowled over by the new language and API features. Sure, some of them are nice, but they are not game changers. But I think that’s where so many of the rah-rah blogs are off track. And Sharat Chander is right. The first considerations should be bug fixes and plumbing improvements. It is is a huge engineering effort to keep the JDK alive and well. The default mode should be to partake in the benefits of that effort, and move on from JDK 11 to JDK 17.

I didn’t list the changes from JDK 8, where many projects still are, but you can go through the same exercise of enumerating JEPs and API changes. There are quite a few pleasant API changes between JDK 8 and 11, but only two significant language changes: local type inference (var) and modules.

And modules are the elephant in the update room. As necessary as they are for the long-term evolution of the Java platform, they have caused pain to which Java developers, blessed with a truly remarkable level of backwards compatibility, are unaccustomed.

Moving on from Java 8 can be some amount of work. But it’s not going to get any easier. Standing still is not a long-term option. Java is alive and well, and exciting improvements lie ahead. And the grass isn’t greener on the other side. Better get moving.

Author: Cay Horstmann

Exit mobile version