The hidden gems in Java 18: The subtle changes

May 6, 2022 | 15 minute read
Text Size 100%:

Not all the enhancements in a Java release are found in the numbered JDK Enhancement Proposals.

Download a PDF of this article

There is much more to a Java release than official JDK Enhancement Proposals (JEPs). Java 18, released in March, has many other small features, additions, enhancements, bug fixes, deprecations, and removals, which I will demonstrate.

The companion article, “The not-so-hidden gems in Java 18: The JDK Enhancement Proposals,” reviews Java 18’s JEPs, not counting the previews, incubators, and deprecations. Those other JEPs are covered in this article, with an emphasis on what is changed from the previous version.

(Are you still using an earlier version of Java? Here’s an article about Java 16 and Java 17 hidden gems.)

Because these articles were researched prior to the March 22 general availability date, I used the Java 18 RC-build 36 jshell tool to demonstrate the code. However, if you would like to test the features, you can follow along with me by downloading the latest release candidate (or the general availability version), firing up a terminal window, checking your version, and running jshell, as follows. Note that you might see a newer version of the build.

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
[mtaman]:~ java -version
 openjdk version "18" 2022-03-22
 OpenJDK Runtime Environment (build 18+36-2087)
 OpenJDK 64-Bit Server VM (build 18+36-2087, mixed mode, sharing)

[mtaman]:~ jshell --enable-preview
|  Welcome to JShell -- Version 18
|  For an introduction type: /help intro

jshell>

JEP 420: Pattern matching for switch (second preview)

The Java language has been enhanced with pattern matching for switch expressions and statements, along with extensions to the language of patterns. In Java 18, JEP 420 is a second preview of pattern matching for switch.

This feature was first previewed in JDK 17 as JEP 406, and I wrote about it in “Hidden gems in Java 16 and Java 17, from Stream.mapMulti to HexFormat.”

The second preview extends pattern matching for switch to allow an expression to be tested against several patterns, each with a specific action so that complex data-oriented queries can be expressed concisely and safely. Java 18 has been extended to cover two corner cases: One is an improvement and the other is a bug fix.

Dominance test enhancement. In Java 17, the following code leads to the following compilation error: This case label is dominated by a preceding case label 'Integer number':5.

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
public static void main(String[] args) {
   Object obj = ...;
   switch (obj) {
       case Integer number                  -> System.out.println(number);  //line 4
       case Integer number && number > 20   -> System.out.println("Big");  //line 5
   }
      }

The reason is evident: The broader pattern in line 4 dominates the narrower pattern in line 5. Therefore, if obj is of type Integer, it will always match the pattern in line 4—thus, no object will ever be matched by the pattern in line 5.

However, you can rearrange the code to compile successfully by combining one more case that has not been considered for validation yet: the combination of the guarded pattern (the pattern with &&) and a constant. That is allowed up to Java 17.

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
public static void main(String[] args) {
   Integer obj = 7;
   switch (obj) {
     case Integer number && number >= 6 -> System.out.println("Big");   // line 4
     case 7                             -> System.out.println("I am here 7");      // line 5
     case Integer number                -> System.out.println(number);
   }
}

Even though obj equals the constant 7, which should be matched with the pattern in line 5, it will always be matched with the pattern in line 4 because its value is greater than 6. Since this constitutes an unreachable case label, it is not required, and in Java 18, the compiler will show the following error:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
error: this case label is dominated by a preceding case label
      case 7                   -> System.out.println("I am here 7");
             ^

Exhaustion analysis for sealed types. Sealed classes allow exhaustion analysis; that is, the compiler can check whether a switch statement or expression covers all possible cases. A bug is fixed in Java 18 for when you work with sealed classes and generics. The following is an example I took from JEP 420:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
sealed interface I<T> permits A, B {}
final class A<X> implements I<String> {}
final class B<Y> implements I<Y> {}

The following code will not compile in either Java 17 or Java 18:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
I<Long> i = ...
switch (i) {
case A<Long> a -> System.out.println("It's an A");  // not compilable
case B<Long> b -> System.out.println("It's a B");
}

Instead, you will see the following error:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
incompatible types: I<Long> cannot be converted to A<Long>

Both Java versions recognize that I<Long> cannot be converted to A<Long>, because A<Long> is an I< String>. Therefore, a possible solution, due to the sealed classes hierarchy, is that the only class that can implement I<Long> is B<Long>, as follows:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
I<Long> i = ...
switch (i) {
  case B<Long> b -> System.out.println("It's a B");
}

If you compile the previous code with Java 17, it will report the following error:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
the switch statement does not cover all possible input values

This bug has been fixed in Java 18.

JEP 417: Vector API (third incubator)

