Derik Lima

Java 14 Features with Examples

javajava14

Java 14 has brought many new features. Some of them are related to memory management and Garbage Collection whereas some others are more about making Java development easier. On this blog post, I'll focus on the ones that make Java development easier, with examples to facilitate your understanding.

Some of these are Preview Features, which basically means they're implemented and working but might suffer big changes in the future or completely cease to exist so I wouldn't recommend using it for anything other than a small personal project or just for learning purposes.

In order to use these Preview Features, we need to explicitly tell the JDK that we are using them in our code before the JDK compiles our code. Let's see a few ways we can enable it.

Enabling preview features in the Command-line #

If you want to execute your Java class directly from the command-line, all you need is this command:

java --source 14 --enable-preview J14Instanceof.java

The command is fairly simple. You need to pass the java version you want to compile and execute the code with (--source) and pass the --enable-preview argument to let the JDK know you're using Preview features.

Nowadays it's not needed to compile the class with javac before running it with java anymore. The command java already compiles the code for you.

Enabling preview features in Maven #

To enable preview features in Maven, you need to add and configure both maven compiler and maven surefire (responsible for running unit tests) plugins, like below.

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>14</release>
<compilerArgs>--enable-preview</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M3</version>
<configuration>
<argLine>--enable-preview</argLine>
</configuration>
</plugin>

Records (Preview) #

A Record is basically a data class, all fields are final and set in the constructor, making the object immutable. It aims to eliminate most of the boilerplate we've got today when implementing simple classes that just hold data.

You can see a very simple example of a Record in the following code:

record Person (String firstName, String lastName) {}

Then, instantiate Person just like a normal class:

Person person = new Person("John", "Doe");
person.firstName();
person.lastName();

You might also add static fields, static methods and instance methods like below:

record Person (String firstName, String lastName) {
public String getFullName() {
return String.format("%s %s", firstName, lastName);
}

public static int getRandomNumber() {
return new Random().nextInt();
}
}

One last thing I believe it's important mentioning is that you can also modify the original constructor:

record Person (String firstName, String lastName) {
public Person {
if (firstName == null) {
throw new IllegalArgumentException("firstName must not be null");
}
}
}

As you can see above, you can omit the parameters and the assignments to the fields.

Text Blocks (Second Preview) #

This one aims a lot to help with String readability and is quite easy to understand, so I'll leave you with an example of how it used to be before Java 13, then the first preview on Java 13 and then the new additions on Java 14.

public static void main(String[] args) {
String multipleLinesString =
"Line 01\n" +
"Line 02\n" +
"Line 03";

String multipleLinesStringJava13 = """
Line 01
Line 02
Line 03
"""
;

String multipleLinesStringJava14 = """
Line 01
Line 02\sLine 02.1
Line 03\
\sLine 03.1
""";

System.out.println(multipleLinesString);
System.out.println("\n---\n");
System.out.println(multipleLinesStringJava13);
System.out.println("\n---\n");
System.out.println(multipleLinesStringJava14);
}

Pattern Matching for instanceof (Preview) #

When you have a method that receive a Object as a parameter or an Interface and you want to verify which implementation it is, usually you'd write two operations: one instanceof to check if the object is the implementation you expect and then you'd cast it to the implementation desired. Java 14 shaves off the casting part, like in the example below:

public static void main(String[] args) {
// Before Java 14
Object obj = new String("java < 14");
if (obj instanceof String) {
String str = (String) obj;
System.out.println(str);
}

// Java 14
Object obj2 = new String("java 14");
if (obj2 instanceof String str) {
System.out.println(str);
}
}

Switch Expressions #

This feature was a preview feature in Java 12 and 13 and now has been promoted to a permanent feature.

There have been many improvements in the switch expressions over the past few Java versions so I'll sum it up in examples of how it used to be a few years ago and how it's nowadays.

public static void main(String[] args) {
System.out.println(fetchFullLanguageName_beforeJava12("PT"));
System.out.println(fetchFullLanguageName_java14("PL"));
System.out.println(fetchFullLanguageNamev2_java14("EN"));
}

private static String fetchFullLanguageName_beforeJava12(String abbreviation) {
String language;
switch (abbreviation) {
case "PL":
language = "Polish";
break;
case "PT":
language = "Portuguese";
break;
case "EN":
language = "English";
break;
default:
throw new IllegalArgumentException("Language non recognizable");
}
return language;
}

private static String fetchFullLanguageName_java14(String abbreviation) {
String language = switch (abbreviation) {
case "PL": yield "Polish";
case "PT": yield "Portuguese";
case "EN": yield "English";
default: throw new IllegalArgumentException("Language non recognizable");
};
return language;
}

private static String fetchFullLanguageNamev2_java14(String abbreviation) {
String language = switch (abbreviation) {
case "PL" -> "Polish";
case "PT" -> "Portuguese";
case "EN" -> "English";
default -> throw new IllegalArgumentException("Language non recognizable");
};
return language;
}

Helpful Null Pointer Exceptions #

If you have been developing Java code for a while, you know how painful it's when you get a NPE (NullPointerException). Java only tells you the line, but doesn't tell you which reference, specifically, is null and you can have calls to many references on the same line. You end up having to debug your code just to find out where, exactly, the NPE is ocurring.

Let's have a look at the following code:

public static void main(String[] args) {
String input = null;
// a bunch of code that will make will forget to initialize 'input'
System.out.println(input.substring(2, 5).toUpperCase());
}

Just bear in mind that the code above is fairly easy to spot where the NPE is, but usually you will have way more complicated cases in real scenarios.

Before Java 14, you'd get the following exception:

Exception in thread "main" java.lang.NullPointerException
at com.deriklima.java14features.ImprovedNPE.main(ImprovedNPE.java:8)

As you can see, java doesn't tell you exactly where the NPE is.

Now, let's add the VM option -XX:+ShowCodeDetailsInExceptionMessages to let the JVM know that we want more details about the exception and see what we get:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.substring(int, int)" because "input" is null
at com.deriklima.java14features.ImprovedNPE.main(ImprovedNPE.java:10)

So much better, isn't it? Now Java tells you exactly which reference is null.

If you're wondering how to add the VM option I mentioned above, it depends on how you're executing your code. If you're running it on the command-line, it should look something like this:

java -XX:+ShowCodeDetailsInExceptionMessages YourClass

As I'm using IntelliJ IDEA, I just add a VM option to the Run Configuration:

VM Option on IntelliJ idea

Well, that's all for now. I might add more features in the future or add new blog posts for other Java versions.

Hope it helps you somehow! Let me know in the comments.