Java 14 Features With Examples

Java 14 Features (with Examples)

by Sven WoltmannNovember 15, 2021

After the previous release was relatively small, Java 14 was released with an impressive 16 implemented JDK Enhancement Proposals (JEPs) on March 17, 2020.

Java 14 introduces a significant change to the language: Switch Expressions.

Another valuable feature, "Helpful NullPointerExceptions", will save us a lot of troubleshooting work in the future.

Two exciting previews, "Records" and "Pattern Matching for instanceof", are also included.

As always, there are also some performance improvements; and quite a few features have been marked as "deprecated" or removed.

Switch Expressions (Standard)

Switch Expressions is the second language enhancement from Project Amber to reach production status (the first was "var" in Java 10). Switch Expressions are actually two enhancements that can be used independently but also combined:

  1. Arrow notation without break and fall-throughs
  2. Using switch as an expression with a return value

Let's look at the changes one by one (I'm using the examples from the JEP, slightly modified).

Starting Point

In the following example, we print the word length for a given day of the week. For the last two cases, I wrote the code a bit more complicated than necessary to demonstrate what is possible with switch expressions.

switch (day) { case MONDAY: case FRIDAY: case SUNDAY: System.out.println(6); break; case TUESDAY: System.out.println(7); break; case THURSDAY: case SATURDAY: System.out.println((int) Math.pow(2, 3)); break; case WEDNESDAY: int three = 1 + 2; System.out.println(three * three); break; }
Code language: Java (java)

This notation is confusing and error-prone due to the so-called "fall-throughs", i.e., the continuation of execution on the following case if the previous one was not terminated with a break statement.

Accordingly, such constructs are marked as code smells by static code analysis (SCA) tools such as Sonar, Checkstyle, and PMD:

Static code analysis (SCA) warnings for 'switch' statements without 'break'
Static code analysis (SCA) warnings for 'switch' statements without 'break'

Arrow Notation

Starting with Java 14, you can use an arrow instead of the colon. Before the arrow, you may list several cases separated by commas. The arrow may be followed by either a single code statement or a code block in curly braces. The break statements are omitted in this notation.

switch (day) { case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); case TUESDAY -> System.out.println(7); case THURSDAY, SATURDAY -> System.out.println((int) Math.pow(2, 3)); case WEDNESDAY -> { int three = 1 + 2; System.out.println(three * three); } }
Code language: Java (java)

Modern IDEs like IntelliJ recognize the improvement potential and offer an automatic conversion to the new format:

Automatic replacement of 'switch' statements
Automatic replacement of 'switch' statements

Switch as an Expression

We often use switch to assign a case-specific value to a variable. In the following example, instead of printing the weekday's length, we store it in the numLetters variable:

int numLetters; switch (day) { case MONDAY: case FRIDAY: case SUNDAY: numLetters = 6; break; case TUESDAY: numLetters = 7; break; case THURSDAY: case SATURDAY: numLetters = (int) Math.pow(2, 3); break; case WEDNESDAY: int three = 1 + 2; numLetters = three * three; break; default: throw new IllegalStateException("Unknown day: " + day); }
Code language: Java (java)

To use the variable afterward, we must – even though we have covered every weekday – either initialize the variable beforehand or specify a default case. Otherwise the compiler would abort with the error message "Variable 'numLetters' might not have been initialized".

As of Java 14, we can convert this statement into an expression. To do so, we return each value using the new keyword yield. We then assign the result of the switch expression directly to the variable:

int numLetters = switch (day) { case MONDAY: case FRIDAY: case SUNDAY: yield 6; case TUESDAY: yield 7; case THURSDAY: case SATURDAY: yield (int) Math.pow(2, 3); case WEDNESDAY: int three = 1 + 2; yield three * three; default: throw new IllegalStateException("Unknown day: " + day); };
Code language: Java (java)

Should you have used yield as a variable name in your source code – don't worry; you can still do that. Even something like that would be allowed:

int yield = 5; yield yield + yield;