The Vector API was introduced in Java 16 and Java 17 as an incubator feature. And it is incubated for a third time in JDK 18. This API is about mathematical vector computation and mapping to modern single instruction, multiple data (SIMD) architectures.

With Java 18, the third incubator has again improved the performance of vector operations that accept masks on architectures that support masking in hardware. It also adds support for the ARM Scalable Vector Extension, an optional extension to the ARM64 platform.

JEP 419: Foreign Function and Memory API (second incubator)

A second incubation of the Foreign Function and Memory API has been introduced in Java 18. It allows Java programs to interact with code and data outside the Java runtime.

The API allows Java programs to call native libraries and process native data without the brittleness and danger of JNI (Java Native Interface), which was also highly complicated to implement, error-prone, and slow.

The new API is being developed within Project Panama and is intended to replace JNI—which has been part of the Java platform since 1.1—with a superior, pure Java development model. This API was first incubated in JDK 17.

In JDK 18, the second incubator’s refinements are focused on the following:

  • More carriers such as Boolean and Memory Address in memory access var handles
  • A new API to copy Java arrays to and from memory segments
  • Reducing the implementation effort by 90% and accelerating API performance by a factor of 4 to 5

ZGC, SerialGC, and ParallelGC support string deduplication

The JVM garbage collector can detect strings whose code and value fields contain the same bytes. To reduce the memory footprint, the garbage collector deletes all but one of these byte arrays and reassigns all the string instance references to the remaining byte array. However, note that the strings themselves are not actually deduplicated as the feature name implies—only their byte arrays are.

String deduplication was first released in Java 8u20 for the G1 garbage collector under JEP 192, back in 2017. It is the same JEP that was used to implement this feature for the garbage collectors mentioned before; therefore, there is no separate JEP to include this functionality in Java SE 18 for ZGC, SerialGC, and ParallelGC.

ZGC was released for production in Java 15, and as of Java 18, ZGC, SerialGC, and ParallelGC support string deduplication.

String deduplication is disabled by default, and it must be explicitly enabled via -XX:+UseStringDeduplication in the JVM.

The subject.getSubject and doAs APIs have been replaced

The javax.security.auth.Subject class has two new methods. The first is current(), which returns the current subject. The second is callAs(Subject, Callable<T>), which executes a Callable with subject as the current subject. These methods have been created as replacements for the existing methods javax.security.auth.Subject::getSubject and javax.security.auth.Subject::doAs, which have been deprecated for removal because they depend on Security Manager APIs deprecated in Java 17’s JEP 411.

Charset.forName() takes a fallback default value

The existing java.nio.charset.Charset#forName() method throws UnsupportedCharsetException or IllegalCharsetNameException if the character set (charset) name is unknown or is not supported. The caller always must use try-catch with the exception to determine whether the desired charset is returned.

A new overloaded method called forName(String charsetName, Charset fallback) has been added to the Charset class, which takes the fallback Charset object. This method was proposed during the review of JEP 400, which specifies UTF-8 as the default charset. The new method returns the specified fallback value instead of throwing exceptions if the named charset is unavailable.

G1 heap regions as large as 512 MB are allowed

The G1 garbage collector usually determines the size of the heap region, based on the heap size, but until now, the heap region size was limited to between 1 MB and 32 MB due to previous limitations in the set data structures.

Thus, the JVM will have approximately 2,048 regions and will set the heap region size accordingly from 1 MB to 32 MB with power of 2 bounds. The following is an important parameter that decides what size of object can be stored in a region:

Heap region size = Heap size/2048

You can overwrite the adaptive selection of the region size by using the new JVM parameter -XX:G1HeapRegionSize=n.

In Java 18, this enhancement increases the maximum heap region size for the G1 garbage collector from 32 MB to 512 MB. The ergonomic default heap region size selection remains limited to 32 MB maximum.

This enhancement can help reduce both inner and outer fragmentation issues when you’re working with large objects on large heaps. Furthermore, using a larger heap region size on large heaps may reduce internal region management overhead while improving performance due to larger local allocation buffers.

Enclosing instance fields are omitted from inner classes that don’t use them

Prior to JDK 18, when the Java compiler compiled an inner class, it always generated a private synthetic field with a name that started with this$ to reference the enclosing instance—even if the inner class did not reference its enclosing instance and the field was unused.

Beginning with JDK 18, unused this$ fields are removed; the field is generated only for inner classes that refer to their enclosing instance.

For example, consider the following program:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
class M {
    class T {
    }
}

Before JDK 18, the program would be translated as follows:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
class M {
    class T {
        private synthetic M this$0;
        T(M this$0) {
            this.this$0 = this$0;
        }
    }
}

Starting in JDK 18, the unused this$0 field is omitted.

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
class M {
    class T {
        T(M this$0) {}
    }
}

