Skip to content

Java 18 Features (with Examples)

A 18 laid out of roasted coffee beans on a light oak board in front of a turquoise wall, in front of it an espresso cup on a silver platter with its cloche lifted off, jasmine sprigs in a glass of water on the right

Java 18, released on March 22, 2022, is the first “interim” release after the Long-Term-Support (LTS) release Java 17.

With nine implemented JEPs, the scope of changes in Java 18 has decreased significantly compared to the previous releases. After the LTS release of Java 17, the JDK developers have earned a little breather.

UTF-8 by Default

For a long time, we Java developers had to deal with the fact that the standard Java character set varied depending on the operating system and language settings.

Java 18 changed that.

The Problem

The Java standard character set determines how Strings are converted to bytes and vice versa in numerous methods of the JDK class library (e.g., when writing and reading a text file). These include, for example:

  • the constructors of FileReader, FileWriter, InputStreamReader, OutputStreamWriter,
  • the constructors of Formatter and Scanner,
  • the static methods URLEncoder.encode() and URLDecoder.decode().

Up to Java 17, this could lead to unpredictable behavior when an application was developed and tested in one environment – and then run in another (where Java chose a different default character set).

For example, let’s run the following code on Linux or macOS (the German text means “Happy coding!”):

try (FileWriter fw = new FileWriter("happy-coding.txt");
    BufferedWriter bw = new BufferedWriter(fw)) {
  bw.write("Fröhliches Programmieren!");
}

And then, we load this file with the following code on Windows with Java 17:

try (FileReader fr = new FileReader("happy-coding.txt");
    BufferedReader br = new BufferedReader(fr)) {
  String line = br.readLine();
  System.out.println(line);
}

Then the following is displayed:

Fröhliches Programmieren!

That is because Linux and macOS store the file in UTF-8 format, and Windows tries to read it in Windows-1252 format. In UTF-8, the “ö” takes up two bytes, and Windows-1252 reads them as two characters: “Ô and “¶”.

The Problem – Stage Two

It becomes even more chaotic because newer class library methods do not respect the default character set but always use UTF-8 if no character set is specified. These methods include, for example, Files.writeString(), Files.readString(), Files.newBufferedWriter(), and Files.newBufferedReader().

Let’s start the following program, which writes the text via FileWriter and reads it directly afterward via Files.readString():

try (FileWriter fw = new FileWriter("happy-coding.txt");
    BufferedWriter bw = new BufferedWriter(fw)) {
  bw.write("Fröhliches Programmieren!");
}

String text = Files.readString(Path.of("happy-coding.txt"));
System.out.println(text);

Linux and macOS display the text correctly. On Windows with Java 17, however, the program terminates with an exception:

Exception in thread "main" java.nio.charset.MalformedInputException: Input length = 4

That is because, on Windows, FileWriter writes the file using the standard Java character set Windows-1252, but Files.readString() reads the file back in as UTF-8 – regardless of the standard character set. Windows-1252 stores the “ö” as a single byte (hexadecimal F6), and that is not valid UTF-8.

Possible Solutions to Date

For protecting an application against such errors, there were two possibilities before Java 18:

  1. Specify the character set when calling all methods that convert strings to bytes and vice versa.
  2. Set the default character set via system property file.encoding.

The first option leads to a lot of code duplication and is thus messy and error-prone:

FileWriter fw = new FileWriter("happy-coding.txt", StandardCharsets.UTF_8);
// ...
FileReader fr = new FileReader("happy-coding.txt", StandardCharsets.UTF_8);
// ...
Files.readString(Path.of("happy-coding.txt"), StandardCharsets.UTF_8);

Specifying the character set parameters also prevents us from using method references, as in the following example:

Stream<String> encodedParams = ...
Stream<String> decodedParams = encodedParams.map(URLDecoder::decode);

Instead, we would have to write:

Stream<String> encodedParams = ...
Stream<String> decodedParams =
    encodedParams.map(s -> URLDecoder.decode(s, StandardCharsets.UTF_8));

The second possibility (system property file.encoding) was firstly not officially documented up to and including Java 17 (see system properties documentation).

Secondly, as explained above, the character set specified is not used for all API methods. So the variant is also error-prone, as we can show with the example from above:

public class Jep400Example {
  public static void main(String[] args) throws IOException {
    try (FileWriter fw = new FileWriter("happy-coding.txt");
        BufferedWriter bw = new BufferedWriter(fw)) {
      bw.write("Fröhliches Programmieren!");
    }

    String text = Files.readString(Path.of("happy-coding.txt"));
    System.out.println(text);
  }
}

Let’s compile the program and run it with the default encoding ISO-8859-1:

$ javac Jep400Example.java
$ java -Dfile.encoding=ISO-8859-1 Jep400Example
Exception in thread "main" java.nio.charset.MalformedInputException: Input length = 4

The program terminates because FileWriter takes the default encoding into account, but Files.readString() ignores it and always uses UTF-8. So this variant only works reliably if you use UTF-8 uniformly:

$ java -Dfile.encoding=UTF-8 Jep400Example
Fröhliches Programmieren!

JEP 400 to the Rescue

With JDK Enhancement Proposal 400, the problems mentioned above have – at least for the most part – been a thing of the past since Java 18.

The default encoding is always UTF-8 regardless of the operating system, locale, and language settings.

Also, the system property file.encoding has been documented since Java 18 – and we can use it legitimately. However, we should do this with caution. JEP 400 did not change the fact that the Files methods ignore the configured default encoding.

According to the documentation, only the values UTF-8 and COMPAT should be used anyway, with UTF-8 providing consistent encoding and COMPAT simulating pre-Java 18 behavior. All other values lead to unspecified behavior.

The best way is always to set -Dfile.encoding to UTF-8 or omit it altogether.

Reading the Encodings at Runtime

The current default encoding can be read at runtime via Charset.defaultCharset() or the system property file.encoding. Since Java 17, the system property native.encoding can be used to read the encoding, which – before Java 18 – would be the default encoding if none is specified:

System.out.println("Default charset : " + Charset.defaultCharset());
System.out.println("file.encoding   : " + System.getProperty("file.encoding"));
System.out.println("native.encoding : " + System.getProperty("native.encoding"));

Without specifying -Dfile.encoding, the program prints the following on Linux and macOS with Java 17 and Java 18:

Default charset : UTF-8
file.encoding   : UTF-8
native.encoding : UTF-8

On Windows and Java 17, the output is as follows:

Default charset : windows-1252
file.encoding   : Cp1252
native.encoding : Cp1252

And on Windows and Java 18:

Default charset : UTF-8
file.encoding   : UTF-8
native.encoding : Cp1252

So the native encoding on Windows remains the same, but the default encoding changes to UTF-8 according to this JEP.

The Previous “Default” Character Set

If we run the little program from above on Linux or macOS and Java 17 with the -Dfile.encoding=default parameter, we get the following output:

Default charset : US-ASCII
file.encoding   : default
native.encoding : UTF-8

This is because, up to Java 17, the name default was recognized as an alias for the encoding US-ASCII.

Since Java 18, default is no longer recognized; the output looks like this:

Default charset : UTF-8
file.encoding   : default
native.encoding : UTF-8

The system property file.encoding is still default – but at this point, we would also see any other invalid input. For an invalid file.encoding input, the default character set is UTF-8 – in Java 17 as well as in Java 18.

Charset.forName() Taking Fallback Default Value

Independent of JEP 400, the new method Charset.forName(String charsetName, Charset fallback) has been available since Java 18. It returns the specified fallback value instead of throwing an IllegalCharsetNameException or an UnsupportedCharsetException if the character set name is unknown or the character set is not supported.

There is no JEP for this change; it is tracked in the bug tracker under JDK-8270490.

Simple Web Server

Almost all modern programming languages allow starting up a rudimentary HTTP server to, for example, quickly test some web functionality.

Through JDK Enhancement Proposal 408, Java also offers this possibility as of version 18.

The easiest way to start the provided webserver is the jwebserver command. It starts the server on localhost:8000 and provides a file browser for the current directory:

$ jwebserver
Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving /home/sven and subdirectories on 127.0.0.1 port 8000
URL http://127.0.0.1:8000/

