Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

New language features since Java 8 to 16

Last updated on 2021/03/30 to include changes up to JDK 16.

This article is also available in Chinese by Alex Tan.

When Java 8 introduced Streams and Lambdas it was a big change, enabling functional programming style to be expressed with much less boilerplate. While recent versions did not add such impactful features, lots of smaller improvements were made to the language.

Language enhancements after Java 8

Java 16

  • Records
    • Tip: Use Local Records to model intermediate transformations
    • Tip: Check your libraries
  • Pattern Matching for instanceof
    • Tip: Stay tuned for updates

Java 15

  • Text Blocks
    • Tip: Preserve trailing spaces
    • Tip: Produce the correct newline characters for Windows
    • Tip: Pay attention to consistent indentation
  • Helpful NullPointerExceptions
    • Tip: Check your tooling

Java 14

  • Switch Expressions
    • Tip: Use arrow syntax

Java 11

  • Local-Variable Type Inference
    • Tip: Keep readability in mind
    • Tip: Pay attention to preserve important type information
    • Tip: Make sure to read the official style guides

Java 9

  • Allow private methods in interfaces
  • Diamond operator for anonymous inner classes
  • Allow effectively-final variables to be used as resources in try-with-resources statements
    • Tip: Watch out for released resources
  • Underscore is no longer a valid identifier name
  • Improved Warnings

What’s next: Preview features in Java 16

  • Sealed Classes

For an overview of all the JEPs shaping the new platform, including API, performance and security improvements check the curated list of all enhancements since Java 8.

Record Classes

Available since: JDK 16 (Preview in JDK 14 JDK 15)

Record Classes introduce a new type declaration to the language to define immutable data classes. Instead of the usual ceremony with private fields, getters and constructors, it allows us to use a compact syntax:

public record Point(int x, int y) { }

The Record Class above is much like a regular class that defines the following:

  • two private final fields, int x and int y
  • a constructor that takes x and y as a parameter
  • x() and y() methods that act as getters for the fields
  • hashCode, equals and toString, each taking x and y into account

They can be used much like normal classes:

var point = new Point(1, 2);
point.x(); // returns 1
point.y(); // returns 2

Record Classes intended to be transparent carriers of their shallowly immutable data. To support this design they come with a set of restrictions.

Fields of a Record Class are not only final by default, it’s not even possible to have any non-final fields.

The header of the definition has to define everything about the possible states. It can’t have additional fields in the body of the Record Class. Moreover, while it’s possible to define additional constructors to provide default values for some fields, it’s not possible to hide the canonical constructor that takes all record fields as arguments.

Finally, Record Classes can’t extend other classes, they can’t declare native methods, and they are implicitly final and can’t be abstract.

Because its fields are immutable, supplying data to a record is only possible through its constructor.

By default a Record Class only has an implicit canonical constructor. If the data needs to be validated or normalized, the canonical constructor can also be defined explicitly:

public record Point(int x, int y) {
  public Point {
    if (x  0) {
      throw new IllegalArgumentException("x can't be negative");
    }
    if (y  0) {
      y = 0;
    }
  }
}

The implicit canonical constructor has the same visibility as the record class itself. In case it’s explicitly declared, its access modifier must be at least as permissive as the access modifier of the Record Class.

It’s also possible to define additional constructors, but they must delegate to other constructors. In the end the canonical constructor will always be called. These extra constructors might be useful to provide default values:

public record Point(int x, int y) {
  public Point(int x) {
    this(x, 0);
  }
}

Getting the data from a record is possible via its accessor methods. For each field x the record classes has a generated public getter method in the form of x().

These getters can also be explicitly defined:

public record Point(int x, int y) {
  @Override
  public int x() {
    return x;
  }
}

Note, that the Override annotation can be used in this case to make sure that the method declaration explicitly defines an accessor and not an extra method by accident.

Similarly to getters, hashCode, equals and toString methods are provided by default considering all fields; these methods can also be explicitly defined.

