The Java 17 source level adds sealed classes and includes records, instanceof pattern matching, text blocks, and switch expressions finalized in earlier releases.
“Available in Java 17” does not mean “introduced in Java 17”; type-pattern switches, record patterns, and when guards are not stable Java 17 syntax.
Compile all sources and tests with a current JDK using javac --release 17, then review mutable record components and every direct branch of sealed hierarchies.
What it is and why it exists
Java 17 features are not one syntax bundle whose parts appeared at the same time. They are a set of language capabilities that the Java 17 source level can use without preview flags. Of these, sealed classes were finalized in Java 17; the other four became final from Java 14 through Java 16. Treating them as one migration baseline separates “what the target project can use” from “when a construct was first released.”
The features remove recurring modeling friction. A record class declares a data carrier from its state; a pattern variable combines a type test with a safe conversion. A text block preserves the source shape of multiline text, while a switch expression makes branches produce a value. A sealed type turns “which direct subtypes are legal” into a compiler-checked constraint.
None of these constructs turns Java into a different language.
A record is still a class, a text block still produces a String, pattern matching still performs a runtime type test, and a switch expression still follows static typing and exhaustiveness rules.
Their value is more accurate expression of intent, not the character count they save.
You meet these boundaries in codebases moving to Java 17, library minimum-version declarations, build-plugin configuration, and AI-generated modern Java. Once a project can use Java 17, the constructs can normally be adopted incrementally rather than through a system-wide rewrite. Source level, runtime version, and dependency class-file version remain three different compatibility questions.
How it works
Five features, five final releases
The table records final releases, not preview releases. Java 17 can compile all five constructs as stable syntax, but only sealed classes became final in Java 17 itself.
| Feature | Final release | Main compiler guarantee |
|---|---|---|
| Switch expressions | Java 14 | Every normally completing path produces a compatible value |
| Text blocks | Java 15 | Line normalization, incidental indentation, and escape processing |
Pattern matching for instanceof | Java 16 | Type binding and flow scoping on successful paths |
| Record classes | Java 16 | Members and value-oriented methods derived from components |
| Sealed classes | Java 17 | Restrictions on permitted direct subclasses or implementors |
A preview feature requires --enable-preview
and is tied to the particular JDK used to compile it.
Production code that promises a stable Java 17 source level should not depend on preview type patterns in switch.
That syntax changed later, so case Type value -> or a when guard copied from a newer tutorial cannot be projected backward as stable Java 17 syntax.
Records declare a data shape
record Account(String owner, Tier tier) declares two record components.
From them, the compiler provides private final fields with the same names, a canonical constructor, owner() and tier() accessors, and equals, hashCode, and toString based on all components.
A record class implicitly extends java.lang.Record and is implicitly final, so it cannot extend another class or be extended by an ordinary class.
A compact canonical constructor omits its parameter list and explicit field assignments. Its body can validate or rebind parameters; the compiler then assigns the final parameter values to fields. This is the right place to establish construction invariants, but it does not automatically copy lists, arrays, or other mutable components.
Pattern variables use flow scoping
The expression value instanceof Account account tests the runtime type before it binds account.
The variable’s availability follows control flow, not only the nearest braces.
The right operand of && can therefore use a successfully matched variable, and code after a negated guard that returns can use it too.
null instanceof Account account evaluates to false; it neither binds the variable nor throws.
Pattern matching removes a repeated explicit cast, but it does not validate fields, close an open hierarchy, or replace business validation.
Sealed types constrain direct inheritance
A sealed type uses permits to list its allowed direct subtypes; the compiler can infer the list when they are declared in the same compilation unit.
Every direct subtype must choose final, sealed, or non-sealed, though records and enums can satisfy the rule implicitly.
non-sealed reopens that branch, so a sealed root does not always imply a permanently finite set of descendants.
In a named module, permitted direct subtypes must belong to the same module as the sealed type. In an unnamed module, they must belong to the same package. The rule gives the type owner control, but it also makes a cross-module plugin contract a poor candidate for a sealed root.
Text blocks and switch still have exact semantics
The opening delimiter of a text block
must be followed by a line terminator.
The compiler normalizes line terminators, removes common incidental indentation, and then interprets escapes; the closing delimiter participates in indentation calculation.
The result is an ordinary String, usually including the final line break before a closing delimiter placed on its own line.
A switch expression must cover every possible selector value.
Arrow rules do not fall through; an expression rule supplies its result directly, while a block rule supplies it with yield.
In stable Java 17, an enum demonstrates this exhaustiveness cleanly, while a pattern switch over a sealed type is still beyond the stable boundary.
Examples
Combine stable features in one Java 17 program
The first example puts a record, an instanceof pattern variable, an enum switch expression, and a text block in one source file.
It needs no preview flag, so it also works as a small Java 17 source-compatibility smoke test.
public class ReleaseFeatures {
enum Tier { BASIC, PRO, ENTERPRISE }
record Account(String owner, Tier tier) {
Account {
if (owner == null || owner.isBlank()) {
throw new IllegalArgumentException("owner is required");
}
owner = owner.strip();
}
}
static String label(Object value) {
if (!(value instanceof Account account)) {
return "unsupported";
}
int seats = switch (account.tier()) {
case BASIC -> 1;
case PRO -> 10;
case ENTERPRISE -> 100;
};
return """
%s (%s)
seats=%d
""".formatted(account.owner(), account.tier(), seats);
}
public static void main(String[] args) {
System.out.print(label(new Account(" Lin ", Tier.PRO)));
System.out.println(label(42));
}
}Lin (PRO)
seats=10
unsupportedThe compact constructor rejects a blank name before the rebound owner parameter is assigned to the record field.
After the negated pattern branch returns, later code can use account as a bound Account.
Every enum constant is covered, so the switch needs no default.
Model a closed result with a sealed interface
Java 17 can declare a sealed hierarchy and use instanceof pattern matching for each branch as stable features.
This example deliberately avoids a type-pattern switch; the final exception also prevents a future edit from failing silently.
import java.util.ArrayList;
import java.util.List;
public class SealedResult {
sealed interface Lookup permits Found, Missing {}
record Found(String id, List<String> roles) implements Lookup {
Found {
roles = List.copyOf(roles);
}
}
record Missing(String id) implements Lookup {}
static String describe(Lookup result) {
if (result instanceof Found found) {
return found.id() + ": " + found.roles().size() + " roles";
}
if (result instanceof Missing missing) {
return missing.id() + ": missing";
}
throw new IllegalStateException("unhandled result: " + result);
}
public static void main(String[] args) {
var sourceRoles = new ArrayList<>(List.of("reader", "author"));
var found = new Found("u-17", sourceRoles);
sourceRoles.add("admin");
System.out.println(describe(found));
System.out.println(describe(new Missing("u-18")));
System.out.println("source roles: " + sourceRoles.size());
System.out.println("snapshot roles: " + found.roles().size());
}
}u-17: 2 roles
u-18: missing
source roles: 3
snapshot roles: 2Both records are implicitly final, so they legally close the two branches of the sealed interface.
List.copyOf gives Found an unmodifiable snapshot; mutating the source list later does not change the record’s value.
A mutable domain object used as a component would still need its own copying or immutability policy.
Make text-block indentation and yield visible
The third example has a block switch rule produce a value with yield, then inserts that result into a text block.
The common indentation shared with the closing delimiter is removed, while the two extra spaces inside the content remain.
public class TextBlockReport {
enum Status { QUEUED, RUNNING, DONE }
static String render(Status status, int count) {
String detail = switch (status) {
case QUEUED -> "waiting";
case RUNNING -> {
if (count == 0) {
yield "starting";
}
yield "processed " + count;
}
case DONE -> "complete";
};
return """
status=%s
detail=%s
""".formatted(status, detail);
}
public static void main(String[] args) {
System.out.print(render(Status.RUNNING, 3));
System.out.print(render(Status.DONE, 0));
}
}status=RUNNING
detail=processed 3
status=DONE
detail=completeyield ends only the current block rule and provides its value to the switch expression; return would try to exit the entire method.
The text block has a final newline because the closing delimiter is on the next line.
Putting the closing delimiter immediately after the final content would omit that automatic newline.
Pitfalls
Fix: Record each final release: switch expressions in 14, text blocks in 15, records and instanceof patterns in 16, and sealed classes in 17.
A migration guide can call them Java 17 baseline features without rewriting their release history.
Fix: Build code that promises Java 17 with javac --release 17 and no --enable-preview.
Use an instanceof chain, a visitor, or polymorphic methods at this source level; if you need final pattern switches, raise the declared minimum to Java 21.
Fix: Validate components in the canonical constructor and apply List.copyOf, array copies, or immutable domain types according to ownership.
Also check whether an accessor re-exposes mutable storage: records provide shallow state constraints, not recursive freezing.
Fix: For a closed enum owned by the current module, name every constant and let source recompilation expose an omission. Handle unknown text or network values at the parsing boundary; source exhaustiveness also does not replace binary-compatibility tests for old consumers.
Fix: Use the serializer, escaper, or parameterized-query API for the target format.
formatted() only performs string formatting; placing untrusted input in a SQL text block still creates an injection flaw.
Fix: Seal a hierarchy only when one module or package owns its type set, and inspect the modifier on every direct subtype.
Use an ordinary interface for extension points; when a branch is intentionally open, do not treat the root permits list as a list of every runtime type.
Migration boundaries and compiled form
--release constrains syntax and platform APIs
On a newer JDK, javac --release 17 applies Java 17 language rules and limits code to the documented Java SE API associated with release 17.
It also produces a class file that Java 17 understands; the Java 17 class-file major version is 61.
Compiling with the current JDK defaults alone does not prove that the result can run on Java 17.
--release does not check whether third-party dependencies also provide Java 17-compatible class files, nor does it prove identical reflection, service-loading, or framework-proxy behavior on the target runtime.
Migration verification therefore needs source compilation, dependency resolution, and tests on an actual Java 17 runtime.
The examples here also run on Java 25 to confirm that these Java 17 class files execute on the repository’s current target JDK.
Records retain structure in the class file
A record class file extends java.lang.Record and carries a Record attribute describing its components.
Its fields, accessors, and object methods are not source text expanded by a preprocessor; they are class members and bootstrap information generated from the record declaration.
Reflection and serialization frameworks can recognize record structure, but support for record construction still depends on the framework version.
Generated equality compares components one by one. That does not turn an array into an element-wise value type, and it does not create a snapshot of a mutable component. If a record serves as a map key or a cross-thread value, component equality, stability, and thread safety remain part of its design contract.
Sealing is a class-file contract
The class file of a sealed class or interface records its permitted direct subtypes. The JVM enforces the constraint while loading related types, so it is more than a hint for the source checker. Adding, removing, or reopening a permitted branch can affect separately compiled code and deserves API-evolution review.
Stable Java 17 syntax still cannot consume that hierarchy with a type-pattern switch, so the example uses an instanceof chain and an explicit fallback.
After moving to a Java 21 or newer source level, an exhaustive pattern switch will normally expose a new permitted branch when consumers are recompiled.
Already published old class files still need binary-compatibility tests; a fresh compilation alone is not enough.
Pattern variables mainly reshape source control flow
An instanceof pattern writes a test, conversion, and local binding as one construct.
The compiler still emits the corresponding runtime type test and conversion operations, then uses control flow to prove where the variable has been bound.
It does not introduce a new dynamic-dispatch protocol or bypass type erasure.
Consequently, value instanceof List<String> strings is still illegal because the runtime cannot test the erased element type argument.
You can match List<?> and then validate elements as the requirement demands.
A shorter binding does not make later casts, nulls, or collection contents safe automatically.
Text blocks and switch promise no performance shortcut
A text block becomes an ordinary string constant or part of ordinary string construction at compile time. A switch expression likewise becomes control flow suited to its selector type; the language contract promises result and exhaustiveness semantics, not that one source form is always faster. Without a benchmark and target JVM configuration, these features should not be sold as performance optimizations.
Choose them for expression during migration: a text block makes multiline content inspectable, and a switch expression lets finite branches produce one value. Performance-sensitive paths still need measurement on the target runtime with representative data and actual compiler options. Shorter source code is not performance evidence.
Further reading
4 questions · 2 predict-the-output · 1 spot-the-bug