Content of this post is based on JDK 14 early-access build of 2019/12/18.

Pattern Matching for instanceof

According to the JEP,

A pattern is a combination of (1) a predicate that can be applied to a target, and (2) a set of binding variables that are extracted from the target only if the predicate successfully applies to it.

In Java code, instanceof is used to check the type of a target object. If the type check passes, the target object is casted to the checked type and used. The method oldWay() below is the old way to use instanceof. We can see that the type String appears three times.

void oldWay(Object obj) {
  if (obj instanceof String) {
    String str = (String) obj;
    System.out.println(str.length());
  }
}

With the pattern matching of instanceof, we can add a variable after the type. If the type check passes, the variable of the type can be used directly. The method patternMatching() below shows the new way. We can see that the type String only appears once.

void patternMatching(Object obj) {
  if (obj instanceof String str) {
    System.out.println(str.length());
  }
}

Pattern matching of instanceof can also simplify the logic of implementing equals(), see the code below.

public class NewEquals {
  private final String id;

  public NewEquals(String id) {
    this.id = id;
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(this.id);
  }

  @Override
  public boolean equals(Object obj) {
    return (obj instanceof NewEquals ne) && Objects.equals(this.id, ne.id);
  }
}

Since this feature is in preview mode, you need use --enable-preview --source 14 option to enable it. Starting from JDK 11, we can use java command to run single Java source file.

$ java --enable-preview --source 14 io/vividcode/javafeatures/NewEquals.java

JFR Event Streaming

This JEP provides a streaming way to consume events generated by Java Flight Recorder.

In the code below, we create a new RecordingStream object and use onEvent() to register event handlers. The method FlightRecorder.getFlightRecorder().getEventTypes() returns all available events. start() method is used to start the streaming.

import jdk.jfr.FlightRecorder;
import jdk.jfr.consumer.RecordingStream;

public class JFRStream {

  public static void main(String[] args) {
    try (var rs = new RecordingStream()) {
      FlightRecorder.getFlightRecorder().getEventTypes()
          .forEach(eventType -> rs.onEvent(eventType.getName(), System.out::println));
      rs.start();
    }
  }
}

The following command is used to run the code above. -XX:StartFlightRecording option is used to start JFR.

$ java -XX:StartFlightRecording io/vividcode/javafeatures/JFRStream.java

Helpful NullPointerExceptions

This JEP adds details information to NullPointerException. Before JDK 14, when a NullPointerException happened, the exception message doesn't provide any meaningful information. You have to check the source code based on the line number. If the reference chain is long, e.g. a.b.c.d, then it's hard to figure out which variable is null.

In the code below, calling b.a.doSomething() results in NullPointerException.

public class NPE {
  private static class A {
    void doSomething() {}
  }

  private static class B {
    A a;
  }

  public static void main(String[] args) {
    B b = new B();
    b.a.doSomething();
  }
}

Below is the exception message from which we only know the line number 14.

Exception in thread "main" java.lang.NullPointerException
        at io.vividcode.javafeatures.NPE.main(NPE.java:14)

We can use option -XX:+ShowCodeDetailsInExceptionMessages to enable showing details of NullPointerException.

$ java -XX:+ShowCodeDetailsInExceptionMessages io/vividcode/javafeatures/NPE.java

Below is the improved exception message. It shows a is null.

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "io.vividcode.javafeatures.NPE$A.doSomething()" because "<local1>.a" is null
        at io.vividcode.javafeatures.NPE.main(NPE.java:14)

Records

Records are similar with data classes in Kotlin and case classes in Scala. Record is a restricted form of class, just like enums. A record has a name and a state description. The state description declares the components of the record. Pair shown below is a record type with two components first and second. Both of these two components have type Object.

Records are created just like normal classes, i.e. using constructors. The components of a record can be accessed using methods which have the same name as the components. For example, first() method is used to access component first in Person.

public class Records {
  record Pair(Object first, Object second) {}

  public static void main(String[] args) {
    Pair pair = new Pair("Hello", "World");
    System.out.println(pair.first());
    System.out.println(pair.second());
  }
}

According to the JEP, a record has following automatic members.

  • A private final field for each component of the state description;
  • A public read accessor method for each component of the state description, with the same name and type as the component;
  • A public constructor, whose signature is the same as the state description, which initializes each field from the corresponding argument;
  • Implementations of equals and hashCode that say two records are equal if they are of the same type and contain the same state; and
  • An implementation of toString that includes the string representation of all the record components, with their names.

If we use javap to check the byte code of Pair, we can see the generated methods.

Compiled from "Pair.java"
public final class io.vividcode.javafeatures.Pair extends java.lang.Record {
  public io.vividcode.javafeatures.Pair(java.lang.Object, java.lang.Object);
  public java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public java.lang.Object first();
  public java.lang.Object second();
}

Switch Expressions

Switch expression is no longer a preview feature, but a standard feature in JDK 14.

We can use -> to specify the code to run in switch. The arrow label doesn't support fall-through. Only the code in matching label is executed.

void printDays(int days) {
  switch (days) {
    case 0 -> System.out.println("None");
    case 1 -> System.out.println("1 day");
    default -> System.out.println(days + " days");
  }
}

switch can be used an expression. The variable output has the value of switch expression in the code below.

void printDays2(int days) {
  String output = switch (days) {
    case 0 -> "None";
    case 1 -> "1 day";
    default -> days + " days";
  };
  System.out.println(output);
}

When arrow label is used, the right side can be an expression, throw, or a block. When it's a block, yield statement is used to return the value.

void printDays3(int days) {
  String output = switch (days) {
    case 0 -> "None";
    case 1 -> "1 day";
    default -> {
      if (days > 10) {
        yield "too many days";
      } else {
        yield days + " days";
      }
    }
  };
  System.out.println(output);
}