Finally, Record Classes can also have static and instance methods that can be handy to get derived information or to act as factory methods:

public record Point(int x, int y) {
  static Point zero() {
    return new Point(0, 0);
  }
  
  boolean isZero() {
    return x == 0 && y == 0;
  }
}

To sum it up: Record Classes are only about the data they carry without providing too much customization options.

Due to this special design, serialization for records are much easier and secure than for regular classes. As written in the JEP:

Instances of record classes can be serialized and deserialized. However, the process cannot be customized by providing writeObject, readObject, readObjectNoData, writeExternal, or readExternal methods. The components of a record class govern serialization, while the canonical constructor of a record class governs deserialization.

Because serialization is based exactly on the field state and deserialization always calls the canonical constructor its impossible to create a Record with invalid state.

From the user point of view, enabling and using serialization can be done as ususal:

public record Point(int x, int y) implements Serializable { }

public static void recordSerializationExample() throws Exception {
  Point point = new Point(1, 2);

  // Serialize
  ObjectOutputStream oos =
    new ObjectOutputStream(new FileOutputStream("tmp"));
  oos.writeObject(point);

  // Deserialize
  ObjectInputStream ois =
    new ObjectInputStream(new FileInputStream("tmp"));
  Point deserialized = (Point) ois.readObject();
}

Note that it’s no longer required to define a serialVersionUID, as the requirement for matching serialVersionUID values is waived for the Record Classes.

Resources:

  • Inside Java Podcast Episode 4: “Record Classes” with Gavin Bierman
  • Inside Java Podcast Episode 14: “Records Serialization” with Julia Boes and Chris Hegarty
  • Towards Better Serialization - Brian Goetz, June 2019
  • Record Serialization

⚠️ Tip: Use Local Records to model intermediate transformations

Complex data transformations require us to model intermediate values. Before Java 16, a typical solution was to rely on Pair or similar holder classes from a library, or to define your own (maybe inner static) class to hold this data.

The problem with this is that the former one quite often proves to be inflexible, and the latter one pollutes the namespace by introducing classes only used in context of a single method. It’s also possible to define classes inside a method body, but due to their verbose nature it was rarely a good fit.

Java 16 improves on this, as now it’s also possible to define Local Records in a method body:

public ListProduct> findProductsWithMostSaving(ListProduct> products) {
  record ProductWithSaving(Product product, double savingInEur) {}

  products.stream()
    .map(p -> new ProductWithSaving(p, p.basePriceInEur * p.discountPercentage))
    .sorted((p1, p2) -> Double.compare(p2.savingInEur, p1.savingInEur))
    .map(ProductWithSaving::product)
    .limit(5)
    .collect(Collectors.toList());
}

The compact syntax of the Record Classes are a nice match for the compact syntax of the Streams API.

In addition to Records, this change also enables the use Local Enums and even Interfaces.

⚠️ Tip: Check your libraries

Record Classes do not adhere to the JavaBeans conventions:

  • They have no default constructor.
  • They do not have setter methods.
  • The accessor methods does not follow the getX() form.

For these reasons, some tools that expect JavaBeans might not fully work with records.

One such case is that records can’t be used as JPA (e.g. Hibernate) entities. There’s a discussion about aligning the specification to Java Records on the jpa-dev mailinglist, but so far I did not find news about the state of the development process. It worth to mention however that Records can be used for projections without problems.

Most other tools I’ve checked (including Jackson, Apache Commons Lang, JSON-P and Guava) support records, but since it’s pretty new there are also some rough edges.

For example, Jackson, the popular JSON library was an early adopter of records. Most of its features, including serialization and deserialization work equally well for Record Classes and JavaBeans, but some features to manipulate the objects are yet to be adapted.

Another example I’ve bumped into is Spring, which also support records out of the box for many cases. The list includes serialization and even dependency injection, but the ModelMapper library — used by many Spring applications — does not support mapping JavaBeans to Record Classes.