Arrow Notation and Switch Expression Combined

The switch expression just shown becomes much more elegant if we write it in arrow notation. We can write the respective values directly behind the arrow. Or – in the example with WEDNESDAY – return them from a code block using yield:

int numLetters = switch (day) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> (int) Math.pow(2, 3); case WEDNESDAY -> { int three = 1 + 2; yield three * three; } default -> throw new IllegalStateException("Unknown day: " + day); };
Code language: Java (java)

We can also let our IDE do the complete refactoring from the conventional switch statement to the switch expression with arrow notation:

Automatic replacement of 'switch' statements by 'switch' expressions
Automatic replacement of 'switch' statements by 'switch' expressions

As the variable day is an enum, the compiler can recognize that we have covered all cases. Thus, we may omit the default case:

int numLetters = switch (day) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> (int) Math.pow(2, 3); case WEDNESDAY -> { int three = 1 + 2; yield three * three; } };
Code language: Java (java)

Our IDE is happy to do that for us as well:

Automatic removal of the 'default' branch
Automatic removal of the 'default' branch

The notation without default case is shorter and assists us in future extensions of the enum. If we extend it – e.g., by a NEWDAY – the compiler will tell us that the switch expression is now incomplete:

Incomplete 'switch' expression
Incomplete 'switch' expression

With this, you have learned all the new switch possibilities that Java 14 offers. I hope that you can use a current JDK for your next task to be able to use the extended switch statements and expressions.

(Switch Expressions were first introduced as a preview feature in Java 12. In the second preview in Java 13, the keyword break, used initially to return values, was replaced by yield. Due to positive feedback, Switch Expressions were released as a final feature in Java 14 by JDK Enhancement Proposal 361 without further changes.)

Helpful NullPointerExceptions

We all know the following problem: Our code throws a NullPointerException:

Exception in thread "main" java.lang.NullPointerException at eu.happycoders.BusinessLogic.calculate(BusinessLogic.java:80)
Code language: plaintext (plaintext)

And in the code, we find something like this:

long value = context.getService().getContainer().getMap().getValue();
Code language: Java (java)

What is null now?

  • context?
  • context.getService()?
  • Service.getContainer()?
  • Container.getMap()?
  • Map.getValue()? (in case this method returns a Long object)

To fix the error, we have the following options:

  • We could analyze the source code.
  • We could debug the application (very costly if the error is difficult to reproduce or only occurs in production).
  • We could split the code into multiple lines and rerun it (we would have to redeploy the application and wait for the error to occur again).

After upgrading to Java 14, you won't have to ask yourself this question anymore because then the error message will look like this, for example:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Map.getValue()" because the return value of "Container.getMap()" is null at eu.happycoders.BusinessLogic.calculate(BusinessLogic.java:80)
Code language: plaintext (plaintext)

We can now see exactly where the exception occurred: Container.getMap() returned null, so Map.getValue() could not be called.

Something similar can happen when accessing arrays, as in the following line of code:

this.world[x][y][z] = value;
Code language: Java (java)

If a NullPointerException occurs in this line, it was previously not possible to tell whether world, world[x], or world[x][y] was null. With Java 14, this is clear from the error message:

Exception in thread "main" java.lang.NullPointerException: Cannot store to int array because "this.world[x][y]" is null at eu.happycoders.BusinessLogic.calculate(BusinessLogic.java:107)
Code language: plaintext (plaintext)

In Java 14, "Helpful NullPointerExceptions" are disabled by default and must be enabled with -XX:+ShowCodeDetailsInExceptionMessages. In Java 15, this feature will be enabled by default.

(Helpful NullPointerExceptions are specified in JDK Enhancement Proposal 358.)

Experimental, Preview, and Incubator Features

The following features are not yet production-ready. They are shipped at a more or less advanced stage of development to be able to improve them based on feedback from the Java community.

The first three features (Records, Pattern Matching for instanceof, and Text Blocks) are being developed (like Switch Expressions) in Project Amber, whose goal is to make Java syntax more modern and concise.

