Java 14 Features – What’s new in JDK 14

Java is officially announced the release date of version 14 on March 17, 2020. We’re gonna look at Java 14 major features with examples.

  1. Install Java 14
  2. Switch Expressions (Standard)
  3. Pattern Matching for instanceof (Preview)
  4. JFR Event Streaming
  5. Helpful NullPointerExceptions
  6. Records (Preview)
  7. Text Blocks (Second Preview)

Install Java 14

You can follow these steps to setup Java 14 on your machine:

  • Run javac -version to verify that it is already installed or not
  • Go to this page and download JDK 14
  • Install JDK 14 with the file you’ve just downloaded. if you don’t know the way, click here
  • Set the Path

Switch Expressions (Standard)

Friendlier and less error-prone, Switch Expressions were a ‘preview’ feature in earlier releases. Java 14 make it standard now.

You typically write a switch like this:

switch (type) {
  case RUN, START:
    System.out.println("RUN");
    break;
  case STOP:
    System.out.println("STOP");
    break;
  default:
    System.out.println("Unknown");
}

All the break statements ensures that the next block in the switch statement is not executed.

Now we can refactor the code to make use of this new switch form:

switch (type) {
  case RUN, START -> System.out.println("RUN");
  case STOP -> System.out.println("STOP");
  default -> System.out.println("Unknown");
};

With Switch Expression:

System.out.println(
    switch (type) {
      case RUN, START -> "RUN";
      case STOP -> "STOP";
      default -> "Unknown";
    }
  );

Java 14 introduces new yield statement to yield a value which becomes the value of the enclosing Switch Expression. For example:

String state = switch (type) {
      case RUN, START -> "RUN";
      case STOP -> "STOP";
      default -> {
        System.out.println("Not recognize state!");
        yield "Unknown";
      }
    }
  );

Or you can also use traditional switch block like this:

String state = switch (type) {
      case RUN, START: 
        yield "RUN";
      case STOP:
        yield "STOP";
      default -> {
        System.out.println("Not regconize state!");
        yield "Unknown";
      }
    }
  );

Pattern Matching for instanceof (Preview)

Pattern matching make conditional extraction of components from objects more concise and safe.

Before Java 14:

if (obj instanceof Tutorial) {
  Tutorial t = (Tutorial) obj;
  
  if (t.getId() == 2020) {
    return t.getTitle();
  }
}

You can see that we have to do 3 things:

  • make a test (is obj a Tutorial?)
  • do a conversion (casting obj to Tutorial)
  • declare a new local variable t

In some cases, we also need to do more check such as t.getId() == 2020.

Java 14 Enhancement:

if (obj instanceof Tutorial t && t.getId() == 2020) {
  return t.getTitle();
} else {
  // ...
}

If obj is an instance of Tutorial, it is cast to Tutorial and assigned to t variable. The binding variable t is in scope on the right hand side of the && operator (only evaluated if instanceof succeeded and assigned to t).

Notice that the pattern will only match (and t will only be assigned) if obj is NOT null.

JFR Event Streaming

Java Flight Recorder (JFR) collects diagnostic and make data profiling about a running Java application.
JFR Event Streaming exposes the Flight Recorder data for continuous monitoring, both for in-process and out-of-process applications. It also helps to record the same set of events as in the non-streaming case.

Without JFR Event Streaming, here are what a user should do for consuming the data:

  • start a recording
  • stop it
  • dump the contents to disk
  • parse the recording file

jdk.jfr module has package jdk.jfr.consumer that provides functionality to subscribe to events asynchronously. We can read recording data directly, or stream, from the disk repository without dumping a recording file.

The jdk.jfr.consumer.EventStream interface provides way to filter and consume events regardless if the source is a live stream or a file on disk.

public interface EventStream extends AutoCloseable {
  public static EventStream openRepository();
  public static EventStream openRepository(Path directory);
  public static EventStream openFile(Path file);

  void setStartTime(Instant startTime);
  void setEndTime(Instant endTime);
  void setOrdered(boolean ordered);
  void setReuse(boolean reuse);

  void onEvent(Consumer<RecordedEvent> handler);
  void onEvent(String eventName, Consumer<RecordedEvent> handler);
  void onFlush(Runnable handler);
  void onClose(Runnable handler);
  void onError(Runnable handler);
  void remove(Object handler);

  void start();
  void startAsync();

  void awaitTermination();
  void awaitTermination(Duration duration);
  void close();
}

You can see 3 factory methods to create a stream:

  • openRepository(Path directory) constructs an EventStream from a disk repository. It helps to monitor other processes by working directly against the file system.
  • openRepository() performs in-process monitoring. This method does not start a recording. Instead, the stream receives events only when recordings are started by external (using JCMD or JMX).
  • openFile(Path file) creates an EventStream from a recording file.

We work with an EventStream by registering a handler which will be invoked in response to the arrival of an event.

The following example prints the overall CPU usage and locks contended for more than 10 ms. We’re gonna use RecordingStream class, an implementation of EventStream interface.

try (var rs = new RecordingStream()) {
  rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
  rs.enable("jdk.JavaMonitorEnter").withThreshold(Duration.ofMillis(10));
  rs.onEvent("jdk.CPULoad", event -> {
    System.out.println(event.getFloat("machineTotal"));
  });
  rs.onEvent("jdk.JavaMonitorEnter", event -> {
    System.out.println(event.getClass("monitorClass"));
  });
  rs.start();
}