My advice is to upgrade and check your tooling before adopting Record Classes to avoid surprises, but generally it’s a fair to assume that popular tools already have most of their features covered.

Check out my experiments with the tool integration for Record Classes on GitHub.

Pattern Matching for instanceof

Available since: JDK 16 (Preview in JDK 14 JDK 15)

In most cases, instanceof is typically followed by a cast:

if (obj instanceof String) {
    String s = (String) obj;
    // use s
}

At least, in the old days, because Java 16 extends instanceof to make this typical scenario less verbose:

if (obj instanceof String s) {
    // use s
}

The pattern is a combination of a test (obj instanceof String) and a pattern variable (s).

The test works almost like the test for the old instanceof, except it results in a compile error if it is guaranteed to pass all the time.

// "old" instanceof, without pattern variable:
// compiles with a condition that is always true
Integer i = 1;
if (i instanceof Object) { ... } // works

// "new" instanceof, with the pattern variable:
// yields a compile error in this case
if (i instanceof Object o) { ... } // error

Note, that the opposite case, where a pattern match will always fail, is already a compile-time error even with the old instanceof.

The pattern variable is extracted from the target only if the test passes. It works almost like a regular non-final variable:

  • it can be modified
  • it can shadow field declarations
  • if there’s a local variable with the same name, it will result in a compile error

However, special scoping rules are applied to them: a pattern variable is in scope where it has definitely matched, decided by flow scoping analysis.

The simplest case is what can be seen in the above example: if the test passes, the variable s can be used inside if block.

But the rule of “definitely matched” also applies for parts of more complicated conditions too:

if (obj instanceof String s && s.length() > 5) {
  // use s
}

s can be used in the second part of the condition because it’s only evaluated when the first one succeeds and the instanceof operator has a match.

To bring an even less trivial example, early returns and exceptions can also guarantee matches:

private static int getLength(Object obj) {
  if (!(obj instanceof String s)) {
    throw new IllegalArgumentException();
  }

  // s is in scope - if the instanceof does not match
  //      the execution will not reach this statement
  return s.length();
}

The flow scoping analysis works similarly to existing flow analyses such as checking for definite assignment:

private static int getDoubleLength(String s) {
  int a; // 'a' declared but unassigned
  if (s == null) {
    return 0; // return early
  } else {
    a = s.length(); // assign 'a'
  }

  // 'a' is definitely assigned
  // so we can use it
  a = a * 2;
  return a;
}

I really like this feature as it’s likely to reduce the unnecessary bloat caused the explicit casts in a Java program. Contrasting it with more modern languages however, this feature still seems to be a bit verbose.

For example in Kotlin you don’t need to define the pattern variable:

if (obj is String) {
    print(obj.length)
}

In Java’s case the pattern variables are added to ensure backwards compatibility as changing the type of obj in obj instanceof String would mean that when obj is used as an argument of an overloaded method, a the call could resolve to a different version of the method.

⚠️ Tip: Stay tuned for updates

The pattern matching feature might not seem like a big deal in its current form, but soon it will get many more interesting features.

JEP 405 proposes to add decomposition features to also match the contents of a Record Class or an Array:

if (o instanceof Point(int x, int y)) {
  System.out.println(x + y);
}

if (o instanceof String[] { String s1, String s2, ... }){
  System.out.println("The first two elements of this array are: " + s1 + ", " + s2);
}

Then, JEP 406 is about adding pattern matching features to switch statements and expressions:

return switch (o) {
  case Integer i -> String.format("int %d", i);
  case Long l    -> String.format("long %d", l);
  case Double d  -> String.format("double %f", d);
  case String s  -> String.format("String %s", s);
  default        -> o.toString();
};

Currently both JEPs are in the Candidate status and don’t have a concrete target version, but I hope that we can see their preview versions soon.

Text Blocks

Available since: JDK 15 (Preview in JDK 13 JDK 14)