If the enclosing instances were previously reachable only via a reference in an inner class, the change might cause them to be garbage collected sooner. This is usually desirable because it eliminates a potential source of memory leaks when inner classes are created that are meant to outlive their enclosing instance.

Note that subclasses of java.io.Serializable are not affected by this change.

Default charset inherited for PrintWriter that wraps PrintStream

As part of changing Java to use UTF-8 by default, the following code could be problematic, especially on a Windows system:

Copied to Clipboard
Error: Could not Copy
Copied to Clipboard
Error: Could not Copy
new PrintWriter(System.out)

The issue arises when PrintStream does not use the same charset as System.out. This must be changed because JEP 400 allows the default charset to be UTF-8, which is especially important on Windows systems, where the encoding of System.out is not UTF-8. This mismatch would cause issues if PrintStream were wrapped in a UTF-8 PrintWriter.

To address this, in Java 18, the constructors of java.io.PrintStream, PrintWriter, and OutputStreamWriter that take a java.io.OutputStream argument with no charset now inherit the charset when the output stream is a PrintStream. And as part of this change, java.io.PrintStream now defines a charset() method to return the print stream’s charset.

The java.security.manager system property default value changed to disallow

The default value of the java.security.manager system property has been changed to disallow in Java 18. Unless the system property is set to allow on the command line, any non-null argument to System.setSecurityManager(SecurityManager) will throw an UnsupportedOperationException.

Long process non-strong reference times have been fixed in ZGC

ZGC had a bug that, on rare occasions, blocked significant progress and caused latency and throughput issues for a Java application. This bug caused long concurrent-process non-strong reference times with ZGC. That has been fixed in Java 18.

SHA-1-signed JARs have been disabled

Any JARs signed with SHA-1 algorithms are now restricted and treated as if they were unsigned by default. This includes the algorithms used to digest, sign, and, if desired, time stamp a JAR.

This change also applies to the signature and digest algorithms of the certificates in the code signer’s and time stamp authority’s certificate chains and to any certificate revocation lists (CRLs) or Online Certificate Status Protocol (OCSP) responses used to determine whether those certificates have been revoked.

There is one exception to this policy to reduce the compatibility risk for previously time stamped applications: Any JAR signed with SHA-1 algorithms and time stamped before January 1, 2019, is unrestricted. This exception might be removed in a subsequent Java release.

You can remove these restrictions, but you do so at your own risk. To remove the restrictions

  • Modify the java.security configuration file, or override it by using the java.security.properties system property.
  • Remove "SHA1 usage SignedJAR & denyAfter 2019-01-01" from the jdk.certpath.disabledAlgorithms security property.
  • Remove "SHA1 denyAfter 2019-01-01" from the jdk.jar.disabledAlgorithms security property.

Exceptions and errors have been merged into exception classes

In Java 18, the Javadoc documentation exceptions and errors tabs have been merged into a single exception classes tab, which includes all exception classes. This is defined in Java Language Specification section 11.1.1.

Navigation has been improved for small devices

Pages generated by the standard doclet provide improved navigation controls when they are viewed on small-screen devices.

Finalization has been deprecated for removal

JEP 421 is to deprecate finalization for removal in a future Java release. The finalizer has flaws that cause significant real-world security, performance, reliability, and maintainability problems. It also has a problematic programming model.

The proposal aims to help developers understand the danger of finalization, prepare them for its eventual removal, and provide simple tools to help detect reliance on finalization. Introduced in Java 1.0, finalization was intended to help avoid resource leaks.

Here’s the problem: A class can declare a finalizer—the method protected void finalize()—whose body releases any underlying resource. The garbage collector will schedule the finalizer of an unreachable object to be called before it reclaims object memory. In turn, the finalize method can take actions such as calling the object’s close.

This approach seems like an effective safety net for preventing resource leaks, but some flaws exist, including

  • Unpredictable latency, with a long time elapsed between an object becoming unreachable and its finalizer being called
  • Unconstrained behavior, with finalizer code able to take any action, including resurrecting an object and making it reachable again
  • A finalizer always being enabled, with no explicit registration mechanism
  • Finalizers being able to run on unspecified threads in an arbitrary order

Given those problems, developers are advised to use the following alternative techniques to avoid resource leaks:

  • The Java 7 try-with-resources statements automatically generate a finally block for all classes that implement the AutoCloseable interface, in which the corresponding close() methods are called. Note that some static code analysis tools find and complain about code that does not generate AutoCloseable objects inside try-with-resources blocks.
  • Java 9 introduced cleaner actions through the Cleaner API, which can be registered to manage a set of object references and corresponding cleaning actions. The garbage collector invokes those actions when an object is no longer accessible (not only when it reclaims its memory). However, the cleaner actions don’t have access to the object itself (so they can’t store a reference to it); you need to register them for an object when that specific object needs them, and then you can determine in which thread they are called.

