Skip to main content

Java 17 Features with Examples: Records, Sealed Classes, Text Blocks, Switch Expressions

Everything that became final between Java 12 and Java 17, with examples and real output from JDK 17: records, sealed classes, text blocks, switch expressions, pattern matching for instanceof, helpful NullPointerException messages, the RandomGenerator API and strong encapsulation of JDK internals, plus interview questions.

Java 17 (September 2021) is a long-term-support release, and its value is cumulative: it is the first LTS that contains records, text blocks, switch expressions, pattern matching for instanceof and sealed classes as final features, plus strong encapsulation of JDK internals. Those did not all arrive in 17. They arrived one release at a time between 14 and 17, so a Java 11 developer sees them all at once. This guide gives each feature a short example, the real output from a JDK 17, and the compiler errors you will actually meet. Where a feature is not final in 17 (pattern matching for switch), it says so and shows the error.
FeatureFinal inJEPWhat it replaces
Switch expressionsJava 14361Statement switch with break and fall-through
Text blocksJava 15378Concatenated, escaped multi-line strings
RecordsJava 16395Boilerplate data classes (constructor, getters, equals, hashCode, toString)
Pattern matching for instanceofJava 16394instanceof followed by a cast
Sealed classesJava 17409Open hierarchies where any class may extend
Strong encapsulation of JDK internalsJava 17403Reflection into java.base working by default
Enhanced pseudo-random generatorsJava 17356A fixed menu of Random classes
Tested against. Temurin 17.0.20.1 for every demo, with Temurin 11.0.32.1 for the strong-encapsulation comparison. Release dates and JEP numbers come from the openjdk.org project pages for JDK 14 (17 March 2020), 15 (15 September 2020), 16 (16 March 2021) and 17 (14 September 2021). The runnable project is the java17-features module; ./run.sh regenerates every output quoted here.

How the Java 17 feature set arrived, one release at a time

If you are on Java 11, here is the path you are skipping. Each release finalised one or two language features that had spent one or two releases as previews, which is why “Java 17 features” is really “Java 12 to 17 features”.
When each feature became final Java 14switch expressions(Mar 2020) Java 15text blocks(Sep 2020) Java 16records, instanceof patterns(Mar 2021) Java 17 (LTS)sealed classes, strong encapsulation(Sep 2021) Pattern matching for switch is NOT on this line: in Java 17 it is only a preview (JEP 406) and became final in Java 21.
The line stops at 17 on purpose. The caption is the part to remember: anything that needs pattern matching inside a switch belongs to the Java 21 guide, and the compiler enforces that boundary, as the last section here shows.

Records: a data class in one line

A record declares an immutable data carrier. From record Point(int x, int y) the compiler generates the private final fields, the constructor, accessors named like the components (x(), not getX()), and equals, hashCode and toString. A compact constructor adds validation without repeating the parameters. Source: RecordsDemo.java.
record Point(int x, int y) {
    Point { if (x < 0 || y < 0) throw new IllegalArgumentException("negative coordinate: " + x + "," + y); }
    double distance() { return Math.sqrt(x * x + y * y); }
}
toString        : Point[x=3, y=4]
equals          : true   hashCode equal: true
accessor        : p.x() = 3, distance = 5.0
compact ctor    : negative coordinate: -1,2
shallow         : Team[name=core, members=[a, b]]   <- the record's list changed after construction
Output from 01-records.txt. The last line is the caveat people miss: a record is shallowly immutable. Its fields cannot be reassigned, but a List held in a field is the same list the caller still holds, so adding to it changes the record. Copy defensively in a compact constructor (List.copyOf) when that matters. Reassigning a field is a compile error (RecordFieldMutation.java):
broken/RecordFieldMutation.java:3: error: cannot assign a value to final variable x
Going deeper: what records cannot do, and where to read more
A record cannot extend a class (it implicitly extends java.lang.Record), cannot declare instance fields beyond its components, and is implicitly final. It can implement interfaces, have static members and nest. The complete treatment, including serialization and JSON mapping, is in the Java records complete guide; for JSON specifically see Jackson with records and Optionals.