Compared to other modern languages, in Java it was notoriously hard to express text containing multiple lines:

String html = "";
html += "\n";
html += "  \n";
html += "    

Hello, world

\n"
; html += " \n"; html += "\n"; System.out.println(html);

To make this situation more programmer-friendly, Java 15 introduced multi-line string literals called Text Blocks:

String html = """
          
            
              

Hello, world

"""
; System.out.println(html);

They are similar to the old String literals but they can contain new lines and quotes without escaping.

Text Blocks start with """ followed by a new line, and end with """. The closing token can be at the end of the last line or in separate line such as is in the example above.

They can be used anywhere an old String literal can be used and they both produce similar String objects.

For each line-break in the source code, there will be a \n character in the result.

String twoLines = """
          Hello
          World
          """;

This can be prevented by ending the line with the \ character, which can be useful in case of very long lines that you’d like to split into two for keeping the source code readable.

String singleLine = """
          Hello \
          World
          """;

Text Blocks can be aligned with neighboring Java code because incidental indentation is automatically removed. The compiler checks the whitespace used for indentation in each line to find the least indented line, and shifts each line to the left by this minimal common indentation.

This means that if the closing """ is in a separate line, the indentation can be increased by shifting the closing token to the left.

String noIndentation = """
          First line
          Second line
          """;

String indentedByToSpaces = """
          First line 
          Second line
        """;

The opening """ does not count for the indentation removal so it’s not necessary to line up the text block with it. For example, both of the following examples produce the same string with the same indentation:

String indentedByToSpaces = """
         First line 
         Second line
       """;

String indentedByToSpaces = """
                              First line 
                              Second line
                            """;

The String class also provides some programmatic ways to deal with indentation. The indent method takes an integer and returns a new string with the specified levels of additional indentation, while stripIndent returns the contents of the original string without all the incidental indentation.

Text Blocks do not support interpolation, a feature I really miss. As the JEP says it may be considered in the future, and until then we can use String::formatted or String::format:

var greeting = """
    hello
    %s
    """.formatted("world");

Resources:

  • Programmer’s Guide To Text Blocks
  • Definitive Guide To Text Blocks In Java 13
  • Java Text Blocks - Bealdung

⚠️ Tip: Preserve trailing spaces

Trailing spaces in Text Blocks are ignored. This is usually not a problem but in some cases they do matter, for example in context of unit test when a method result is compared to a baseline value.

If this is the case be mindful about them and if a line ends with whitespace add \s or \t instead of the last space or tab to the end of the line.

⚠️ Tip: Produce the correct newline characters for Windows

Line endings are represented with different control characters on Unix and Windows. The former one uses a single line feed (\n), while the latter uses carriage return followed by line feed (\r\n).

However, regardless to the operating system you choose to use or how you encode new lines in the source code, Text Blocks will use a single \n for each new line, which can lead to compatibility issues.

Files.writeString(Paths.get(""), """
    first line
    second line
    """);

If a tool compatible only with the Windows line ending format (e.g. Notepad) is used to open such a file, it will display only a single line. Make sure that you use the correct control characters if you also target Windows, for example by calling String::replace to replace each "\n" with "\r\n".

⚠️ Tip: Pay attention to consistent indentation

Text Blocks work well with any kind of indentation: tabs spaces or even the mix of these two. It’s important though to use consistent indentation for each line in the block, otherwise the incidental indentation can’t be removed.

Most editors offer autoformatting and automatically add indentation on each new line when you hit enter. Make sure to use the latest version of these tools to ensure they play well with Text Blocks, and don’t try to add wrong indentations.

Helpful NullPointerExceptions

Available since: JDK 15 (Enabled with -XX:+ShowCodeDetailsInExceptionMessages in JDK 14)

This little gem is not really a language feature, but it’s so nice that I wanted to include it in this list.

Traditionally, experiencing a NullPointerException was like this:

node.getElementsByTagName("name").item(0).getChildNodes().item(0).getNodeValue();