Remember, finalization is still enabled by default for now. It will be disabled by default in a feature release and removed altogether in a future Java release. But you can disable it now to help you test applications before migrating them to a future Java version where finalization has been removed. Use the JVM option --finalization=disabled to completely disable finalization in the standard Java API.

A new Java Flight Recorder event was added for finalization

To help developers prepare for the removal of finalizers from their code, a new Java Flight Recorder event, jdk.FinalizerStatistics, identifies at runtime the classes that use finalizers. This event is enabled by default in the JDK through the Java Flight Recorder configuration files profile.jfc and default.jfc.

With this new event, the Java Flight Recorder will note each instantiated class that contains a nonempty finalize() method by emitting a jdk.FinalizerStatistics event. The event includes

  • The number of times the class’s finalizer has run
  • The class that overrides finalize() in that class’s CodeSource
  • The number of objects still on the heap that are not yet finalized

By the way, if finalization has been disabled with the JVM --finalization=disabled option, no jdk.FinalizerStatistics events are emitted. For information about using the Java Flight Recorder, see the user guide.

Thread.stop() was terminally deprecated

The Thread.stop() method is inherently unsafe and has been deprecated since Java 1.2 back in 1998, but you could still use it. In Java 18, the method is terminally deprecated so that it can be degraded in a future release and eventually removed. Ideally Thread.stop() will be deleted with suspend() and resume() and the corresponding ThreadGroup methods.

Subject::doAs methods have been deprecated for removal

As a part of the ongoing effort to remove the Security Manager–related APIs, two doAs methods of the javax.security.auth.Subject class have been deprecated for removal.

-XX:G1RSetRegionEntries and -XX:G1RSetSparseRegionEntries have been obsoleted

The options -XX:G1RSetRegionEntries and -XX:G1RSetSparseRegionEntries have been obsoleted, representing a complete refactoring from JDK-8017163.

JDK-8017163 implements an entirely new remembered set in which these two options no longer apply. In Java 18, neither -XX:G1RSetRegionEntries nor -XX:G1RSetSparseRegionEntries have a function, and their usage will cause JVM to trigger an obsoletion warning.

The legacy PlainSocketImpl and PlainDatagramSocketImpl have been removed

By default, the legacy implementation of SocketImpl has not been used since JDK 13, and by default, the legacy implementation of DatagramSocketImpl has not been used since JDK 15.

Therefore, the legacy implementations of java.net.SocketImpl and java.net.DatagramSocketImpl, alongside support for the system properties jdk.net.usePlainSocketImpl and usePlainDatagramSocketImpl used to select these implementations, have been removed from JDK 18. Setting these properties now has no effect.

Support for pre-JDK 1.4 DatagramSocketImpl implementations has been removed

Support for pre-JDK 1.4 DatagramSocketImpl implementations (that is, DatagramSocketImpl implementations that don’t support connected datagram sockets, peeking, or joining multicast groups on a specific network interface) has been dropped in Java 18.

However, if you try to call disconnect() or connect() on a MulticastSocket or DatagramSocket that uses an old implementation, it will now throw SocketException or UncheckedIOException.

Similarly, calling leaveGroup() or joinGroup() will throw an AbstractMethodError.

Empty finalize() methods in the java.desktop module have been removed

The java.desktop module had a few implementations of finalize() that did nothing. As part of JEP 421, finalizers have been deprecated for removal. These methods were deprecated in Java 9 and terminally deprecated in Java 16, and they have been removed from Java 18.

The impl.prefix JDK system property was removed from InetAddress

The undocumented but sometimes used impl.prefix system property dates to early JDK releases when it was possible to implement both the JDK internal and nonpublic java.net.InetAddressImpl interface to the java.net package and have the java.net.InetAddress API use it.

That is no longer possible; impl.prefix is no longer a system property. The new JEP 418 internet address resolution service provider interface (SPI) defines a standard method for implementing a name and address resolver using the InetAddressResolver SPI.

Conclusion

This article looked at many of the small—but significant—changes in Java 18. The companion article, “The not-so-hidden gems in Java 18: The JDK Enhancement Proposals,” looks at the core JEPs in Java 18.

Dig deeper

Mohamed Taman

Mohamed Taman (@_tamanm) is CEO of SiriusXI Innovations and a Chief Solutions Architect for Nortal. He is a Java Champion, an Oracle ACE, a Jakarta EE ambassador, a JCP member, and a member of the Adopt-a-Spec program for Jakarta EE and Adopt-a-JSR for OpenJDK. Taman is Egyptian and based in Belgrade, Serbia.


Previous Post

Curly Braces #4: Network data transmission and compression in Java

Eric J. Bruno | 10 min read

Next Post


The not-so-hidden gems in Java 18: The JDK Enhancement Proposals

Mohamed Taman | 16 min read