Switch expressions: switch that returns a value

A switch expression produces a value, uses arrows (->) so cases do not fall through, accepts several labels per case, and, for an enum, the compiler checks that every constant is covered. A block body returns its value with yield. Source: SwitchExprDemo.java.
int len = switch (d) {
    case SAT, SUN -> 0;
    case MON, TUE, WED, THU, FRI -> {
        int hours = d == Day.MON ? 9 : 8;
        yield hours;
    }
};
MON -> 9 working hours
SAT -> 0 working hours
WED -> 8 working hours
old fall-through: one two
That is 04-switch-expressions.txt. The last line shows what the arrow form removes: in the old statement form, case 1 without a break ran straight into case 2, printing “one two”. Switch expressions make that bug impossible.

Text blocks: multi-line strings without the noise

A text block starts and ends with three quotes. The compiler strips the common leading indentation, so the code can stay indented while the string does not; a trailing backslash joins lines; formatted() (added alongside) fills %s placeholders. Source: TextBlocksDemo.java.
{
  "name": "Ankur",
  "city": "Mumbai"
}
lines           : 4
backslash-newline: joined on one line
formatted       : Hello, reader!
Output from 03-text-blocks.txt. The JSON came out flush left even though the source lines were indented twelve spaces, because the closing delimiter’s position sets the indentation to strip. For the interaction of text blocks with string concatenation and interning, see the Java strings deep dive.

Pattern matching for instanceof: test and cast in one step

if (o instanceof String s) tests the type and binds s as a variable of that type, so the cast line disappears. The binding is in scope only where the test is known to have succeeded, including the right-hand side of &&. Source: PatternInstanceofDemo.java.
if (o instanceof String s && s.length() > 3) return "long string " + s;
if (o instanceof Integer i) return "int " + (i + 1);
long string hello
other hi
int 42
Stream.toList   : [1, 2, 3]
toList() result is unmodifiable
Output: 05-instanceof-pattern.txt. “hi” falls through to “other” because the && s.length() > 3 condition failed even though the type test passed. The last two lines are a small Java 16 addition worth knowing: Stream.toList() returns an unmodifiable list, unlike collect(Collectors.toList()), whose list type is unspecified. See the Streams guide for the rest.

Sealed classes: a closed set of subtypes

A sealed class or interface lists exactly which classes may extend it with permits. Anyone else who tries gets a compile error, and the compiler knows the full set, which is what later makes exhaustive switch possible. Combined with records, sealed types model “one of these shapes” cleanly. Source: SealedDemo.java.
sealed interface Shape permits Circle, Square, Rect {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}
record Rect(double w, double h) implements Shape {}

static double area(Shape s) {
    if (s instanceof Circle c) return Math.PI * c.r() * c.r();
    if (s instanceof Square q) return q.side() * q.side();
    if (s instanceof Rect r) return r.w() * r.h();
    throw new AssertionError("unreachable for a sealed Shape");
}
Circle[r=1.0]              area = 3.142
Square[side=2.0]           area = 4.000
Rect[w=2.0, h=3.0]         area = 6.000
permitted       : [Circle, Square, Rect]
isSealed        : true
From 02-sealed.txt; the last two lines come from reflection (getPermittedSubclasses(), isSealed()), so the restriction exists in the class file, not only in the source. On Java 17 the area method has to be an if chain. A pattern switch would be tidier but is not final until 21. Add a class that is not in the list and the compiler stops you (SealedViolation.java):
broken/SealedViolation.java:5: error: class is not allowed to extend sealed class: Shape (as it is not listed in its permits clause)
Going deeper: sealed rules and exhaustive switch
Every permitted subclass must be declared final, sealed or non-sealed; records are implicitly final. Within one compilation unit the permits clause can be omitted. The payoff, a switch without default that the compiler checks for completeness, is covered in Sealed classes and interfaces with exhaustive switch and in the Java 21 guide.

