A fast-forward tour through new, production-ready features when upgrading from Java 11 to Java 17.

Photo by Anders Jilden, https://unsplash.com/photos/-N2UXcPBIYI

Three years after Java 11 (the last long term support version so far), Java 17 LTS will be released in September 2021. Time to take a quick tour through the new features that developers can enjoy after upgrading from 11 to 17. Note that many more improvements have been made under the hood - this article focuses on those features that can be directly used by most developers.

Feature JEP
Switch Expressions 361
Text Blocks 378
Packaging Tool 392
Pattern Matching for instanceof 394
Records 395
Sealed Classes 409

Switch Expressions

Switch can now return a value, just like an expression:

// assign the group of the given planet to a variable
String group = switch (planet) {
  case MERCURY, VENUS, EARTH, MARS -> "inner planet";
  case JUPITER, SATURN, URANUS, NEPTUNE -> "outer planet";
};

If the right-hand side of a single case requires more code, it can be written inside a block, and the value returned using yield:

// print the group of the given planet, and some more info,
// and assign the group of the given planet to a variable
String group = switch (planet) {
  case EARTH, MARS -> {
    System.out.println("inner planet");
    System.out.println("made up mostly of rock");
    yield "inner";
  }
  case JUPITER, SATURN -> {
    System.out.println("outer planet");
    System.out.println("ball of gas");
    yield "outer";
  }
};

Compared to a traditional switch, the new switch expression

  • uses “->” instead of “:”
  • allows multiple constants per case
  • does not have fall-through semantics (i.e., doesn’t require breaks)
  • makes variables defined inside a case branch local to this branch

Moreover, the compiler guarantees switch exhaustiveness in that exactly one of the cases gets executed, meaning that either

  • all possible values are listed as cases (as with the above enum consisting of eight planets), or
  • a “default” branch has to be provided

Switching with the new arrow labels does not require returning a value:

// print the group of the given planet
// without returning anything
switch (planet) {
  case EARTH, MARS -> System.out.println("inner planet");
  case JUPITER, SATURN -> System.out.println("outer planet");
}

However, note that for this switch statement (as opposed to a switch expression that returns a value), the compiler does not guarantee exhaustiveness.

Text Blocks

Text blocks allow writing multi-line strings that contain double quotes, without having to use \n or \" escape sequences:

String block = """
  Multi-line text
   with indentation
    and "double quotes"!
  """;

A text block is opened by three double quotes """ followed by a line break, and closed by three double quotes.

The Java compiler applies a smart algorithm to strip leading white space from the resulting string such that

  • the indentation that is relevant only for better readability of the Java source code is removed
  • the indentation relevant to the string itself remains untouched

In the above example, the resulting string looks as follows, where each . marks a white space:

Multi-line.text
.with.indentation
..and."double.quotes"!

Imagine a vertical bar that spans the text block’s height, moving from left to right and deleting white spaces until it touches the first non-whitespace character. The closing text block delimiter also counts, so moving it two positions to the left

String block = """
  Multi-line text
   with indentation
    and "double quotes"!
""";

results in the following string:

..Multi-line.text
...with.indentation
....and."double.quotes"!

In addition, trailing white space is removed from every line, which can be prevented by using the new escape sequence \s.

Line breaks inside text blocks can be escaped:

String block = """
    No \
    line \
    breaks \
    at \
    all \
    please\
    """;

results in the following string, without any line breaks:

No.line.breaks.at.all.please

Alternatively, the final line break can also be removed by appending the closing delimiter directly to the string’s end:

String block = """
    No final line break
    at the end of this string, please""";

Inserting variables into a text block can be done as usual with the static method String::format, or with the new instance method String::formatted, which is a little shorter to write:

String block = """
    %s marks the spot.
    """.formatted("X");

Packaging Tool

Suppose you have a JAR file demo.jar in a lib directory, along with additional dependency JARs. The following command

jpackage --name demo --input lib --main-jar demo.jar --main-class demo.Main

packages up this demo application into a native format corresponding to your current platform:

  • Linux: deb or rpm
  • Windows: msi or exe
  • macOS: pkg or dmg

The resulting package also contains those parts of the JDK that are required to run the application, along with a native launcher. This means that users can install, run, and uninstall the application in a platform-specific, standard way, without having to explicitly install Java beforehand.

Cross-compilation is not supported: If you need a package for Windows users, you must create it with jpackage on a Windows machine.

Package creation can be customized with many more options, which are documented in the jpackage man page.

Pattern Matching for instanceof

Pattern matching for instanceof eliminates boilerplate code for performing casts after type comparisons:

Object o = "string disguised as object";
if (o instanceof String s) {
  System.out.println(s.toUpperCase());
}

In the example above, the scope of the new variable s is intuitively limited to the if branch. To be precise, the variable is in scope where the pattern is guaranteed to have matched, which also makes the following code valid:

if (o instanceof String s && !s.isEmpty()) {
  System.out.println(s.toUpperCase());
}

And also vice versa:

if (!(o instanceof String s)) {
  throw new RuntimeException("expecting string");
}
// s is in scope here!
System.out.println(s.toUpperCase());

Records

Records reduce boilerplate code for classes that are simple data carriers:

record Point(int x, int y) { }

This one-liner results in a record class that automatically defines

  • fields for x and y (both private and final)
  • a canonical constructor for all fields
  • getters for all fields
  • equals, hashCode, and toString (taking all fields into account)
// canonical constructor
Point p = new Point(1, 2);
    
// getters - without "get" prefix
p.x();
p.y();
    
// equals / hashCode / toString
p.equals(new Point(1, 2)); // true
p.hashCode();              // depends on values of x and y
p.toString();              // Point[x=1, y=2]

Some of the most important restrictions of record classes are that they

  • are immutable (since their fields are private and final)
  • are implicitly final
  • cannot define additional instance fields
  • always extend the Record class

However, it is possible to

  • define additional methods
  • implement interfaces
  • customize the canonical constructor and accessors
record Point(int x, int y) {

  // explicit canonical constructor
  Point {

    // custom validations
    if (x < 0 || y < 0) 
      throw new IllegalArgumentException("no negative points allowed");

    // custom adjustments (usually counter-intuitive)
    x += 1000;
    y += 1000;

    // assignment to fields happens automatically at the end

  }
  
  // explicit accessor
  public int x() {
    // custom code here...
    return this.x;
  }
  
}

Besides, it is possible to define a local record inside a method:

public void withLocalRecord() {
  record Point(int x, int y) { };
  Point p = new Point(1, 2);
}

Sealed Classes

A sealed class explicitly lists the permitted direct subclasses. Other classes must not extend from this class:

public sealed class Parent
  permits ChildA, ChildB, ChildC { ... }

Likewise, a sealed interface explicitly lists the permitted direct subinterfaces and implementing classes:

sealed interface Parent
  permits ChildA, ChildB, ChildC { ... }

The classes or interfaces in the permits list must be located in the same package (or in the same module if the parent is in a named module).

The permits list can be omitted if the subclasses (or interfaces) are located inside the same file:

public sealed class Parent {
  final class Child1 extends Parent {}
  final class Child2 extends Parent {}
  final class Child3 extends Parent {}
}

Every subclass or interface in the permits list must use exactly one of the following modifiers:

  • final (disallows further extension; for subclasses only, since interfaces cannot be final)
  • sealed (permits further, limited extension)
  • non-sealed (permits unlimited extension again)