Friday, May 27, 2022

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

Five of Java 18’s JEPs add new features or major enhancements. See what they do and how they work.

Java 18 was officially released in March, and it included nine implemented JDK Enhancement Proposals (JEPs).

This article covers five of the JEPs in Java 18. The companion article, “The hidden gems in Java 18: The subtle changes,” goes over Java 18’s small improvements and changes that aren’t reflected in those JEPs. It also talks about the four JEPs that are incubators, previews, and deprecations.

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 nine JEPs in the Java 18 release as are follows:

◉ JEP 400: UTF-8 by default

◉ JEP 408: Simple Web Server

◉ JEP 413: Code snippets in Java API documentation

◉ JEP 416: Reimplement core reflection with method handles

◉ JEP 417: Vector API (third incubator)

◉ JEP 418: Internet address resolution service provider interface

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

◉ JEP 420: Pattern matching for switch (second preview)

◉ JEP 421: Deprecate finalization for removal

Two of the JEPs are incubator features, one is a preview, and is one is a deprecation, that is, a JEP that prepares developers for the removal of a feature. In this article, I’ll walk through the other five JEPs, and I will not explore the incubators, the preview, and the deprecation.

JEP 400: UTF-8 by default

UTF-8 is a variable-width character encoding for electronic communication and is considered the web’s standard character set (charset). It can encode all the characters of any human language.

The Java standard charset determines how a string is converted to bytes and vice versa. It is used by many methods of the JDK library classes, mainly when you’re reading and writing text files using, for example, the FileReader, FileWriter, InputStreamReader, OutputStreamWriter, Formatter, and Scanner constructors, as well as the URLEncoder encode() and decode() static methods.

Prior to Java 18, the standard Java charset could vary based on the runtime environment’s language settings and operating systems. Such differences could lead to unpredictable behavior when an application was developed and tested in one environment and then run in another environment where the Java default charset changed.

Consider a pre-Java 18 example that writes and reads Arabic text. Run the following code on Linux or macOS to write Arabic content (which means “Welcome all with Java JEP 400”) to the Jep400-example.txt file:

private static void writeToFile() {

   try (FileWriter writer = new FileWriter("Jep400-example.txt");

   BufferedWriter bufferedWriter = new BufferedWriter(writer)) {

      bufferedWriter.write("مرحبا بكم في جافا جيب ٤٠٠");

      }

   catch (IOException e) {

   System.err.println(e);

   }

}

Then read the file back using a Windows system and the following code:

private static void readFromFile() {

   try (FileReader reader = new FileReader("Jep400-example.txt");

   BufferedReader bufferedReader = new BufferedReader(reader)) {

      String line = bufferedReader.readLine();

      System.out.println(line);

      }

   catch (IOException e) {

   System.err.println(e);

   }

}

The output will be the following:

٠رحبا ب٠٠٠٠جا٠ا ج٠ب Ù¤Ù Ù

This rubbish output happens because, under Linux and macOS, Java saves the file with the default UTF-8 encoding format, but Windows reads the file using the Windows-1252 encoding format.

This problem becomes even worse when you mix text in the same program by writing the file using an older facility, FileWriter, and then reading the contents using newer I/O class methods such as Files writeString(), readString(), newBufferedWriter(), and newBufferedReader(), as in the following:

private static void writeReadFromFile() {

   try (FileWriter writer = new FileWriter("Jep400-example.txt");

   BufferedWriter bufferedWriter = new BufferedWriter(writer)) {

      bufferedWriter.write("مرحبا بكم في جافا جيب ٤٠٠");

      bufferedWriter.flush();

      var message = Files.readString(Path.of("Jep400-example.txt"));

      System.out.println(message);

      }

   catch (IOException e) {

   System.err.println(e);

   }

}

Running the program under Linux and macOS will print the Arabic content without any problem, but doing so on Windows will print the following:

????? ??? ?? ???? ??? ???

These question marks are printed because newer APIs don’t respect the default OS character set and always used UTF-8; so in this case, the file is written with FileWriter using the Windows-1252 charset. When Files.readString(Path.of("Jep400-example.txt")) reads back the file, the method use UTF-8 regardless of the standard charset.

The best solution is to specify the charset when you’re reading or writing files and when calling all methods that convert strings to bytes (and vice versa).

You might think another solution is to set the default charset with the file.encoding system property, as follows:

var fileWriter = new FileWriter("Jep400-example.txt", StandardCharsets.UTF_8);

var fileReader = new FileReader("Jep400-example.txt", StandardCharsets.UTF_8);

Files.readString(Path.of("Jep400-example.txt"), StandardCharsets.UTF_8);

What happens?

[mtaman]:~ java -Dfile.encoding=US-ASCII FileReaderWriterApplication.java

?????????????????????????????????

The result is rubbish because the FileWriter respects the default encoding system property, while Files.readString() ignores it and uses UTF-8. Therefore, the correct encoding should be used to run correctly with -Dfile.encoding=UTF-8.

The goal of JEP 400 is to standardize the Java API default charset to UTF-8, so that APIs that depend on the default charset will behave consistently across all implementations, operating systems, locales, and configurations. Therefore, if you run the previous snippets on Java 18, the code will produce the correct contents.

JEP 408: Simple Web Server

Many development environments let programmers start up a rudimentary HTTP web server to test some functionality for static files. That capability comes to Java 18 through JEP 408.

The simplest way to start the web server is with the jwebserver command. By default, this command listens to localhost on port 8000. The server also provides a file browser to the current directory.

[mtaman]:~ jwebserver 

Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".

Serving /Users/mohamed_taman/Hidden Gems in Java 18/code and subdirectories on 127.0.0.1 port 8000

URL http://127.0.0.1:8000/

If you visit http://127.0.0.1:8000/, you will see a directory listing in your web browser, and in the terminal you will see the following two lines:

127.0.0.1 - - [06/Mar/2022:23:27:13 +0100] "GET / HTTP/1.1" 200 -

127.0.0.1 - - [06/Mar/2022:23:27:13 +0100] "GET /favicon.ico HTTP/1.1" 404 –

You can change the jwebserver defaults with several parameters, as follows:

[mtaman]:~ jwebserver -b 127.1.2.200 -p 9999 -d /tmp -o verbose 

Serving /tmp and subdirectories on 127.0.0.200 port 9999

URL http://127.0.0.200:9999/

Here are a few of the parameters.

◉ -b specifies the IP address on which the server should listen.

◉ -p changes the port.

◉ -d changes the directory the server should serve.

◉ -o configures the log output.

For a complete list of configuration options, run jwebserver -h.

The web server is simple, as its name implies, and has the following limitations:

◉ Only the HTTP GET and HEAD methods are allowed.

◉ The only supported protocol is HTTP/1.1.

◉ HTTPS is not provided.

The API. Fortunately, you can extend and run the server programmatically from the Java API. That’s because jwebserver itself is not a standalone tool; it is a wrapper that calls java -m jdk.httpserver. This command calls the main() method of the sun.net.httpserver.simpleserver.Main class of the jdk.httpserver module, which, in turn, calls SimpleFileServerImpl.start(). This starter evaluates the command-line parameters and creates the server via SimpleFileServer.createFileServer().

You can start a server via Java code as follows:

// Creating the server

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

// Starting the server

server.start();

With the Java API, you can extend the web server. For example, you can make specific file system directories accessible via different HTTP paths, and you can implement your handlers to use your choice of paths and HTTP methods, such as PUT and POST. Check the JEP 408 documentation for more API details.

JEP 413: Code snippets in Java API documentation

Prior to Java 18, if you wanted to integrate multiline code snippets into Java documentation (that is, into Javadoc), you had to do it tediously via <pre> ... </pre> and sometimes with {@code ...}. With these tags, you must pay attention to the following two points:

◉ You can’t put line breaks between <pre> and </pre>, which means you may not get the formatting you want.

◉ The code starts directly after the asterisks, so if there are spaces between the asterisks and the code, they also appear in the Javadoc.

Here’s an example that uses <pre> and </pre>.

/**

  * How to read a text file with Java 8:

  *

  * <pre><b>try</b> (var reader = Files.<i>newBufferedReader</i>(path)) {

  *    String line = reader.readLine();

  *    System.out.println(line);

  * }</pre>

  */

This code will appear in the Javadoc as shown in Figure 1.

JDK Enhancement Proposals, Oracle Java Exam Prep, Oracle Java Certification, Java Career, Java Jobs, Java Skills, Java News, Java Process, Oracle Java Preparation Exam

Figure 1. Example using <pre> and </pre>

Here’s an example that uses <pre> and {@code ...}.