As shown, you can use the -b parameter to specify the IP address on which the server should listen. With -p, you can change the port and with -d the directory the server should serve. With -o, you can configure the log output. For example:

$ jwebserver -b 127.0.0.100 -p 4444 -d /tmp -o verbose
Serving /tmp and subdirectories on 127.0.0.100 port 4444
URL http://127.0.0.100:4444/

You get a list of options with explanations with jwebserver -h.

Web Server Features

The web server is very rudimentary and has the following limitations:

  • The only supported protocol is HTTP/1.1.
  • HTTPS is not provided.
  • Only the HTTP GET and HEAD methods are allowed.

Java API: SimpleFileServer

jwebserver is a launcher for the jdk.httpserver module. You can achieve the same with java -m jdk.httpserver. In both cases, SimpleFileServerImpl.start(…) is called – by jwebserver via the class sun.net.httpserver.simpleserver.JWebServer, by java -m jdk.httpserver via the class Main in the same package.

SimpleFileServerImpl.start(…) evaluates the command line parameters and finally creates the server via SimpleFileServer.createFileServer(…).

With SimpleFileServer.createFileServer(…), you can also start a server via Java code:

HttpServer server =
    SimpleFileServer.createFileServer(
        new InetSocketAddress(8080), Path.of("/tmp"), OutputLevel.INFO);
server.start();

Using the Java API, you can extend the web server. You can, for example, make specific directories of the file system accessible via different HTTP paths, and you can extend the server with your own handlers for certain paths and HTTP methods (e.g., PUT).

A complete tutorial is beyond the scope of this article. See the “API” and “Enhanced request handling” sections in the JEP for more details.

Code Snippets in Java API Documentation

Before Java 18, if we wanted to integrate multiline code snippets into JavaDoc, we had to do this cumbersomely via <pre>…</pre> – optionally in combination with {@code … }. For this, we had to pay attention to two things:

  1. There must be no line breaks between <pre> and the code and between the code and </pre>.
  2. The code starts directly after the asterisks; i.e., if there are spaces between the asterisks and the code, they also appear in the JavaDoc. So the code must be shifted one character to the left compared to the rest of the text in the JavaDoc comment.

Here is an example with <pre>:

/**
 * How to write a text file with Java 7:
 *
 * <pre><b>try</b> (BufferedWriter writer = Files.<i>newBufferedWriter</i>(path)) {
 *  writer.write(text);
 *}</pre>
 */

And one with <pre> and {@code … }:

/**
 * How to write a text file with Java 7:
 *
 * <pre>{@code try (BufferedWriter writer = Files.newBufferedWriter(path)) {
 *  writer.write(text);
 *}}</pre>
 */

The difference between the two variants is that in the first variant, we can format the code with HTML tags such as <b> and <i>, while in the second variant, such tags would not be evaluated but displayed.

The @snippet Tag in Java 18

JDK Enhancement Proposal 413 enhances the JavaDoc syntax with the @snippet tag, specifically designed to display source code. With the @snippet tag, we can write the comment as follows:

/**
 * How to write a text file with Java 7:
 *
 * {@snippet :
 * try (BufferedWriter writer = Files.newBufferedWriter(path)) {
 *   writer.write(text);
 * }
 * }
 */

We can also highlight parts of the code using @highlight – for example, all occurrences of “text” within the second line of code:

/**
 * {@snippet :
 * try (BufferedWriter writer = Files.newBufferedWriter(path)) {
 *   writer.write(text);  // @highlight substring="text"
 * }
 * }
 */

The following example highlights all words starting with “write” within the block marked with @highlight region and @end. With type="…", we can also specify the type of highlighting: bold, italic, or highlighted (with a colored background).

/**
 * {@snippet :
 * // @highlight region regex="\bwrite.*?\b" type="highlighted"
 * try (BufferedWriter writer = Files.newBufferedWriter(path)) {
 *   writer.write(text);
 * }
 * // @end
 * }
 */

With @link, we can link a part of the text, e.g., BufferedWriter, to its JavaDoc:

/**
 * {@snippet :
 * // @link regex="\bBufferedWriter\b" target="java.io.BufferedWriter" :
 * try (BufferedWriter writer = Files.newBufferedWriter(path)) {
 *   writer.write(text);
 * }
 * }
 */