Helpful NullPointerExceptions

Assume that tutorial is a null object, so for the code:

tut.id = 2020;

We’re gonna get a NullPointerException (NPE):

Exception in thread "main" java.lang.NullPointerException
    at Prog.main(Prog.java:8)

What if the following code throws a NPE:

blog.tutorial.author.name = "bezkoder";

We don’t know exactly which variable was null. Was it blog or tutorial or author?
Now look at the following cases which also throw NPE:

// a or a[i] or a[i][j]?
arr[i][j][k] = 42;

// a or b?
a.i = b.j;

// x() or y() return null?
x().y().i = 42;

Java 14 improves the usability of NullPointerException to reduce the confusion and concern that new developers often have about it.

To enable code details in exception messages, we can run with command-line option: -XX:+ShowCodeDetailsInExceptionMessages.

JVM will generate messages like this:

tut.id = 2020;
Exception in thread "main" java.lang.NullPointerException: 
        Cannot assign field "id" because "tut" is null
    at Prog.main(Prog.java:8)


blog.tutorial.author.name = "bezkoder";
Exception in thread "main" java.lang.NullPointerException: 
        Cannot read field "author" because "blog.tutorial" is null
    at Prog.main(Prog.java:8)


arr[i][j][k] = 42;
Exception in thread "main" java.lang.NullPointerException:
        Cannot load from object array because "arr[i][j]" is null
    at Prog.main(Prog.java:8)


a.i = b.j;
Exception in thread "main" java.lang.NullPointerException:
        Cannot read field "j" because "b" is null
    at Prog.main(Prog.java:8)

In the future, this feature might be enabled default.

Records (Preview)

Records help us to write domain classes with only storing data in fields purpose without any custom behaviors.

For example, we have a Tutorial class with 3 fields: id, title, description.
What we need to do is to declare:

  • constructor
  • Getter methods
  • toString()
  • hashCode() & equals()
public class Tutorial {
  private int id;
  private String title;
  private String author;

  public Tutorial(int id, String title, String author) {

    this.id = id;
    this.title = title;
    this.author = author;
  }

  public int getId() {
    return id;
  }

  public String getTitle() {
    return title;
  }

  public String getAuthor() {
    return author;
  }

  @Override
  public String toString() {
    return "Tutorial [id=" + id + ", title=" + title + ", author=" + author + "]";
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((author == null) ? 0 : author.hashCode());
    result = prime * result + id;
    result = prime * result + ((title == null) ? 0 : title.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    Tutorial other = (Tutorial) obj;
    if (author == null) {
      if (other.author != null)
        return false;
    } else if (!author.equals(other.author))
      return false;
    if (id != other.id)
      return false;
    if (title == null) {
      if (other.title != null)
        return false;
    } else if (!title.equals(other.title))
      return false;
    return true;
  }

}

Using a record, we can make the implementations of constructor, getters, hashCode(), equals() and toString() automatically.

public record Tutorial(int id,
                       String title,
                       String author) { }

The Tutorial record will be compiled like this:

final class Tutorial extends java.lang.Record {  
  private final int id;
  private final java.lang.String title;
  private final java.lang.String author;

  public Tutorial(int id, java.lang.String title, java.lang.String author) { /* compiled code */ }

  public java.lang.String toString() { /* compiled code */ }

  public final int hashCode() { /* compiled code */ }

  public final boolean equals(java.lang.Object o) { /* compiled code */ }

  public int id() { /* compiled code */ }

  public java.lang.String title() { /* compiled code */ }
  
  public java.lang.String author() { /* compiled code */ }
}

Note:: We need to compile the file using the preview flag command:

javac --enable-preview --release 14 Tutorial.java

Text Blocks (Second Preview)

Text Blocks was introduced in Java 13 as a preview feature and continue to be the second round of preview with Java 14. It helps us to work with multiline string literals.

We sometimes write code with many string concatenations and escape sequences for multiline text formatting.
For example:

String html = "<html>" +
"\n\t" + "<body>" +
"\n\t\t" + "<h1>\"Be zKoder!\"</h1>" +
"\n\t" + "</body>" +
"\n" + "</html>";

Using Java 14 Text Blocks, we can write easy to read code with 3 quotation marks at the beginning and end of a text block as following:

String html = """
<html>
  <body>
    <h1>"Java 14 is here!"</h1>
  </body>
</html>""";

We can also use a backslash \ for a long line to split up for nice-looking text block.

String text = """
                Lorem ipsum dolor sit amet, consectetur \
                adipiscing elit, sed do eiusmod tempor incididunt \
                ut labore et dolore magna aliqua.\
                """;

Conclusion

Java 14 provides features and updates to help us.

  • Switch Expressions become standard.
  • Pattern Matching instanceof reduces explicit casts.
  • JDK Flight Recorder provides Event Streaming for continuous monitoring.
  • NullPointerException for better debugging & diagnostics
  • Records declare classes for storing data purpose
  • Text blocks helps to work with multiline string values and supports new escape sequences.

Happy Learning! See you again.

Further Reading

3 thoughts to “Java 14 Features – What’s new in JDK 14”

  1. Hello there! This tutorial couldn’t be written any better! Going through this article reminds me of my previous roommate! He always kept talking about this. I most certainly will send this article to him. Fairly certain he’s going to have a very good read. Thanks for sharing!

Comments are closed to reduce spam. If you have any question, please send me an email.