This covers most of the major language changes from Java 11 (the last LTS) through Java 17.
Records #
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 #
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 accessibleNeither 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 #
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
defaultcase. Hence why Sealed cases were created
Pattern Matching for instanceof
#
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 hereWith 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 #
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
""";