The regular expression with \b (word boundary) ensures that only the type BufferedWriter is linked. With substring="BufferedWriter", the identical part of newBufferedWriter would become a link as well.

Note that the colon at the end of the line with the @link tag is essential in this case; it means that the comment refers to the following line. We could also write the comment at the end of the next line, just like in the first @highlight example – or use @link region and @end to specify a part within which all occurrences of BufferedWriter should be linked.

Integrate Snippets from Other Files

We can also refer to marked code in another file:

/**
 * How to write a text file with Java 7:
 *
 * {@snippet file="FileWriter.java" region="writeFile"}
 */

In the FileWriter.java file, we mark the code as follows:

// @start region="writeFile"
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
  writer.write(text);
}
// @end

For javadoc to find the file, it must be located in a snippet-files subdirectory of the package whose source code contains the @snippet tag – or in a directory that we specify via --snippet-path:

$ javadoc -d docs -sourcepath src --snippet-path snippets eu.happycoders.demo

If neither is the case, javadoc aborts with a “file not found” error.

These were the most important @snippet tags, in my opinion. You can find a complete reference in the JEP.

Internet-Address Resolution SPI

To find out the IP address(es) for a hostname in Java, we can use InetAddress.getByName(…) or InetAddress.getAllByName(…). Here is an example:

InetAddress[] addresses = InetAddress.getAllByName("www.happycoders.eu");
System.out.println("addresses = " + Arrays.toString(addresses));

The code gives me the following output (I added the line breaks manually for better readability):

addresses = [www.happycoders.eu/91.99.59.195,
             www.happycoders.eu/2a01:4f8:1c19:9fc0:0:0:0:1]

For reverse lookups (i.e., resolving an IP address to a hostname), the JDK provides the methods InetAddress::getCanonicalHostName and InetAddress::getHostName.

By default, InetAddress uses the operating system’s resolver, i.e., it usually consults the hosts file and the configured DNS servers.

This hardwiring has a few disadvantages:

  • Within tests, it is not possible to map a hostname to the IP address of a mocked server.
  • New hostname lookup protocols (such as DNS over QUIC, TLS, or HTTPS) cannot be easily implemented in Java.
  • The current implementation leads to a blocking operating system call. That alone is unattractive since this call can sometimes take long and cannot be interrupted. When using virtual threads, this even leads to the point that the operating system thread cannot serve any other virtual threads during this time.

JDK Enhancement Proposal 418 introduces a Service Provider Interface (SPI) to allow the platform’s built-in default resolver to be replaced by other resolvers.

Internet-Address Resolution SPI / JEP 418 – Example

The following example shows how to implement and register a simple resolver that responds to every request with the IP address 127.0.0.1. You can also find the code in this GitHub repository.

We first write the resolver by implementing the java.net.spi.InetAddressResolver interface introduced in Java 18 (class HappyCodersInetAddressResolver in GitHub):

public class HappyCodersInetAddressResolver implements InetAddressResolver {
  @Override
  public Stream<InetAddress> lookupByName(String host, LookupPolicy lookupPolicy)
      throws UnknownHostException {
    return Stream.of(InetAddress.getByAddress(new byte[] {127, 0, 0, 1}));
  }

  @Override
  public String lookupByAddress(byte[] addr) {
    throw new UnsupportedOperationException();
  }
}

Since I only want to present the basic principle here, I kept the resolver as simple as possible, and it does not support reverse lookups.

Second, we need a resolver provider (class HappyCodersInetAddressResolverProvider in GitHub):

public class HappyCodersInetAddressResolverProvider
    extends InetAddressResolverProvider {
  @Override
  public InetAddressResolver get(Configuration configuration) {
    return new HappyCodersInetAddressResolver();
  }

  @Override
  public String name() {
    return "HappyCoders Internet Address Resolver Provider";
  }
}

The provider creates a new instance of the previously implemented resolver in the get() method.

In the third step, we have to register the resolver. To do this, we create a file in the META-INF/services directory with the name java.net.spi.InetAddressResolverProvider and the following content (file in GitHub):

eu.happycoders.jep418.HappyCodersInetAddressResolverProvider