Helpful NullPointerException messages

Since Java 14 (switched on by default in 15) a NullPointerException says what was null. Instead of a bare exception on a line holding three dereferences, you get the expression. Source: HelpfulNpeDemo.java, compiled with javac -g so local variable names are present.
Cannot read field "city" because "u.address" is null
That is 06-helpful-npe.txt. Without -g the same message names the variable as "<local1>" instead of u, so build with debug info in development if you want readable messages.

RandomGenerator: one interface, many algorithms

JEP 356 added the java.util.random.RandomGenerator interface and named algorithms such as L64X128MixRandom, selected through RandomGeneratorFactory; ThreadLocalRandom and Random now implement the interface. Source: RandomDemo.java.
L64X128MixRandom nextInt(100) x3: 98 1 80
ThreadLocalRandom implements RandomGenerator: true
algorithms >= 10: true
Output from 08-random-generator.txt. The generator is seeded (42L), so the three numbers repeat on every run of this build; the practical point is that code can now accept any RandomGenerator and swap algorithms without changing call sites.

What breaks: strong encapsulation of JDK internals (JEP 403)

This is the Java 17 change that fails at runtime rather than compile time, which is why it surprises people. On Java 8 and 11, reflection into JDK internals worked, and from 9 to 15 the JVM only warned. Java 16 made denial the default (JEP 396) and Java 17 removed the escape-hatch option (JEP 403). Libraries that reached into java.base with setAccessible(true) now throw. The demo reads the private value array of String; run on 11 and on 17 it behaves differently. Source: EncapsulationDemo.java; transcript: 07-strong-encapsulation.txt.
$ javac src/EncapsulationDemo.java && java EncapsulationDemo     (11.0.32.1)
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by EncapsulationDemo (file:/…/) to field java.lang.String.value
read String.value: byte[]

$ javac src/EncapsulationDemo.java && java EncapsulationDemo     (17.0.20.1)
java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @<hash>

$ java --add-opens java.base/java.lang=ALL-UNNAMED EncapsulationDemo     (17.0.20.1)
read String.value: byte[]
On 11 you get a warning and the access succeeds. On 17 you get InaccessibleObjectException. The deliberate fix, when you cannot upgrade the library, is --add-opens, shown in the third run. That works, but it is a flag you must carry in every launch script, container image and IDE run configuration; treat it as a temporary bridge to a library upgrade. Frameworks that did this internally (older Lombok, Mockito, Jackson modules, serialization libraries) published versions that no longer need it, so the first step in a 11-to-17 move is to bump dependencies, then add --add-opens only for what remains.
The fingerprint to recognise. If a library works on Java 11 and dies on 17 with InaccessibleObjectException ... does not "opens ...", this is the cause. The message names the exact package to open, so you can fix it with one --add-opens line as a stopgap.

What is not final in Java 17: pattern matching for switch

A frequent mistake in tutorials is to show switch (o) { case String s -> ... } as a “Java 17 feature”. In 17 it is a preview feature (JEP 406) and the compiler refuses it unless you pass --enable-preview (SwitchPatternOn17.java, 09-compile-errors.txt):
broken/SwitchPatternOn17.java:3: error: patterns in switch statements are a preview feature and are disabled by default.
  (use --enable-preview to enable patterns in switch statements)
Pattern matching for switch became final in Java 21. See pattern matching for switch, record patterns and primitive patterns.
Going deeper: every JEP in JDK 17
The JDK 17 project page lists 14 JEPs:
  • 306 Restore Always-Strict Floating-Point Semantics; 356 Enhanced Pseudo-Random Number Generators
  • 382 New macOS Rendering Pipeline; 391 macOS/AArch64 Port
  • 398 Deprecate the Applet API for Removal; 407 Remove RMI Activation; 410 Remove the Experimental AOT and JIT Compiler
  • 403 Strongly Encapsulate JDK Internals; 409 Sealed Classes
  • 406 Pattern Matching for switch (Preview); 412 Foreign Function & Memory API (Incubator); 414 Vector API (Second Incubator)
  • 411 Deprecate the Security Manager for Removal; 415 Context-Specific Deserialization Filters
