Getting started with Java 18

Java 18 is finally available for download! In this article we will learn some of the most interesting Enhancement Proposals (JEPs) which are now available in the JDK.

Installing Java 18

Firstly, let’s download the Java 18 platform from https://jdk.java.net/18/

Choose the version for your Operating System. Then, unpack it on your machine and set JAVA_HOME to point to your new distribution. Also, it is recommended to include the JAVA_HOME/bin folder in your PATH environment variable.

To set Java 18 in your Maven project, add the java.version property in your pom.xml:

<properties>
	<java.version>18</java.version>
</properties>

Installing with JBang

If you are using JBang to build scripts with Java, then you can install Java 18 in an even simpler way:

$ jbang jdk install 18

[jbang] Downloading JDK 18. Be patient, this can take several minutes...
[jbang] Installing JDK 18...
[jbang] Default JDK set to 18
[jbang] There is a new version of jbang available!
[jbang] You have version 0.90.1 and 0.92.2 is the latest.
[jbang] Run 'jbang version --update' to update to the latest version.

UTF-8 by Default

How to know Java’s default Character Set ? the obvious answer is via:

Charset.defaultCharset()

The default Character set varies, depending on your Operating System. On some OS, like Windows, it depends upon the user’s locale and the default encoding, . On macOS, it is UTF-8 except in the POSIX C locale.

A quick hack to check the default charset is to run Java with the following parameters:

java -XshowSettings:properties -version 2>&1 | grep file.encoding

Java 18 allows to set as default charset UTF-8 unless configured otherwise by an implementation-specific means.

System.out.println("Default charset : " + Charset.defaultCharset());

The above code will return the following output, whatever is your OS default:

Default charset : UTF-8

By making UTF-8 the charset Java programs can be more predictable and portable.

On the other hand, you can change the default value by using the following System Property:

-Dfile.encoding=UTF-8

Simple Web Server

JDK 18 now includes an out-of-the-box Web server via command line. The goal is not to provide advanced features such as authentication, access control, or encryption. You can use the built-in Web server tool for local tests or file-sharing purposes.

You can use the JAVA_HOME/bin/jwebserver command to start the HTTP Server:

java 18 what is new

And here is how to add some extra parameters such as the bind address (-b), the target port (-p), the document folder (-d) and verbose output (-v):

$ jwebserver -b 127.0.0.1 -p 8081 -d /home/java/docs -o verbose
Serving /home/java/docs and subdirectories on 127.0.0.1 port 8081
URL http://127.0.0.1:8081/

You can also manage the Web server programmatically. Here’s as an example, how to use JShell Command Line to bind the Web Server on Port 8080:

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

Much the same way, you can start it from a jshell script:

jshell> var server = SimpleFileServer.createFileServer(new InetSocketAddress(8080),
   ...> Path.of("/some/path"), OutputLevel.VERBOSE);
jshell> server.start()

Code Snippets in Java API Documentation

With the current Java implementation, there is no standard way to include code snippets in the Java Doc. Since Java 18 you can use the inline tag @snippet to declare code fragments to appear in the generated documentation.

Code fragments are usually Java source code, but they may also be also source code in other languages, fragments of properties files, or plain text

Here is an example of how the @snippet tag works:

/**
  {@snippet :
  // @highlight region regex="\bPaths.*?\b" type="highlighted"
  try  {
    	 Files.lines(Paths.get("/tmp/file.txt"))
         .forEach(System.out::println);
    	}
    	catch (Exception exc)  {
    		exc.printStackTrace();
    	}
  // @end
  }
 */

And here is the outcome, when you generate the javadoc on the class App.java which uses the @snippet tag:

java 18 tutorial

Reimplement Core Reflection with Method Handles

Firstly, no panic! This change is not intended to make any change to the java.lang.reflect public API but it is purely an implementation change.

On the other hand, it introduces Method Handles in the implementation of java.lang.reflect.Method, Constructor, and Field.

In a nutshell, Method Handles are a low-level mechanism for finding, adapting and invoking methods by using optional transformations of arguments or return values.

Making Method Handlesthe underlying mechanism for reflection will reduce the maintenance and development cost of both the java.lang.reflect and java.lang.invoke APIs.