Now we run the code from above again (class Jep418Demo in GitHub):

InetAddress[] addresses = InetAddress.getAllByName("www.happycoders.eu");
System.out.println("addresses = " + Arrays.toString(addresses));

The output now reads:

addresses = [/127.0.0.1]

That is precisely the IP address we returned in our resolver.

Preview and Incubator-Features

In the following sections, you will find preview and incubator features that we already know from previous releases. They are resubmissions with minor changes.

Pattern Matching for switch (Second Preview)

“Pattern Matching for switch” was first introduced in Java 17 and enables switch statements (and expressions) such as the following (for more, see the linked Java 17 article):

switch (obj) {
  case String s && s.length() > 5 -> System.out.println(s.toUpperCase());
  case String s                   -> System.out.println(s.toLowerCase());

  case Integer i                  -> System.out.println(i * i);

  default -> {}
}

JDK Enhancement Proposal 420 introduced two changes in Java 18 – one in dominance checking and one related to exhaustiveness analysis in combination with sealed types.

Improvement of the Dominance Test

I described what dominance checking is in the Java 17 article linked above. In a nutshell, the following code leads to a compiler error:

Object obj = ...
switch (obj) {
  case String s                   -> System.out.println(s.toLowerCase());
  case String s && s.length() > 5 -> System.out.println(s.toUpperCase());
  ...
}

The reason is that the pattern in line 3 “dominates” the longer pattern from line 4: If obj is a String, it is matched by the pattern in line 3, no matter how long it is. So no object is ever matched by the pattern in line 4.

However, one case was not considered in Java 17 – namely, the combination of a constant and a guarded pattern (a pattern with &&). So the following code is allowed in Java 17:

String string = ...
switch (string) {
  case String s && s.length() > 5 -> System.out.println(s.toUpperCase());
  case "foobar"                   -> System.out.println("baz");
  ...
}

However, if string is equal to “foobar”, it is not matched by line 4 but already by line 3 (because it is also longer than five characters).

Since unreachable code is obviously not intended, we get the following compiler error in Java 18:

java --enable-preview --source 18 SwitchTest.java
SwitchTest.java:9: error: this case label is dominated by a preceding case label
      case "foobar"                   -> System.out.println("baz");
           ^

Bugfix in the Exhaustiveness Analysis with Sealed Types

You will learn what exhaustiveness analysis is in the article about sealed types.

I explain the change in Java 18 using the following sealed example class hierarchy from the JEP:

sealed interface I<T> permits A, B {}
final class A<X> implements I<String> {}
final class B<Y> implements I<Y> {}

The following code is not compilable:

I<Integer> i = ...
switch (i) {
  case A<Integer> a -> System.out.println("It's an A");  // not compilable
  case B<Integer> b -> System.out.println("It's a B");
}

Both Java 17 and Java 18 recognize that I<Integer> cannot be converted to A<Integer> (since A<Integer> is an I<String>) and report:

incompatible types: I<Integer> cannot be converted to A<Integer>

In fact, because of the sealed class hierarchy, B<Integer> is the only class that can implement I<Integer>. Thus, the switch statement is complete as follows:

I<Integer> i = ...
switch (i) {
  case B<Integer> b -> System.out.println("It's a B");
}

Java 17 reports here, however:

the switch statement does not cover all possible input values

That is an obvious bug that has been fixed in Java 18.

Vector API (Third Incubator)

The Vector API was already introduced in Java 16 and Java 17 as an Incubator feature. This API is not about the java.util.Vector class from Java 1.0 but about mathematical vector computation and its mapping to modern single-instruction-multiple-data (SIMD) architectures.

JDK Enhancement Proposal 417 has again improved performance and extended support to “ARM Scalable Vector Extension” – an optional extension to the ARM64 platform.

The incubator stage means that the feature can still go through significant changes. The Vector API is still in the incubator stage in Java 27 – in its twelfth round. I explain the reason in the section Status of the Vector API of the Java 27 article.

Foreign Function & Memory API (Second Incubator)

The Foreign Function & Memory API was created in Java 17 by combining the “Foreign Memory Access API” and the “Foreign Linker API”, both of which previously went through several incubator phases.

