Last September 2023 a new version of Java was released as the latest LTS (Long Time Support). This 21st version brought lots of new features that will improve performance and clarity in our code base.
But taking advantage of these changes and new features, which we are not used to including in our code, can be a tough task. Also, it can lead to improper use or poor uptake, bugs, or basically not taking full advantage of new improvements.
To help you on that Sonar has released a group of new Java 21 rules that will guide you from the very beginning. You will benefit from the first keystroke with SonarLint in your IDE checking your code as you code, to the CI Quality Gates with SonarQube and SonarCloud.
The 11 rules are as follows:
- Use built-in "Math.clamp" methods
- Use correct ranges with Math.clamp
- Use SequencedCollection reversed() for reverse iteration order
- Use reversed immutable lists with SequencedCollection reversed() view
- Use switch instead of if-else for pattern matching
- Use record pattern matching instead of explicit field access
- Use VirtualThreads for heavy blocking operations
- Don't misuse Thread methods with Virtual Threads
- Virtual threads should not run blocks with synchronized code
- Use guarded pattern labels instead of if/else
- Use indexOf(char|String, int, int) with correct ranges
Use built-in "Math.clamp" methods
Sometimes you need to bounds check a number, ensuring that the value is not out of a certain range. To do this we’ve been using manual checks like these ones
These 2 options are hard to read and understand, and error-prone. The first one using the nested ternary operator overcomplicates the code, making it difficult to understand the intention. The second one with the Math methods needs a deep read in order to understand it.
Which is the best approach then?
The new Java 21 Math.clamp method is clear, and focused and reduces the options to include a bug.
Use correct ranges with Math.clamp
When you use the Math.clamp method from Java 21 as suggested by the previous rule, you need to use the correct ranges, like other range-based APIs. This method throws IllegalArgument exceptions when the ranges are not considered legal.
The following example throws an IllegalArgumentException
The following example is a redundant operation
Use SequencedCollection reversed() for reverse iteration order
When you need to iterate a collection but in reverse order, often you do manual processes using the iterator.
This approach is verbose, hard to understand, and also can lead to errors if we don’t do the right previous/hasPrevious calls.
Java 21 introduces the new Sequenced Collections API, which is applicable to all collections with a defined sequence on their elements, such as `LinkedList`, `TreeSet`, and others.
This approach is way clearer, doesn’t give space to do it wrong, and ensures consistency across your code.
Use reversed immutable lists with SequencedCollection reversed() view
Sometimes you need to iterate a collection in reverse order, and you have to do it manually, using the `Collections.reverse` method which mutates the list. Mutability can bring problems, especially in this case mutating the original list just to use a reversed view of it. Almost always immutable approaches are preferred.
Java 21 introduces the new Sequenced Collections API, which is applicable to all collections with a defined sequence on their elements, such as `LinkedList`, `TreeSet`, and others.
For projects using Java 21 and onwards, this API should be utilized instead of workaround implementations that were necessary prior to Java 21.
For read-only usages of reverse iterations, the old `Collection.reverse(List)` call should be replaced by `SequencedCollection.reversed()` which will not mutate the original collection.
Should be changed to
Use switch instead of if-else for pattern matching
In versions of Java before 21, matching a variable against multiple patterns required you to chain if/else statements. However, since Java 21, the enhanced switch expression is a preferable alternative in most scenarios.
Using a switch expression provides advantages such as clearer code, assurance of handling all cases, and improved performance.
But we can use `switch expressions` in order to make this code more readable, and also reduce the cognitive complexity.
Use record pattern matching instead of explicit field access
When you use type pattern matching you also declare a local variable of the type you matched against, to easily access its specific members, which is a benefit on top of the use of the instanceOf conditionals.
With Java 21 we can now go a step further when we type-match on records, directly extracting their components into local variables, improving readability, and reducing the possibility of introducing errors with bad or missing assignments.
Use VirtualThreads for heavy blocking operations
Java 21 comes with a powerful feature called Virtual Threads. Before this, when you created a new Thread it was taking a thread from the OS. This basically meant that depending on the CPU we were capable of creating only a specific number of threads.
But now these virtual threads come from a shared pool of OS threads allowing us to create millions of threads that will be put on hold for access to the IO system.
So, using virtual threads is the suggested approach.
Don't misuse Thread methods with Virtual Threads
If you want to migrate from the use of platform Threads to the new Java 21 Virtual Threads there are some methods that you should not use since they don’t make any sense for the new type and can even cause runtime errors.
In the old platform threads, we could have a code similar to this
However, the 3 central methods will have no effect or result in a runtime exception when migrated to Virtual Threads.
Virtual threads are always daemon threads, so invoking .setDaemon() will not change them to non-daemon threads. It will, at best, have no effect, and at worst (when you pass false as a parameter) cause an IllegalArgumentException.
The same goes for .setPriority because the priority of virtual threads cannot be changed from Thread.NORM_PRIORITY, and finally virtual threads are not active members of a ThreadGroup, therefore invoking .getThreadGroup() on a virtual thread returns a dummy "VirtualThreads" group that is empty.
Virtual threads should not run blocks with synchronized code
The CPU usage optimization introduced with VirtualThread relies on the fact that these new types of threads can be “mounted” and “dismounted” from an OS thread whenever they find themselves waiting for some blocking operation ( I/O, network, etc..).
When the task wrapped by the virtual thread runs synchronized code, which will prevent other threads from entering that method, it will get pinned to its current underlying OS thread.
If during this time a blocking operation occurs, the virtual thread will not be dismounted, blocking the OS thread, and defeating the purpose of using a virtual thread in the first place.
In order to obtain the best result from the Virtual Threads we should not use synchronized blocks that will block the thread.
Use guarded pattern labels instead of if/else
When we check for the type of an object, often it also involves checking the object value. Even when we use pattern matching to make the code more readable and avoid the use of `instanceOf`, our code is still not using all the benefits of the Java language.
Guards are a safe and clear approach when evaluating different branches in our code but have preconditions that will make the code that follows irrelevant. So, using a guard instead of a control flow operation inside the pattern body makes the code more readable.
This is a common Java code using switch pattern matching and conditionals:
But, we can go further. Java 21 implements guarded pattern labels that can be used in switch pattern matching expressions that will make the code more readable.
Use indexOf(char|String, int, int) with correct ranges
Java 21 adds new indexOf methods that accept ranges rather than single start or stop indices. While these new API methods make it easier to provide ranges rather than having to do substring operations and adding/subtracting resulting offsets, they also throw StringIndexOutOfBounds exceptions when the range used is not considered legal.
The following cases all throw a StringIndexOutOfBoundsException but are not detected at compile time.
Conclusion
Java 21 brings a lot of new features and methods that will help us to code in a more consistent way. But it’s easy to not be aware of them or miss their usage as it’s a relatively new version.
Clean code also means using our programming language in the best possible way, including taking advantage of the methods provided to solve problems in a more efficient and consistent way, and doing it without misusages especially when migrating code from an older version of Java to a newer one.
The use of tools on the coding side can help us discover the best ways to code using the last features and improve our code in performance and readability.
Remember that SonarLint, SonarQube, and SonarCloud with their Java analyzer will help you deliver clean code with a long list of rules to consider when you code.