If your code depends upon highly implementation-specific aspects of the existing implementation there can be impacts though. Also, method-handle invocation may consume more resources than the old core reflection implementation. To mitigate this compatibility risk, as a workaround you can enable the old implementation by using:

-Djdk.reflect.useDirectMethodHandle=false.


Vector API

The Vector API aims to improve the performance of vector computations. A vector computation consists of a sequence of operations on vectors.

This API leverages the existing HotSpot auto-vectorizer but with a user model, which makes the computation more predictable and robust.

The new API includes at the top the the abstract class Vector<E> . The type variable E is instantiated as the boxed type of the scalar primitive integral or floating point element types covered by the vector.

A vector also has a shape which defines the size, in bits, of the vector. The combination of element type and shape determines a vector’s species, represented by VectorSpecies<E>.  As an example, here is a simple scalar computation over elements of arrays:   

void scalarComputation(float[] a, float[] b, float[] c) {
   for (int i = 0; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
   }
}

This is the counterpart using the Vector API:

static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;

void vectorComputation(float[] a, float[] b, float[] c) {
    int i = 0;
    int upperBound = SPECIES.loopBound(a.length);
    for (; i < upperBound; i += SPECIES.length()) {
        // FloatVector va, vb, vc;
        var va = FloatVector.fromArray(SPECIES, a, i);
        var vb = FloatVector.fromArray(SPECIES, b, i);
        var vc = va.mul(va)
                   .add(vb.mul(vb))
                   .neg();
        vc.intoArray(c, i);
    }
    for (; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
    }
}

In the above example, a VectorSpecies, whose shape is optimal for FloatVector, has been stored in a static final field so that the runtime compiler treats the value as constant and can therefore better optimize the vector computation.

The main loop then iterates over the array parameters in strides of the vector length (that is., the species length). It loads two FloatVector of the given species from arrays a and b at the corresponding index, fluently performs the arithmetic operations, and then stores the result into array c.

If any array elements are left over after the last iteration then the results for those tail elements are computed with an ordinary scalar loop.  

Internet-Address Resolution SPI

The java.net.InetAddress API resolves host names to Internet Protocol (IP) addresses, and vice versa. In the current implementation, this API uses the OS’s native resolver (typically configured with a combination of a local hosts file and the DNS).
The Java 18 API defines a service-provider interface (SPI) for host name and address resolution, so that java.net.InetAddress can make use of resolvers other than the platform’s built-in resolver.
The new InetAddress API uses a Service Loader to locate a resolver provider.

The built-in implementation will be used as before when no providers are found.
To manage this SPI, you will use one of the following classes within the java.net.spi package:

  • InetAddressResolverProvider — an abstract class defining the service to be located by java.util.ServiceLoader.
  • InetAddressResolver — an interface that defines methods for the fundamental forward and reverse lookup operations.
  • InetAddressResolver.LookupPolicy — a class whose instances describe the characteristics of a resolution request,
  • InetAddressResolverProvider.Configuration — an interface describing the platform’s built-in configuration for resolution operations.

Let’s see it with a practical example. We will create a sample AddressResolver Class which extends java.net.spi.InetAddressResolver:

package com.sample.provider;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.net.spi.InetAddressResolver;
import java.util.stream.Stream;

public class MyAddressResolver implements InetAddressResolver {
  @Override
  public Stream<InetAddress> lookupByName(String host, LookupPolicy lookupPolicy)
      throws UnknownHostException {
      if (host.equals("fedora")) {
           return Stream.of(InetAddress.getByAddress(new byte[] {192, 168, 10, 1}));
      }
    return Stream.of(InetAddress.getByAddress(new byte[] {127, 0, 0, 1}));
  }

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

The Class contains a minimal address resolution policy to return the IP Address 192.168.10.1 for the Host “fedora”.

Next, let’s extend the class InetAddressResolverProvider to return an instance of our AddressResolver Class:

package com.sample.provider;

import java.net.spi.InetAddressResolver;
import java.net.spi.InetAddressResolverProvider;

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

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

Finally, to register the Class as a Service Provider Interface, we need to add to the Service Provider Configuration Class in a file META-INF/services/java.net.spi.InetAddressResolverProvider

com.sample.provider.MyAddressResolverProvider

That’s all. You can test the InetAddress resolution as follows:

public class App {
  public static void main(String[] args) throws UnknownHostException {
    InetAddress[] addresses = InetAddress.getAllByName("fedora");
    System.out.println("addresses = " + Arrays.toString(addresses));
  }
}

Foreign Function & Memory API

The Java Platform has a significant amount of libraries to reach non-JVM platforms. For example, it is possible to reach RDBMS with JDBC Drivers. It is also possible to invoke web services (HTTP client) or serve remote clients (NIO channels), or communicate with local processes using Sockets.

However, Java still has significant pitfalls in accessing code and data on the same machine which runs outside the Java runtime. It is true that Java Native Interface (JNI) supports the invocation of native code , yet it is inadequate for many reasons:

  • Firstly, JNI involves several tedious artifacts: a Java API (to wrap native methods), a C header file derived from the Java API, and a C implementation that calls the native library of interest.
  • Then, JNI can only interoperate with libraries written in languages that adopt some calling conventions – typically C and C++..
  • Finally, JNI does not reconcile the Java type system with the C type system (e.g. Java represents data as objects while C represents data with structs)

The Foreign Function & Memory API (FFM API) defines classes and interfaces so that client code in libraries and applications can

  1. Allocate foreign memory (MemorySegment, MemoryAddress, and SegmentAllocator)
  2. Manipulate and access structured foreign memory (MemoryLayout, VarHandle),
  3. Manage the lifecycle of foreign resources (ResourceScope)
  4. Call foreign functions (SymbolLookup, CLinker, and NativeSymbol).

This API includes the FFM API in the jdk.incubator.foreign package of the jdk.incubator.foreign module.

As an example, here is Java code snippet that obtains a method handle for a C library function radixsort and then uses it to sort four strings which start life in a Java array (a few details are elided):

// 1. Find foreign function on the C library path
CLinker linker = CLinker.getInstance();
MethodHandle radixSort = linker.downcallHandle(
                             linker.lookup("radixsort"), ...);
// 2. Allocate on-heap memory to store four strings
String[] javaStrings   = { "mouse", "cat", "dog", "car" };
// 3. Allocate off-heap memory to store four pointers
MemorySegment offHeap  = MemorySegment.allocateNative(
                             MemoryLayout.ofSequence(javaStrings.length,
                                                     ValueLayout.ADDRESS), ...);
// 4. Copy the strings from on-heap to off-heap
for (int i = 0; i < javaStrings.length; i++) {
    // Allocate a string off-heap, then store a pointer to it
    MemorySegment cString = implicitAllocator().allocateUtf8String(javaStrings[i]);
    offHeap.setAtIndex(ValueLayout.ADDRESS, i, cString);
}
// 5. Sort the off-heap data by calling the foreign function
radixSort.invoke(offHeap, javaStrings.length, MemoryAddress.NULL, '\0');
// 6. Copy the (reordered) strings from off-heap to on-heap
for (int i = 0; i < javaStrings.length; i++) {
    MemoryAddress cStringPtr = offHeap.getAtIndex(ValueLayout.ADDRESS, i);
    javaStrings[i] = cStringPtr.getUtf8String(0);
}
assert Arrays.equals(javaStrings, new String[] {"car", "cat", "dog", "mouse"});  // true

Pattern Matching for switch

This is a core language update. The change aims to enhance switch statements and expressions.

As an example, here is a Switch expression:

    int numLetters = 0;
    Day day = Day.WEDNESDAY;
    switch (day) {
        case MONDAY, FRIDAY, SUNDAY -> numLetters = 6;
        case TUESDAY                -> numLetters = 7;
        case THURSDAY, SATURDAY     -> numLetters = 8;
        case WEDNESDAY              -> numLetters = 9;
        default -> throw new IllegalStateException("Invalid day: " + day);
    };
    System.out.println(numLetters);

Extending pattern matching to switch allows to test an expression against a number of patterns, each with a specific action. This way, you will be able to express complex data-oriented queries in a concise and safe way.

As an example,in the following code, the value of “o” matches the pattern Long l. Therefore, the code associated with case Long l will be executed:

Object o = 123L;
String formatted = switch (o) {
    case Integer i -> String.format("int %d", i);
    case Long l    -> String.format("long %d", l);
    case Double d  -> String.format("double %f", d);
    case String s  -> String.format("String %s", s);
    default        -> o.toString();
};
Found the article helpful? if so please follow us on Socials