Exception in thread "main" java.lang.NullPointerException
        at Unlucky.method(Unlucky.java:83)

From the exception it’s not obvious which method returned null in this case. For this reason many developers used to spread such statements over multiple lines to make sure they’ll be able to figure out which step led to the exception.

From Java 15, there’s no need to do that because NPE’s describe which part was null in the statement. (Also, in in Java 14 you can enable it with the -XX:+ShowCodeDetailsInExceptionMessages flag.)

Exception in thread "main" java.lang.NullPointerException:
  Cannot invoke "org.w3c.dom.Node.getChildNodes()" because
  the return value of "org.w3c.dom.NodeList.item(int)" is null
        at Unlucky.method(Unlucky.java:83)

(Check the example project on GitHub)

The detailed message contains the action that could not be performed (Cannot invoke getChildNodes) and the reason for the failure (item(int) is null), making it much easier to find the exact source of the problem.

So overall this feature is good for debugging, and also good for code readability as there’s one less reason to sacrifice it for a technical reason.

The Helpful NullPointerExceptions extension is implemented in the JVM so you get the same benefits for code compiled with older Java versions, and when using other JVM languages, such as Scala or Kotlin.

Note, that not all NPEs get this extra info, just the ones that are created and thrown by the JVM upon:

  • reading or writing a field on null
  • invoking method on null
  • accessing or assigning an element of an array (indices are not part of the error message)
  • unboxing null

Also note that this feature does not support serialization. For example, when an NPE is thrown on the remote code executed via RMI, the exception will not include the helpful message.

Currently the Helpful NullPointerExceptions are disabled by default, and have to be enabled with the -XX:+ShowCodeDetailsInExceptionMessages flag.

⚠️ Tip: Check your tooling

When upgrading to Java 15, make sure to check your application and infrastructure to ensure:

  • sensitive variable names not end up in log files and web server responses
  • log parsing tools can handle the new message format
  • the additional overhead required to construct the additional details is okay

Switch Expressions

Available since: JDK 14 (Preview in JDK 12 JDK 13)

The good old switch got a facelift in Java 14. While Java keeps supporting the old switch statement, it adds the new switch expression to the language:

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    default      -> {
        String s = day.toString();
        int result = s.length();
        yield result;
    }
};

The most striking difference is that this new form can be used as an expression. It can be used to populate variables as demonstrated in the example above, and it can be used wherever an expression is accepted:

int k = 3;
System.out.println(
    switch (k) {
        case  1 -> "one";
        case  2 -> "two";
        default -> "many";
    }
);

However, there are some other, more subtle differences between switch expressions and switch statements.

First, for switch expressions cases don’t fall-through. So no more subtle bugs caused by missing breaks. To avoid the need for fall-through, multiple constants can be specified for each case in a comma separated list.

Second, each case has its own scope.

String s = switch (k) {
    case  1 -> {
        String temp = "one";
        yield temp;
    }
    case  2 -> {
        String temp = "two";
        yield temp;
    }
    default -> "many";
}

A branch is either a single expression or if it consist of multiple statements it has to be wrapped in a block.

Third, cases of a switch expression are exhaustive. This means that for String, primitive types and their wrappers the default case always has to be defined.

int k = 3;
String s = switch (k) {
    case  1 -> "one";
    case  2 -> "two";
    default -> "many";
}

For enums either a default case has to be present, or all cases have to be explicitly covered. Relying on the latter is quite nice to ensure that all values are considered. Adding an extra value to the enum will result in a compile error for all switch expressions where it’s used.

enum Day {
   MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

Day day = Day.TUESDAY;
switch (day) {
    case  MONDAY -> ":(";
    case  TUESDAY, WEDNESDAY


This post first appeared on Blog - Advanced Web Machinery, please read the originial post: here

Share the post

New language features since Java 8 to 16

×

Subscribe to Blog - Advanced Web Machinery

Get updates delivered right to your inbox!

Thank you for your subscription

×