Skip to main content
Java 17 (LTS)
  1. Posts/

Java 17 (LTS)

·1013 words·5 mins·
Roman
Author
Roman
Photographer with MSci in Computer Science and a Home Lab obsession
Table of Contents

This covers most of the major language changes from Java 11 (the last LTS) through Java 17.

Records
#

JEP 395 - java.lang.Record

Records are a concise way to declare data carrier classes. They are transparent, immutable, and remove the boilerplate of writing constructors, getters, equals, hashCode, and toString manually.

record Point(int x, int y) {}

// Equivalent to a class with:
// - private final int x, y
// - public int x(), int y()
// - equals(), hashCode(), toString()

The compiler auto-generates for each component:

Member Description
private final field Stores the component value
Accessor method Same name as the component, e.g. x()
equals() True when all components are equal
hashCode() Consistent with equals()
toString() Prints all component names and values

Records have a few key restrictions:

  • Implicitly final – cannot be extended
  • Cannot extend any class (implicitly extends java.lang.Record)
  • Cannot declare instance fields beyond those in the header
  • Cannot be abstract

You can add custom validation using a compact constructor, which omits the parameter list and lets the compiler handle field assignment:

record Range(int min, int max) {
    Range {  // compact constructor
        if (min > max) throw new IllegalArgumentException();
    }
}


// Would throw IllegalArgumentException
Range test = new Range(10, 1);

For more details see Baeldung: Java Record Keyword.


Sealed Classes
#

JEP 409

Sealed Classes allow us to control which code is responsible for implementing it. This in turn will help with exhaustive pattern matching.

Before sealed classes, Java had two ways to restrict extension. Making a class final prevents all subclassing entirely. Making a class package-private limits subclasses to the same package, but it also makes the class invisible to external packages, meaning outside code cannot use it as a type at all:

// package: com.example.shapes
class Shape {}          // package-private

class Circle extends Shape {}  // fine, same package
class Rectangle extends Shape {}  // fine, same package

// package: com.example.app  
Shape s = new Shape(); // Not accessible

Neither approach lets you have a class that is publicly usable but only extendable by a known set of subclasses.

it should be possible for a superclass to be widely accessible (since it represents an important abstraction for users) but not widely extensible (since its subclasses should be restricted to those known to the author). The author of such a superclass should be able to express that it is co-developed with a given set of subclasses, both to document intent for the reader and to allow enforcement by the Java compiler. At the same time, the superclass should not unduly constrain its subclasses by, e.g., forcing them to be final or preventing them from defining their own state

Sealed Class restricts which classes or interfaces may extend or implement a it. Every permitted subclass must be final, sealed, or non-sealed.

public abstract sealed class Shape
        permits Circle, Rectangle, WeirdShape {}

// no further extension
public final class Circle extends Shape {}

// sealed - further restricts subclasses
public sealed class Rectangle extends Shape
    permits TransparentRectangle, FilledRectangle {}

// open again
public non-sealed class WeirdShape extends Shape {}
// Sealed interfaces work the same way
sealed interface Expr permits Num, Add, Mul {}

record Num(int value)        implements Expr {}
record Add(Expr l, Expr r)   implements Expr {}
record Mul(Expr l, Expr r)   implements Expr {}

Records pair naturally with sealed interfaces to model Algebraic Data Types (ADTs).

Sealed and non-sealed may be abstract and can have abstract members


Switch Expressions
#

JEP 361

Extends switch to be usable as an expression that returns a value, not just a statement. Also introduces arrow labels (->) which prevent fall-through.

// Statement (old) - verbose, fall-through prone
int numLetters;
switch (day) {
    // No break - falls through to FRIDAY
    case MONDAY:
    case FRIDAY:
        numLetters = 6;
        break;
    default:
        numLetters = 9;
}

// Expression (new) - concise, no fall-through
int numLetters = switch (day) {
    case MONDAY, FRIDAY -> 6;
    default             -> 9;
};

Use yield to return a value from a block arm:

int result = switch (day) {
    case MONDAY -> 0;
    default -> {
        int k = compute(day);
        yield k;
    }
};

Switch expressions must be exhaustive, all possible values must be covered, typically with a default case. Hence why Sealed cases were created


Pattern Matching for instanceof
#

JEP 394

Eliminates the redundant cast after an instanceof check by introducing a binding variable that is automatically declared and assigned when the test succeeds.

// Before
if (obj instanceof String) {
    String s = (String) obj;  // redundant cast
    System.out.println(s.length());
}

// After
if (obj instanceof String s) {
    System.out.println(s.length());  // s is already bound
}

The binding variable uses flow scoping, it is only in scope where the compiler can prove the match succeeded.

s is in scope after && since the right side only executes if the match succeeded:

if (obj instanceof String s && s.length() > 5) {
    System.out.println(s);  // s in scope
}
// s NOT in scope here

With an early return, the compiler knows s is bound for all code that follows:

if (!(obj instanceof String s)) {
    return;
}
// s in scope - match is guaranteed
System.out.println(s);

Text Blocks
#

JEP 378

Text blocks are multi-line string literals that remove the need for escape sequences and string concatenation. Delimited by triple quotes (""").

// Before
String html = "<html>\n" +
              "    <body>\n" +
              "        <p>Hello</p>\n" +
              "    </body>\n" +
              "</html>";

// After
String html = """
        <html>
            <body>
                <p>Hello</p>
            </body>
        </html>
        """;

The compiler strips common leading whitespace automatically, so indentation in source does not affect the resulting string.

ill-formed text blocks:

String a = """""";   
// no line terminator after opening delimiter
String b = """ """;  
// no line terminator after opening delimiter
String c = """
           ";        
// no closing delimiter (text block continues to EOF)
String d = """
           abc \ def
           """;      

Sources
#