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.
[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>
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
.
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.
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:
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:
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:
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:
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:
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:
the switch statement does not cover all possible input values
This bug has been fixed in Java 18.
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.
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:
Boolean
and Memory Address
in memory access var handlesThe 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 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.
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.
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.
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:
class M {
class T {
}
}
Before JDK 18, the program would be translated as follows:
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.
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.
As part of changing Java to use UTF-8 by default, the following code could be problematic, especially on a Windows system:
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 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
.
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.
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
java.security
configuration file, or override it by using the java.security.properties
system property."SHA1 usage SignedJAR & denyAfter 2019-01-01"
from the jdk.certpath.disabledAlgorithms
security property."SHA1 denyAfter 2019-01-01"
from the jdk.jar.disabledAlgorithms
security property.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.
Pages generated by the standard doclet provide improved navigation controls when they are viewed on small-screen devices.
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
Given those problems, developers are advised to use the following alternative techniques to avoid resource leaks:
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.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.
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
finalize()
in that class’s CodeSource
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.
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.
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.
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.
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 (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
.
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 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.
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.
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
Next Post