Regarding yield and break in switch, the rule of thumb is to use yield with switch expressions and break with switch statements.

Text Blocks

Text blocks is still a preview feature in JDK 14.

Text blocks are multi-line strings delimited using """. Incidental white space will be removed. In the code below, value of xml will have two incidental white spaces removed for each line. Trailing white spaces will also be removed.

String xml = """
  <root>
    <a>Hello</a>
    <b>
      <c>
        <d>World</d>
      </c>
    </b>
  </root>
  """;

Please note, position of the closing """ does matter when removing incidental white spaces. The number of incidental white spaces to remove is the minimum number of incidental white spaces in all lines, including the last line. In the following code, no incidental white spaces will be removed, because the last line has no incidental white spaces.

String xml2 = """
  <root>
    <a>Hello</a>
    <b>
      <c>
        <d>World</d>
      </c>
    </b>
  </root>
""";

Besides normal escape sequences supported in string literals, text blocks support two extra escape sequences.

  • \ explicitly suppresses the insertion of a newline character.
  • \s translates to a single space.

By using \, longString in the code below only has one line.

String longString = """
    hello \
    world \
    goodbye
    """;

By using \s, each line in the code below has 10 characters. With \s, we can keep trailing white spaces.

String names = """
    alex     \s
    bob      \s
    long name\s
    """;

A new method formatted() is added to String to make string formatting easier.

String formatted = """
    <person>
      <name>%s<name>
      <age>%d</age>
    </person>
    """.formatted("alex", 18);

There are two more methods added to String to support text blocks.

  • stripIndent(): Remove incidental white spaces.
  • translateEscapes(): Translate escape sequences.

Foreign Memory Access API

Foreign memory access API is still in incubation stage, so it's highly likely to change in the future.

This JEP introduces a supported, safe, and efficient foreign-memory access API. Below are key interfaces in the API.

  • MemorySegment models a contiguous region of memory.
  • MemoryAddress encodes an offset within a given MemorySegment.
  • MemoryLayout can be used to describe the contents of a memory segment in a language neutral fashion.

There are different types of MemoryLayouts.

  • ValueLayout is used to model the memory layout associated with values of basic data types, such as integral types (either signed or unsigned) and floating-point types. Each value layout has a size and a byte order.
  • A padding layout created with MemoryLayout.ofPaddingBits​(long size).
  • SequenceLayout is used to denote a repetition of a given layout.
  • GroupLayout is used to combine together multiple member layouts. There are two ways in which member layouts can be combined: if member layouts are laid out one after the other, the resulting group layout is said to be a struct which can be created using MemoryLayout.ofStruct(); conversely, if all member layouts are laid out at the same starting offset, the resulting group layout is said to be a union which can be created using MemoryLayout.ofUnion().

Layout paths select layout by a sequence of one or more MemoryLayout.PathElement elements. There are two kinds of path elements: group path elements and sequence path elements. Group path elements are used to select a given named member layout within a GroupLayout. Sequence path elements are used to select a sequence element layout within a SequenceLayout.

To operate on a memory address, you can use MemoryLayout.varHandle​() to create a memory access VarHandle object to dereference memory at the layout selected by a given layout path.

Below is an example of using this API to manage 100 points with x and y. pointLayout is the memory layout of a struct with two 32-bit value layouts to encode x and y coordinates. pointsLayout is a sequence layout with 100 elements of pointLayout. xHandle and yHandle are used to access the x and y value layout of a point, respectively.

In the for loop, xHandle.set() takes three parameters: the first parameter is the base address, the second parameter is the index of the element of access in the sequence layout, the last parameter is the value to set. After the loop, MemorySegment is wrapped in a ByteBuffer and we check the values.

import java.lang.invoke.VarHandle;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemoryLayout;
import jdk.incubator.foreign.MemoryLayout.PathElement;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.SequenceLayout;

public class ForeignMemoryAccess {

  public static void main(String[] args) {
    MemoryLayout pointLayout = MemoryLayout.ofStruct(
        MemoryLayout.ofValueBits(32, ByteOrder.BIG_ENDIAN).withName("x"),
        MemoryLayout.ofValueBits(32, ByteOrder.BIG_ENDIAN).withName("y")
    );
    long numberOfPoints = 100;
    SequenceLayout pointsLayout = MemoryLayout.ofSequence(numberOfPoints, pointLayout);
    VarHandle xHandle = pointsLayout.varHandle(int.class,
        PathElement.sequenceElement(), PathElement.groupElement("x"));
    VarHandle yHandle = pointsLayout.varHandle(int.class,
        PathElement.sequenceElement(), PathElement.groupElement("y"));
    try (MemorySegment memorySegment = MemorySegment.allocateNative(pointsLayout)) {
      MemoryAddress base = memorySegment.baseAddress();
      for (int i = 0; i < numberOfPoints; i++) {
        xHandle.set(base, (long) i, i);
        yHandle.set(base, (long) i, i + 1);
      }
      ByteBuffer byteBuffer = memorySegment.asByteBuffer().order(ByteOrder.BIG_ENDIAN);
      while (byteBuffer.hasRemaining()) {
        int x = byteBuffer.getInt();
        int y = byteBuffer.getInt();
        System.out.println("[%3d, %3d]".formatted(x, y));
      }
    }
  }
}

Use the command below to run the code. The module jdk.incubator.foreign needs to be added using --add-modules.

$ java --add-modules jdk.incubator.foreign \
  --enable-preview --source 14 \
  io/vividcode/javafeatures/ForeignMemoryAccess.java

Source Code

Source code can be found on GitHub.