# Java 17 features

Source: https://codewiki.com/java/java17-features/

> - **what**: The Java 17 source level adds sealed classes and includes records, `instanceof` pattern matching, text blocks, and switch expressions finalized in earlier releases.
> - **trap**: “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.
> - **fix**: 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.

<!-- quick -->

```java
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));
    }
}
```

```text
Lin (PRO)
seats=10
unsupported
```

<!-- /quick -->

The 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.

```java
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());
    }
}
```

```text
u-17: 2 roles
u-18: missing
source roles: 3
snapshot roles: 2
```

Both 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.

```java
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));
    }
}
```

```text
status=RUNNING
  detail=processed 3
status=DONE
  detail=complete
```

`yield` 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

> **Pitfall:** Reading “Java 17 features” as “five constructs first released in Java 17” makes release notes and minimum-version declarations inaccurate.

**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.

> **Pitfall:** AI output and newer tutorials often generate `switch (value) { case Order order -> ... }`, sometimes with a `when` guard, and label it stable Java 17 code.

**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.

> **Pitfall:** Record component fields are `final`, but a record does not make a referenced `List`, array, or domain object immutable.

**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.

> **Pitfall:** Adding a casual `default` to a switch over a closed enum lets a new enum constant compile and fall into a vague old branch.

**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.

> **Pitfall:** A text block improves layout but does not escape dynamic data for JSON, HTML, shell code, or SQL.

**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.

> **Pitfall:** Sealing an extension-facing plugin interface, or assuming all descendants remain known after a permitted branch becomes `non-sealed`, creates a false closed model.

**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.

<!-- deep -->

## 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 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.

<!-- /deep -->

[Checkpoint: java/java17-features](https://codewiki.com/java/java17-features/#checkpoint)

## Further reading

- [JEP 409: Sealed Classes](https://openjdk.org/jeps/409)
- [JEP 394: Pattern Matching for `instanceof`](https://openjdk.org/jeps/394)
- [JEP 395: Records](https://openjdk.org/jeps/395)
- [JEP 378: Text Blocks](https://openjdk.org/jeps/378)
- [JEP 361: Switch Expressions](https://openjdk.org/jeps/361)