The new API is being developed within Project Panama and is intended to replace JNI (Java Native Interface), which has already been part of the platform since Java 1.1. JNI allows C code to be called from Java. Anyone who has worked with JNI knows: JNI is highly complicated to implement, error-prone, and slow.

The goal of the new API is to replace JNI with a concise, readable, pure-Java API – with overhead comparable to, if not lower than, that of JNI.

JDK Enhancement Proposal 419 has made extensive changes to the API. In Java 19, the API reached the preview stage, and it has been production-ready since Java 22.

Deprecations and Deletions

Again in Java 18, some features have been deprecated, deprecated for removal, or removed.

Deprecate Finalization for Removal

Finalization has existed since Java 1.0 and is intended to help avoid resource leaks by allowing classes to implement a finalize() method to release system resources (such as file handles or non-heap memory) requested by the operating system.

The garbage collector calls the finalize() method before releasing an object’s memory.

That seems to be a reasonable solution. However, it has been shown that finalization has some fundamental, critical flaws:

Performance:

  • It is unpredictable when the garbage collector will clean up an object (and whether it will do so at all). Therefore, it may happen that – after an object is no longer referenced – it takes a very long time for its finalize() method to be called (or that it is never called).
  • When the garbage collector performs a full GC, there can be noticeable latency if many of the objects being cleaned up have finalize() methods.
  • The finalize() method is called for every instance of a class, even if it is not necessary. There is no way to specify that individual objects do not need finalization.

Security risks:

  • The finalize() method can execute arbitrary code, e.g., storing a reference of the object to be deleted. Thus, the garbage collector will not clean it up. If the reference to the object is later removed and the garbage collector deletes the object, its finalize() method is not called again.
  • When the constructor of a class throws an exception, the object resides on the heap. When the garbage collector later removes it, it calls its finalize() method, which can then perform operations on a possibly incompletely initialized object or even store it in the object graph.

Error-proneness:

  • A finalize() method should always call the finalize() method of the parent class as well. However, the compiler does not enforce this (as it does with the constructor). Even if we write our code without errors, someone else could extend our class, override the finalize() method without calling the overridden method, and thereby cause a resource leak.

Multithreading:

  • The finalize() method is called in an unspecified thread, so thread safety of the entire object must be maintained – even in an application that does not use multithreading.

Alternatives to Finalization

The following alternatives to finalization exist:

  • The “try-with-resources” introduced in Java 7 automatically generates a finally block for all classes that implement the AutoCloseable interface, in which the corresponding close() methods are called. Typical static code analysis tools find and complain about code that does not generate AutoCloseable objects inside “try-with-resources” blocks.
  • Through the Cleaner API introduced in Java 9, so-called “Cleaner Actions” can be registered. The garbage collector invokes them when an object is no longer accessible (not only when it reclaims its memory). Cleaner actions do not have access to the object itself (so they cannot store a reference to it); we only need to register them for an object when that specific object needs them; and we can determine in which thread they are called.

For the above reasons and the availability of sufficient alternatives, the finalize() methods in Object and numerous other classes of the JDK class library were already marked as “deprecated” in Java 9.

JDK Enhancement Proposal 421 marks the methods in Java 18 as “deprecated for removal”.

Furthermore, the VM option --finalization=disabled is introduced, which completely disables finalization. This allows us to test applications before migrating them to a future Java version where finalization has been removed.

As of Java 27, that has not happened: finalization is still enabled by default.

JDK Flight Recorder Event for Finalization

Also new in Java 18 is the Flight Recorder event jdk.FinalizerStatistics. It is enabled by default and logs every instantiated class with a non-empty finalize() method. That makes it easy to identify those classes that still use a finalizer.

These events are not triggered when finalization is disabled via --finalization=disabled.

There is no JEP for this change; it is tracked in the bug tracker under JDK-8266936.

Terminally Deprecate Thread.stop

Thread.stop() has been “deprecated” since Java 1.2. In Java 18, the method was marked as “deprecated for removal”.

There is no JEP for this change; it is tracked in the bug tracker under JDK-8277861.

Deprecate sun.misc.Unsafe Methods That Return Offsets

