switch), it says so and shows the error.
| Feature | Final in | JEP | What it replaces |
|---|---|---|---|
| Switch expressions | Java 14 | 361 | Statement switch with break and fall-through |
| Text blocks | Java 15 | 378 | Concatenated, escaped multi-line strings |
| Records | Java 16 | 395 | Boilerplate data classes (constructor, getters, equals, hashCode, toString) |
Pattern matching for instanceof | Java 16 | 394 | instanceof followed by a cast |
| Sealed classes | Java 17 | 409 | Open hierarchies where any class may extend |
| Strong encapsulation of JDK internals | Java 17 | 403 | Reflection into java.base working by default |
| Enhanced pseudo-random generators | Java 17 | 356 | A 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”.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. Fromrecord 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 withpermits. 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) aNullPointerException 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 thejava.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 intojava.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 withInaccessibleObjectException ... does not "opens ...", this is the cause. The message names the exact package to open, so you can fix it with one--add-opensline as a stopgap.
What is not final in Java 17: pattern matching for switch
A frequent mistake in tutorials is to showswitch (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
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
- Companion code: java17-features (sources, the three compile-error cases, and every captured output).
- On ankurm.com: records, sealed classes, pattern matching, immutable collections, strings deep dive, Collections interview questions, Stream API interview questions.
- Official: JDK 17 on openjdk.org, JEP 395 (records), JEP 409 (sealed classes), JEP 403 (strong encapsulation).
No Comments yet!