Java 14 & 15: Records, Text Blocks, and Sealed Classes
Java 14 finalised switch expressions and previewed records and pattern matching for instanceof. Java 15 made text blocks production-ready, introduced sealed classes in preview, and delivered the long-awaited helpful NullPointerExceptions. Together they laid the groundwork for modern Java's data-oriented programming model.
Java Evolution — Part 6
Java 14 (March 17, 2020) and Java 15 (September 15, 2020) continued the pattern of progressive feature delivery. Switch expressions were finalised, records and pattern matching arrived in preview, text blocks became production-ready, and sealed classes began their journey to standardisation.
Switch Expressions — Finalised (Java 14)
After two preview rounds in Java 12 and 13, switch expressions were standardised in Java 14 (JEP 361). They are now a permanent, stable feature of the language.
// Fully standardised — no --enable-preview flag neededint numLetters = switch (day) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> { System.out.println("Wednesday has 9 letters"); yield 9; }};Records (Preview — Java 14, Finalised Java 16)
Records (JEP 359) are a new kind of class declaration designed for immutable data carriers. Before records, writing a simple data class required a constructor, getters, equals(), hashCode(), and toString() — often dozens of lines for what is conceptually a few fields.
The Problem Records Solve
// Before Java 14 — a simple Point classpublic final class Point { private final int x; private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; } public int y() { return y; }
@Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Point)) return false; Point p = (Point) o; return x == p.x && y == p.y; }
@Override public int hashCode() { return Objects.hash(x, y); }
@Override public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }}The Record Declaration
// Java 14+ — all of the above in one linerecord Point(int x, int y) {}The compiler generates:
- A canonical constructor
Point(int x, int y) - Accessor methods
x()andy()(notgetX()— records use the field name directly) equals()based on all componentshashCode()based on all componentstoString()in the formPoint[x=1, y=2]
Using Records
Point p1 = new Point(1, 2);Point p2 = new Point(1, 2);Point p3 = new Point(3, 4);
p1.x(); // 1p1.y(); // 2p1.equals(p2); // truep1.equals(p3); // falsep1.toString(); // "Point[x=1, y=2]"Customising Records
Records are not just passive data holders. You can add custom logic:
record Range(int min, int max) { // Compact canonical constructor — validates before storing Range { if (min > max) { throw new IllegalArgumentException( "min (%d) must be <= max (%d)".formatted(min, max) ); } }
// Additional methods int length() { return max - min; }
boolean contains(int value) { return value >= min && value <= max; }
// Static factory static Range of(int min, int max) { return new Range(min, max); }}
Range r = Range.of(1, 10);r.length(); // 9r.contains(5); // truer.contains(11); // falseRecords and Interfaces
Records can implement interfaces, which makes them useful for modelling algebraic data types:
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}record Rectangle(double width, double height) implements Shape {}record Triangle(double base, double height) implements Shape {}
double area(Shape shape) { return switch (shape) { case Circle c -> Math.PI * c.radius() * c.radius(); case Rectangle r -> r.width() * r.height(); case Triangle t -> 0.5 * t.base() * t.height(); };}Helpful NullPointerExceptions (Java 14)
Before Java 14, a NullPointerException told you only the line number. On a line with chained method calls, this was often insufficient.
// Before Java 14 — which reference is null?String city = user.getAddress().getCity().toUpperCase();// Exception in thread "main" java.lang.NullPointerExceptionJava 14 (JEP 358) made NPE messages dramatically more informative:
// Java 14+ — the message tells you exactly what was null// Cannot invoke "Address.getCity()" because the return value of// "User.getAddress()" is nullThis feature was enabled by default from Java 15 onwards. It is one of those small changes that saves significant debugging time in production.
Pattern Matching for instanceof (Preview — Java 14)
JEP 305 introduced pattern matching for instanceof. Instead of testing the type and then casting separately, you can do both in one expression.
// Before Java 14 — test then castif (obj instanceof String) { String s = (String) obj; System.out.println(s.toUpperCase());}
// Java 14+ — pattern variable eliminates the castif (obj instanceof String s) { System.out.println(s.toUpperCase());}The pattern variable s is in scope only within the if block (and the condition itself, for &&):
// Combine type check and conditionif (obj instanceof String s && s.length() > 5) { System.out.println("Long string: " + s);}This feature was finalised in Java 16.
Text Blocks — Production Ready (Java 15)
After two preview rounds, text blocks were finalised in Java 15 (JEP 378). They are now a permanent feature with no --enable-preview flag required.
// Fully standard in Java 15+String query = """ SELECT id, name, email FROM users WHERE active = true AND created_at > :since ORDER BY name """;
String config = """ { "server": { "host": "localhost", "port": 8080 }, "database": { "url": "jdbc:postgresql://localhost/mydb" } } """;Sealed Classes (Preview — Java 15)
JEP 360 introduced sealed classes as a preview feature. A sealed class or interface restricts which other classes may extend or implement it. This gives you precise control over the class hierarchy and enables the compiler to reason exhaustively about all possible subtypes.
// Java 15 preview — sealed hierarchypublic abstract sealed class Shape permits Circle, Rectangle, Triangle { abstract double area();}
public final class Circle extends Shape { private final double radius; Circle(double radius) { this.radius = radius; }
@Override public double area() { return Math.PI * radius * radius; }}
public final class Rectangle extends Shape { private final double width, height; Rectangle(double width, double height) { this.width = width; this.height = height; }
@Override public double area() { return width * height; }}
public non-sealed class Triangle extends Shape { // non-sealed — allows further extension private final double base, height; Triangle(double base, double height) { this.base = base; this.height = height; }
@Override public double area() { return 0.5 * base * height; }}The three modifiers for permitted subclasses are:
| Modifier | Meaning |
|---|---|
final | Cannot be extended further |
sealed | Can be extended, but only by its own permits list |
non-sealed | Opens the hierarchy — any class may extend it |
Sealed classes were finalised in Java 17.
ZGC — Production Ready (Java 15)
The Z Garbage Collector, introduced as experimental in Java 11, was declared production-ready in Java 15 (JEP 377). It delivers consistently low pause times (typically under 1ms) regardless of heap size, making it suitable for latency-sensitive production workloads.
java -XX:+UseZGC -Xmx32g -Xms32g MyLowLatencyServiceSummary
Java 14 and 15 delivered several features that are now central to modern Java development. Records in particular changed how developers model data, and sealed classes set the stage for the powerful pattern matching that would arrive in Java 21.
| Feature | Version | Key Benefit |
|---|---|---|
| Switch Expressions (Final) | 14 | Stable, expression-oriented switch |
| Records (Preview) | 14 | Concise, immutable data classes |
| Helpful NullPointerExceptions | 14 | Precise NPE messages for faster debugging |
Pattern Matching for instanceof (Preview) | 14 | Eliminate redundant casts |
| Text Blocks (Final) | 15 | Multi-line strings without escape sequences |
| Sealed Classes (Preview) | 15 | Controlled, exhaustive class hierarchies |
| ZGC (Production) | 15 | Sub-millisecond GC pauses at scale |