The sun.misc.Unsafe class has three methods that can be used to determine the position of a field in memory: objectFieldOffset(), staticFieldOffset(), and staticFieldBase(). In Java 18, these three methods were marked as “deprecated”.

As a replacement, the JDK developers recommend the VarHandle class introduced in Java 9, which provides type-safe access to a field.

There is no JEP for this change; it is tracked in the bug tracker under JDK-8277863.

Change the java.security.manager System Property Default Value to disallow

The Security Manager was marked as “deprecated for removal” in Java 17. Java 18 sets the default value of the system property java.security.manager to disallow.

As a result, calling System.setSecurityManager(…) throws an UnsupportedOperationException. In Java 17, such a call was still allowed and only resulted in a warning. If you still want to set the Security Manager at runtime, you must start the application with -Djava.security.manager=allow.

There is no JEP for this change; it is tracked in the bug tracker under JDK-8270380.

Remove the Legacy PlainSocketImpl and PlainDatagramSocketImpl Implementation

In Java 13 and Java 15, the JDK developers reimplemented the Socket API and the DatagramSocket API.

The old implementations could since be reactivated via the jdk.net.usePlainSocketImpl or jdk.net.usePlainDatagramSocketImpl system properties.

In Java 18, the old code was removed; the two system properties have had no effect since then.

There is no JEP for this change; it is tracked in the bug tracker under JDK-8253119.

Obsolete Biased Locking

Biased locking has been disabled by default and “deprecated” since Java 15. In Java 18, the code was removed from the JVM.

Java 18 still accepts the VM option -XX:+UseBiasedLocking and the five related options BiasedLockingStartupDelay, BiasedLockingBulkRebiasThreshold, BiasedLockingBulkRevokeThreshold, BiasedLockingDecayTime, and UseOptoBiasInlining: The JVM prints a warning and ignores the options.

There is no JEP for this change; it is tracked in the bug tracker under JDK-8256425.

Other Changes in Java 18

In this section, you will find those changes that you will rarely encounter during your daily programming work. Nevertheless, it certainly does not hurt to skim them once.

Reimplement Core Reflection with Method Handles

If you have a lot to do with Java reflection, you will know that there is always more than one way to go. For example, to read the private value field of a String via reflection, there are two ways:

1. Per so-called “core reflection”:

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
byte[] value = (byte[]) field.get(string);

2. Via “method handles”:

VarHandle handle =
    MethodHandles.privateLookupIn(String.class, MethodHandles.lookup())
        .findVarHandle(String.class, "value", byte[].class);
byte[] value = (byte[]) handle.get(string);

There is a third form that we can’t see directly: core reflection uses additional native JVM methods for the first few calls after starting the JVM and only starts compiling and optimizing the Java reflection bytecode after a while.

Maintaining all three variants means a considerable effort for the JDK developers. Therefore, as part of JDK Enhancement Proposal 416, it was decided to reimplement the code of the reflection classes java.lang.reflect.Method, Field, and Constructor using method handles and thus reduce the development effort.

The old implementation could initially be reactivated via the VM option -Djdk.reflect.useDirectMethodHandle=false; it was removed in Java 22.

ZGC / SerialGC / ParallelGC Support String Deduplication

Since Java 18, the Z garbage collector, which was released as production-ready in Java 15, and the serial and parallel garbage collectors also support string deduplication.

String deduplication means that the garbage collector detects strings whose value and coder fields contain the same bytes. The GC deletes all but one of these byte arrays and lets all string instances reference this single byte array.

Remember, it is not the Strings that are deduplicated (as the name of the feature implies), but only their byte arrays. Nothing changes in the identities of the String objects themselves.

String deduplication is disabled by default and must be explicitly enabled via VM option -XX:+UseStringDeduplication. It costs the garbage collector additional work, which, according to JEP 192, only pays off if the application contains enough duplicate strings.

String deduplication was first released with this JEP in Java 8u20 for G1. There is no JEP for the inclusion in ZGC, serial GC, and parallel GC in Java 18; the changes are tracked in the bug tracker under JDK-8267186, JDK-8272609, and JDK-8267185.

Allow G1 Heap Regions up to 512 MB