Deserialization filters (415) matter if you still use Java serialization; see why Java serialization is dangerous and what replaced it. Moving past 17 to the next LTS is covered in the Java 17 to 25 migration guide.

Common mistakes with Java 17 features

Assuming a record is deeply immutable
Only the fields are final. A mutable component (a List, an array, a Date) can still change, as the “shallow” line above shows. Copy in the compact constructor.
Using pattern matching for switch on Java 17
It is a preview in 17. Use if/instanceof patterns, or move to Java 21.
Forgetting yield in a block switch arm
A case X -> { ... } block in a switch expression must produce a value with yield; a statement-switch block does not need one.
Fixing every InaccessibleObjectException with a blanket --add-opens
Open only the package the message names, and upgrade the offending library when a fixed version exists.
Expecting Stream.toList() to return a mutable list
It is unmodifiable; add throws UnsupportedOperationException. Wrap in new ArrayList<>(...) if you need to modify it.

Common interview questions on Java 17

What are the main features of Java 17?
Sealed classes (JEP 409), strong encapsulation of JDK internals (403), the enhanced pseudo-random generators (356), and everything finalised since the Java 11 LTS: records, text blocks, switch expressions and pattern matching for instanceof. Pattern matching for switch is only a preview in 17.
What does the compiler generate for a record?
Private final fields, a canonical constructor, accessor methods named after the components, and equals, hashCode and toString. You can add a compact constructor for validation, static methods and extra instance methods, but not extra instance fields.
Can a record extend a class? Can it implement an interface?
It cannot extend a class (it implicitly extends java.lang.Record) and is implicitly final, but it can implement interfaces, including sealed ones.
What is a sealed class and why use one?
A class or interface that restricts its direct subtypes with permits. Subtypes must be final, sealed or non-sealed. It models a closed set of alternatives and lets the compiler reason about completeness.
Difference between a switch statement and a switch expression?
An expression yields a value, uses -> arms with no fall-through, must be exhaustive, and uses yield in block arms. The statement form can fall through if you forget break, as the output above shows.
How do text blocks handle indentation?
The compiler removes the common leading whitespace of all content lines and the closing delimiter line, so surrounding code indentation does not leak into the string. A trailing backslash suppresses the line break.
Why does a library that worked on Java 11 fail on Java 17 with InaccessibleObjectException?
JEP 403 strongly encapsulates JDK internals; reflective access to non-open packages is denied. Upgrade the library, or as a stopgap pass --add-opens for the named package.
Is pattern matching for switch available in Java 17?
Only as a preview feature (JEP 406), requiring --enable-preview. It is final in Java 21.
What is the scope of a pattern variable from instanceof?
It is in scope only where the compiler can prove the test succeeded: inside the if body, and in the right operand of &&, but not after the if unless the true branch always exits.

FAQ

Is Java 17 an LTS release?
Yes. Java 17, released 14 September 2021, is a long-term-support release. The next LTS in this series is covered in the Java 21 guide.
Which Java version introduced records?
Records were previewed in Java 14 and 15 and became final in Java 16 (JEP 395).
Which Java version introduced text blocks?
Java 15 finalised them (JEP 378) after previews in 13 and 14.
Should I upgrade from Java 11 to 17?
Usually yes: you gain the features above and the strong encapsulation work, at the cost of fixing any library that reflects into JDK internals. The migration guide covers the steps.
Should you stop at 17? Java 17 is a sensible floor, not a ceiling. Java 21 adds virtual threads, record patterns and final pattern matching for switch, and the upgrade is smaller than 11 to 17 was. See Java 21 features and the Java 21 to 25 LTS guide. If you are still earlier than 17, start with Java 11 and Java 8.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.