I will not present the features in all details but only briefly outline each and refer to the Java version in which the features reach production readiness. I will then cover them in detail in the corresponding article of this series.

Records (Preview)

The first new preview feature in Java 14 is Records, defined in JDK Enhancement Proposal 359.

A record offers a compact syntax for a class with only final fields. These are set in the constructor and can be read via access methods.

Here is a simple example:

record Point(int x, int y) {}
Code language: Java (java)

This one-liner creates a class Point with

  • final instance fields x and y,
  • a constructor setting both fields,
  • and access methods x() and y() to read the fields.

Point can be used as follows:

Point p = new Point(3, 5); int x = p.x(); int y = p.y();
Code language: Java (java)

The equals(), hashCode() and toString() methods are generated automatically for records.

Records can have static fields and methods but no instance fields. They can implement interfaces but cannot inherit from other classes. They are implicitly final, so you can't inherit from them either.

For a detailed presentation, see the article on Java 16 when records reach production maturity.

Pattern Matching for instanceof (Preview)

The second preview in Java 14 is "Pattern Matching for instanceof". This feature, defined in JDK Enhancement Proposal 305, eliminates the annoying need to cast to the target type after an instanceof check.

The easiest way to illustrate this is with an example.

The following code gets the Object obj. If it is a String and longer than five characters, it should be converted to uppercase and printed. If it is an Integer, it should be squared and printed.

To do this, we need to perform a cast in lines 4 and 9:

Object obj = getObject(); if (obj instanceof String) { String s = (String) obj; if (s.length() > 5) { System.out.println(s.toUpperCase()); } } else if (obj instanceof Integer) { Integer i = (Integer) obj; System.out.println(i * i); }
Code language: Java (java)

Many of us have become so accustomed to this style that we no longer even question it. But there is a better way!

Starting with Java 14, we can omit the casts and write the code as follows instead:

if (obj instanceof String s) { if (s.length() > 5) { System.out.println(s.toUpperCase()); } } else if (obj instanceof Integer i) { System.out.println(i * i); }
Code language: Java (java)

After an instanceof statement, we can now specify a variable name. If obj is of the specified type, it is bound to the new variable name; this new variable is then of the specified target type and visible in the "then block".

We can even go one step further and combine the if statements of lines 1 and 2:

if (obj instanceof String s && s.length() > 5) { System.out.println(s.toUpperCase()); } else if (obj instanceof Integer i) { System.out.println(i * i); }
Code language: Java (java)

Thus, we have reduced nine lines of code to five while significantly increasing readability.

Just like Records, "Pattern Matching for instanceof" will be production-ready in Java 16.

Text Blocks (Second Preview)

Text Blocks were introduced in Java 13 as a preview. They allow the notation of multiline strings as in the following example:

String sql = """ SELECT id, firstName, lastName FROM Employee WHERE departmentId = "IT" ORDER BY lastName, firstName""";
Code language: Java (java)

JDK Enhancement Proposal 368 introduces two new escape sequences in Java 14:

Backslash at the End of the Line

A backslash ('\') at the end of a line signals that there should be no line break before the following line – similar to Linux shell commands. Here is an example:

String sql = """ SELECT * FROM Employee WHERE departmentId = "IT" ORDER BY lastName, firstName"""; System.out.println("sql = " + sql);
Code language: Java (java)

The program prints the following:

sql = SELECT * FROM Employee WHERE departmentId = "IT" ORDER BY lastName, firstName
Code language: plaintext (plaintext)

Space with \s

Trailing spaces are usually removed, as in the following example (the dots are supposed to represent spaces):

String text = """ one····· two····· three···"""; text.lines().map(line -> "|" + line + "|").forEachOrdered(System.out::println);
Code language: Java (java)

The program prints the following:

|one| |two| |three|
Code language: plaintext (plaintext)

In this example, however, we would like to preserve the spaces. As of Java 14, this is possible by using the escape sequence \s instead of spaces:

String text = """ onesssss twosssss threesss""";
Code language: Java (java)

It is clearer and completely sufficient if we only escape the last space :-)