The G1 Garbage Collector usually determines the size of the heap regions automatically. Depending on the heap size, the size of the regions is set to a value between 1 MB and 32 MB.

You can also set the region size manually via VM option -XX:G1HeapRegionSize. Up to Java 17, sizes between 1 MB and 32 MB were allowed here as well.

In Java 18, the maximum size of regions is increased to 512 MB. This is particularly intended to help reduce heap fragmentation for very large objects.

The change only applies to the manual setting of the region size. When determined automatically by the JVM (i.e., without specifying the VM option), the maximum size remains 32 MB.

There is no JEP for this change; it is tracked in the bug tracker under JDK-8275056.

New Methods in Math and Duration

The Math and StrictMath classes were extended by the following methods in Java 18:

  • ceilDiv() divides two integers and rounds the result up – the counterpart to floorDiv(), which rounds down. You can use it, for example, to calculate how many pages you need for 23 entries if ten entries fit on a page: Math.ceilDiv(23, 10) returns 3, while Math.floorDiv(23, 10) and 23 / 10 return 2.
  • ceilMod() returns the remainder that matches ceilDiv(): Math.ceilMod(23, 10) returns -7 because 3 · 10 − 7 = 23.
  • divideExact(), floorDivExact(), and ceilDivExact() throw an ArithmeticException if the result overflows. That happens for exactly one division: Integer.MIN_VALUE or Long.MIN_VALUE divided by -1. In this case, the / operator returns Integer.MIN_VALUE again without any error message.
  • unsignedMultiplyHigh() multiplies two long values as unsigned numbers and returns the upper 64 bits of the 128-bit product.

The Duration class got the method isPositive(). It returns true if the duration is greater than zero.

There is no JEP for these changes; they are tracked in the bug tracker under JDK-8271602 (ceilDiv(), ceilMod(), ceilDivExact()), JDK-8271225 (floorDivExact()), JDK-8075806 (divideExact()), JDK-8188044 (unsignedMultiplyHigh()), and JDK-8171382 (Duration.isPositive()).

Complete List of All Changes in Java 18

In addition to the JDK Enhancement Proposals and changes to the class libraries presented in this article, there are other changes (e.g., to the cryptography modules) beyond this article’s scope. You can find a complete list in the JDK 18 release notes.

Summary

Java 18 marked the beginning of the next cycle of non-LTS releases, which ended in September 2023 with the LTS release Java 21. If you have done the math, you will notice that there were five non-LTS releases and three years between Java 11 and Java 17 – but only three non-LTS releases and two years between Java 17 and 21. Shortly before the release of Java 17, Oracle had announced that it would shorten the release cycle.

Java 18 is manageable and – for the first time in a long while – does not contain any change to the language itself (after numerous language extensions like records and sealed classes).

The main changes of Java 18 are:

  • UTF-8 is the default character set on all operating systems, regardless of language and locale settings.
  • Using the jwebserver command (or the SimpleFileServer class), we can quickly start a rudimentary web server.
  • With the @snippet tag, we get a powerful tool to integrate source code snippets into our JavaDoc documentation.
  • With the “Internet-Address Resolution SPI”, we can replace the standard resolver for IP addresses, which is especially helpful in tests.
  • The preview and incubator features “Pattern Matching for switch”, “Vector API”, and “Foreign Function & Memory API” were each sent to the next preview or incubator round.
  • Finalization and Thread.stop() have been marked “deprecated for removal”, and the biased locking code has been removed from the JVM.

As always, various other changes round out the release. The Java 18 builds are still available in the OpenJDK archive. For new projects, you are better off with the current LTS version, Java 25.

If you enjoyed this article, I’d appreciate a review on my ProvenExpert profile.

👉 Leave a review

This subject in your own code?

You have read the article – in the training your team works with it. Over 2 days we go through the subjects on your own projects instead of constructed examples.

Hands-on, easy to understand, and directly applicable to your day-to-day project work. Instead of theory, I teach principles that help you write code that is better, more maintainable, and more performant in the long run.

Java 21 TrainingSee all trainings

Become a Better Java Developer

My free newsletter keeps you ahead. Modern Java: new versions & features, performance, and JVM insights – once a month.

Search