/**
     * How to read a text file with Java 8:
     *
     * <pre>{@code try (var reader = Files.newBufferedReader(path)) {
     *    String line = reader.readLine();
     *    System.out.println(line);
     * }}</pre>
     */

Figure 2 shows the output Javadoc.

JDK Enhancement Proposals, Oracle Java Exam Prep, Oracle Java Certification, Java Career, Java Jobs, Java Skills, Java News, Java Process, Oracle Java Preparation Exam

Figure 2. Example using <pre> and {@code}

The difference between the two examples is that in the first example with <pre>, you can format the code with HTML tags such as <b> and <i>, but in the second example with {@code}, such tags would not be evaluated but instead the code would be displayed as is.

JEP 413 introduces the @snippet tag for Javadoc’s standard doclet to simplify the inclusion of example source code in the API documentation. Among the goals of JEP 413 are the following:

◉ Facilitating the validation of source code fragments by providing API access to those fragments; however, correctness is the author’s responsibility

◉ Enabling modern styling, such as syntax highlighting and the automatic linkage of names to declarations

◉ Promoting better IDE support for creating and editing snippets

Using the @snippet tag, you can rewrite the example from Figure 1, which used the <pre> tag, as follows:

/**
     * How to read a text file with Java 8:
     * {@snippet :
     *  try (var reader = Files.newBufferedReader(path)) {
     *      String line = reader.readLine();
     *      System.out.println(line);
     *  }
     * }
     */

Figure 3 shows how this rewritten code would appear in the Javadoc.

JDK Enhancement Proposals, Oracle Java Exam Prep, Oracle Java Certification, Java Career, Java Jobs, Java Skills, Java News, Java Process, Oracle Java Preparation Exam

Figure 3. Example using @snippet

You can do more with @snippet. For instance, you can highlight parts of the code using @highlight. The following code highlights the readLine() method within the second line of code:

/**
     * How to read a text file with Java 8:
     * {@snippet :
     *  try (var reader = Files.newBufferedReader(path)) {
     *      String line = reader.readLine(); // @highlight substring="readLine()"
     *      System.out.println(line);
     *  }
     * }
     */

The output Javadoc appears as shown in Figure 4.

JDK Enhancement Proposals, Oracle Java Exam Prep, Oracle Java Certification, Java Career, Java Jobs, Java Skills, Java News, Java Process, Oracle Java Preparation Exam

Figure 4. Example using @highlight

Within the block marked with @highlight and @end below, you can highlight all the words beginning with line by using type="…", which can specify one of the following: bold, italic, or highlighted (with a colored background):

/**
     * How to read a text file with Java 8:
     * {@snippet :
     * // @highlight region regex="\bline\b" type="highlighted"
     * try (var reader = Files.newBufferedReader(path)) {
     *     String line = reader.readLine();
     *     System.out.println(line);
     *  }
     * // @end
     * }
     */

Then the code would appear in the Javadoc as shown in Figure 5.

JDK Enhancement Proposals, Oracle Java Exam Prep, Oracle Java Certification, Java Career, Java Jobs, Java Skills, Java News, Java Process, Oracle Java Preparation Exam

Figure 5. Example using type=

Using @link, you can link a part of the text, such as Files.newBufferedReader, to its Javadoc. Note that the colon at the end of the line with the @link tag is essential in this case, and it means that the comment refers to the next line.

/**
     * How to read a text file with Java 8:
     * {@snippet :
     * // @link substring="Files.newBufferedReader" target="Files#newBufferedReader" :
     * try (var reader = Files.newBufferedReader(path)) {
     *      String line = reader.readLine();
     *      System.out.println(line);
     * }
     * }
     */

Figure 6 shows how the code would appear in the Javadoc.

JDK Enhancement Proposals, Oracle Java Exam Prep, Oracle Java Certification, Java Career, Java Jobs, Java Skills, Java News, Java Process, Oracle Java Preparation Exam

Figure 6. Example using @link

You could also write the comment at the end of the following line, just like in the first @highlight example, or you could use @link and @end to specify a part within which all occurrences of Files.newBufferedReader should be linked.

JEP 416: Reimplement core reflection with method handles


Sometimes I want to do Java reflection, such as reading a private id field of a Person object’s class via reflection.

package org.java.mag.j18.reflection;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class Person {
    private Long id;
    private String name;
}

Surprisingly, there are two ways to do that. First, I can use core reflection, as follows:

private static Long getLongId(Object obj) throws Exception {
          Field id = obj.getClass().getDeclaredField("id");
          id.setAccessible(true);
          return (Long) id.get(obj);
      }

Alternatively, I can use method handles, as shown below.

private static Long getLongId2(Object obj) throws Exception {
          VarHandle handle =
                 MethodHandles.privateLookupIn(Person.class, MethodHandles.lookup())
                        .findVarHandle(Person.class, "id", Long.class);
          return (Long) handle.get(obj);
      }

If you call both options from main, you’ll see that they print the same value, verifying that they both work.

public static void main(String[] args) throws Exception {
   Person person = Person.builder()
                  .id(2L)
                  .name("Mohamed Taman")
                  .build();

   System.out.println("Person Id (Core reflection): " + getLongId(person));
   System.out.println("Person Id (method handles): " + getLongId2(person));
      }

Person Id (Core reflection): 2
Person Id (method handles):  2

A third hidden option happens under the hood: leveraging additional native JVM methods used by the core reflection for the first few calls after starting the JVM. After a while, the JVM begins compiling and optimizing the Java reflection bytecode.

Maintaining all three of these options would necessitate a significant amount of effort from the JDK team. As a result, the core reflection reimplementation with method handles, as in JEP 416, now reimplements lang.reflect.Method, Constructor, and Field on top of java.lang.invoke method handles. The use of method handles as the underlying mechanism for reflection reduces the maintenance and development costs of the java.lang.reflect and java.lang.invoke APIs; thus, it is good for the future of the platform.

JEP 418: Internet address resolution service provider interface


JEP 418 enhances the currently limited implementation of java.net.InetAddress by developing a service provider interface (SPI) for hostname and address resolution. The SPI allows java.net.InetAddress to use resolvers other than the operating system’s native resolver, which is usually set up to use a combination of a local hosts file and the domain name system (DNS).

Here are the benefits of providing Java with name and address resolution SPI resolvers.

◉ It enables the seamless integration of new emerging network protocols such as DNS over Quick UDP Internet Connections (QUIC), Transport Layer Security (TLS), or HTTPS.

◉ Customization gives frameworks and applications more control over resolution results and the ability to retrofit existing libraries with a custom resolver.

◉ It would allow Project Loom to work more efficiently. The current InetAddress API resolution operation blocks an OS call, which is a problem for Loom’s user-mode virtual threads. Platform threads cannot service other virtual threads while waiting for a resolution operation to be completed. Therefore, an alternative resolver could directly implement the DNS client protocol without blocking.

◉ It would allow developers more control of hostname and address resolution results when prototyping and testing, which is often required.

Here’s a quick example that doesn’t use the new JEP 418 SPI to obtain all the IP addresses of a hostname (blogs.oracle.com). You can use one of the InetAddress class methods, such as getByName(String host) or getAllByName(String host). For reverse lookups (to get the IP address of the hostname), use getCanonicalHostName() or getHostName(), as follows:

public static void main(String[] args) throws UnknownHostException {
    InetAddress[] addresses = InetAddress.getAllByName("blogs.oracle.com");
    System.out.println("address(es) = " + Arrays.toString(addresses));
}

By default, InetAddress uses the operating system’s resolver, and on my computer this code prints the following:

Address(es) = [ blogs.oracle.com/2.17.7.152, 
                       blogs.oracle.com/2a02:26f0:b5:18a:0:0:0:a15, 
                       blogs.oracle.com/2a02:26f0:b5:18d:0:0:0:a15]

Next, try that sample task using an SPI in Java 18 to change the resolution address for internal use. First, create a resolver by implementing the interface java.net.spi.InetAddressResolver.

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

    @Override
    public String lookupByAddress(byte[] addr) {
        return null;
    }
}

This code implements preliminary functionality, but it doesn’t support reverse lookups. Therefore implement a resolver provider by implementing java.net.spi.InetAddressResolverProvider, which returns an instance of a previously implemented resolver by its get() method, as follows:

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

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

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

Finally, register this resolver SPI to be used by the JDK by creating a file with the name java.net.spi.InetAddressResolverProvider under the META-INF/services folder, adding to the file the following entry: org.java.mag.j18.intaddr.MyInetAddressResolverProvider.

Run the same code in the main method as before, and you should see the following output:

addresses = [/127.0.0.4]

Source: oracle.com

Related Posts

0 comments:

Post a Comment