String text = """ one \s two \s three \s""";
Code language: Java (java)

Now the program prints the following as desired:

|one | |two | |three |
Code language: plaintext (plaintext)

Text Blocks will reach production status in the next release, Java 15. In the next part of the series, I will describe them in full detail.

ZGC on macOS + Windows (Experimental)

ZGC, a garbage collector developed by Oracle to achieve pause times of 10 ms or less, was first introduced in Java 11 as an experimental feature for Linux.

The JDK Enhancement Proposals JEP 364 and JEP 365 now make the Z Garbage Collector available on macOS and Windows (still as an experimental feature).

You can enable ZGC with the JVM flags -XX:+UnlockExperimentalVMOptions -XX:+UseZGC.

ZGC will be ready for production in Java 15. I will present it in detail in the corresponding article.

Packaging Tool (Incubator)

The jpackage tool is being developed based on JDK Enhancement Proposal 343. You can use this tool to create a platform-specific installer for a Java application, which in turn installs the application and the JRE required for it.

Platform-specific means that the installer feels familiar to users of a particular platform. On Windows, for example, this is an .msi or .exe file that is launched by double-clicking it. For macOS, a .pkg or .dmg file. And for Linux, a .deb or .rpm file.

The functionality is based on the javapackager tool, which was included since JDK 8, but removed in Java 11 along with JavaFX.

jpackage will be ready for production in Java 16. In the corresponding article of this series, I will show how to use the tool.

Foreign-Memory Access API (Incubator)

JDK Enhancement Proposal 370 introduces an API that allows Java programs to efficiently and securely access memory outside the Java heap.

The Foreign-Memory Access API is part of Project Panama, which aims to create a faster and easier-to-use replacement for the Java Native Interface (JNI).

This interface will remain in incubator status until at least Java 17.

Performance Improvements

Java 14 has some performance improvements in memory access and garbage collectors.

Non-Volatile Mapped Byte Buffers

With a MappedByteBuffer, you can "map" a file into a memory region to read and write it via regular memory access operations. In the "Memory-mapped files" section of the article "Java Files" series, you can read more about this.

Changed data must be transferred to the storage medium regularly. For NVMs, there are more efficient procedures with less overhead than with conventional storage media.

Starting with Java 14, you can use these efficient mechanisms. For this purpose, you have to specify that the file is located on an NVM medium when creating the MappedByteBuffer. To do this, select one of the new modes ExtendedMapMode.READ_ONLY_SYNC or ExtendedMapMode.READ_WRITE_SYNC when calling FileChannel.map(), as in the following example:

try (FileChannel channel = FileChannel.open( Path.of("test-file.bin"), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ)) { MappedByteBuffer buffer = channel.map(ExtendedMapMode.READ_WRITE_SYNC, 0, 256); // read from / write to the buffer }
Code language: Java (java)

Non-Volatile Mapped Byte Buffers are only available on Linux since only Linux has the required special operating system calls.

(Non-Volatile Byte Buffers are specified in JDK Enhancement Proposal 352.)

NUMA-Aware Memory Allocation for G1

The physical distance between CPU core and memory module plays an increasingly important role on modern machines with multiple CPUs and multiple cores per CPU. The further away a memory module is from the CPU core, the higher the latency for memory access.

As of Java 14, through JDK Enhancement Proposal 345, the G1 Garbage Collector can take advantage of such architectures to increase overall performance.

Threads are assigned to nearby NUMA nodes. Objects created by a thread are always created on the same NUMA node. And as long as they are in Young Generation, they remain on that node by being evacuated only to Survivor Regions on the same NUMA node.

NUMA-Aware Memory Allocation is only available for Linux, and you must explicitly enable it via the VM option +XX:+UseNUMA.

Parallel GC Improvements

In the parallel garbage collector, management for parallel processing of tasks has been optimized, potentially leading to significant performance improvements.

Deprecations and Deletions

The following features have been marked as "deprecated" or "for removal" or permanently removed from the JDK in Java 14.

Thread Suspend/Resume Are Deprecated for Removal

Besides Thread.stop(), the following methods have also been marked as "deprecated" since Java 1.2:

  • Thread.suspend()
  • Thread.resume()
  • ThreadGroup.suspend()
  • ThreadGroup.resume()
  • ThreadGroup.allowThreadSuspension()

The reason for this is that thread suspension is highly prone to deadlocks:

If Thread.suspend() is called within a synchronized block, the corresponding monitor remains locked at least until Thread.resume() is called. However, if this happens in another thread, within a synchronized block on the same monitor, this second thread will block when trying to enter the synchronized block.

In Java 14, the above methods were marked as "for removal".

The method Thread.stop(), which is also marked as "deprecated" – and in my eyes much more dangerous – has not been marked as "for removal" yet and will probably be with us for a while.

(No JDK enhancement proposal exists for this change.)

Deprecate the Solaris and SPARC Ports

The Solaris operating system and the SPARC processor architecture are no longer state-of-the-art. To use development resources elsewhere, Oracle has proposed in JDK Enhancement Proposal 362 to mark the Solaris/SPARC, Solaris/x64, and Linux/SPARC ports as "deprecated" in Java 14 and to remove them entirely in one of the subsequent releases.

Remove the Concurrent Mark Sweep (CMS) Garbage Collector

The Concurrent Mark Sweep (CMS) garbage collector was marked as "deprecated" in Java 9 with JEP 291. Development resources should be reallocated in favor of more modern garbage collectors such as G1GC and ZGC.

As a replacement for CMS, the allrounder G1, available since Java 6 and promoted to the standard garbage collector in Java 9, is recommended.

JEP 363 finally removes CMS from the JDK in Java 14.

Deprecate the ParallelScavenge + SerialOld GC Combination

There is an unusual and rarely used combination of GC algorithms: the pairing of parallel GC algorithm for the young generation ("ParallelScavenge") and serial algorithm for the old generation ("SerialOld").

You can activate this combination by enabling the ParallelGC and disabling the ParallelOldGC at the same time, which in turn automatically enables the SerialOldGC:

-XX:+UseParallelGC -XX:-UseParallelOldGC

This setting can reduce the total memory consumption of up to 3% of the Java heap.

However, the upside does not outweigh the high maintenance effort. Therefore, it was decided to use the development resources elsewhere and mark this GC combination as "deprecated" in Java 14 (JEP 366).

As a substitute, parallel GC is recommended for both young and old generations, which is activated as follows:

-XX:+UseParallelGC

The ParallelScavenge + SerialOld combination will no longer be usable in Java 15.

Remove the Pack200 Tools and API

The compression method for .class and .jar files, Pack200, introduced in Java 5, has been marked as "deprecated" in Java 11.

In times of 100-Mbps DSL lines and 18-TB hard disks, the effort required to maintain such unique compression methods – just to squeeze out a few extra bytes compared to standard methods – is out of all proportion to the benefits.

With JDK Enhancement Proposal 367, Pack200 and the associated tools were finally removed from the JDK.

Other Changes in Java 14

In this category, I have usually listed changes that Java developers did not necessarily need to know about. In Java 14, however, the "other changes" are pretty interesting.

JFR Event Streaming

In the article on Java 11, I introduced Java Flight Recorder (JFR) and JDK Mission Control (JMC). Flight Recorder collects valuable data about the JVM during the execution of an application and stores it in a file. You can then visualize the stored data with Mission Control:

JDK Mission Control
JDK Mission Control

As of Java 14, JDK Enhancement Proposal 349 enables continuous monitoring of a Java application by allowing the data collected by Flight Recorder to be read from within the running application (instead of storing it in a file and analyzing it after the fact).

The following sample source code shows how this works:

int[] array = createRandomArray(1_000_000_000); try (var rs = new RecordingStream()) { rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1)); rs.onEvent( "jdk.CPULoad", event -> { float jvmUser = event.getFloat("jvmUser"); float jvmSystem = event.getFloat("jvmSystem"); float machineTotal = event.getFloat("machineTotal"); System.out.printf( Locale.US, "JVM User: %5.1f %%, JVM System: %5.1f %%, Machine Total: %5.1f %%%n", jvmUser * 100, jvmSystem * 100, machineTotal * 100); }); rs.startAsync(); Arrays.parallelSort(array); }
Code language: Java (java)

First, you create a stream of JFR events with new RecordingStream().

You activate a concrete event (in the example "jdk.CPULoad" with RecordingStream.enable().

Using RecordingStream.onEvent(), we define how to react to the event. The event itself consists of several data fields, which we can read with getFloat() – or other getters, depending on the data type.

With RecordingStream.startAsync(), we start the recording in a separate thread. In the main thread, we sort an array with a billion elements, which takes about 15 seconds on my laptop.

Meanwhile, you can see well from the flight recorder data that Arrays.parallelSort() almost completely utilizes the CPU:

JVM User: 45.1 %, JVM System: 15.0 %, Machine Total: 60.1 % JVM User: 86.5 %, JVM System: 0.5 %, Machine Total: 95.2 % JVM User: 91.8 %, JVM System: 0.3 %, Machine Total: 100.0 % JVM User: 93.0 %, JVM System: 0.2 %, Machine Total: 96.9 % ...
Code language: plaintext (plaintext)

Accounting Currency Format Support

In some countries, such as the USA, negative numbers in accounting are not represented by a minus sign but by parenthesis.

In Java 14, the so-called language tag extension "u-cf-account" is added, which allows specifying in a Locale object the additional information whether we use it in the context of accounting.

You can use this extension as in the following example:

// Example *without* language tag extension Locale locale = Locale.forLanguageTag("en-US"); NumberFormat cf = NumberFormat.getCurrencyInstance(locale); System.out.println("Normal: " + cf.format(-14.95)); // Example *with* language tag extension Locale localeAccounting = Locale.forLanguageTag("en-US-u-cf-account"); NumberFormat cfAccounting = NumberFormat.getCurrencyInstance(localeAccounting); System.out.println("Accounting: " + cfAccounting.format(-14.95));
Code language: Java (java)

As of Java 14, the program prints the following:

Normal: -$14.95 Accounting: ($14.95)
Code language: plaintext (plaintext)

If you run the program under Java 13 or older, the tag extension is ignored, and the program formats both lines the same:

Normal: -$14.95 Accounting: -$14.95
Code language: plaintext (plaintext)

You can find more Unicode language tag extensions in the article about Java 10.

Complete List of All Changes in Java 14

This article presented all features of Java 14 defined in JDK Enhancement Proposals and some performance improvements and deletions not associated with any JEP.

For a complete list of changes, see the official Java 14 release notes.

Summary

Java 14 is an impressive release. Switch Expressions are ready for production. And thanks to Helpful NullPointerExceptions, we will save a lot of debugging work in the future.

Two additional features from Project Amber have been added to the JDK as previews: Records and "Pattern Matching for instanceof". And Text Blocks have been extended by the escape sequences "Backslash at the end of the line" and "\s".

The (still experimental) low-latency garbage collector ZGC is now also available on Windows and macOS. And those who miss javapackager can now experiment with its successor jpackage.

JFR event streaming and several performance improvements round out the release.

If you liked the article, feel free to leave me a comment or share the article using one of the share buttons at the end.

And if you want to be informed when the next article goes online, click here to sign up for the free HappyCoders newsletter.

Sven Woltmann
About the author
I'm a freelance software developer with more than two decades of experience in scalable Java enterprise applications. My focus is on optimizing complex algorithms and on advanced topics such as concurrency, the Java memory model, and garbage collection. Here on HappyCoders.eu, I want to help you become a better Java programmer. Read more about me here.

Leave a Reply

Your email address will not be published. Required fields are marked *

You might also like the following articles