Java · Java 17 Features (9 to 17)

Java 17: var, Switch Expressions, Text Blocks and instanceof Patterns

The release train and LTS versions, then the syntax that arrived between Java 8 and 17: var, arrow labels and exhaustive switch expressions, text blocks, pattern matching for instanceof and helpful NullPointerException messages.

  • 6 min read
  • 13 practice questions
  • 1 graded problem
On this page
  1. Description
  2. var
  3. Switch expressions
  4. Text blocks
  5. Pattern matching for instanceof
  6. Helpful NullPointerException messages
  7. Common mistakes
  8. Check yourself
  9. Key takeaways
  10. Quick revise
  11. Practice

Description

Since Java 9 (2017), a new Java version comes out every six months, and every few years one of them is a long-term-support (LTS) release that vendors patch for many years: 11 (2018), 17 (2021), 21 (2023) and 25 (2025). Most production code runs on an LTS, so "what's new since Java 8?" really means "what's in 11 and 17?".

New language features usually arrive first as a preview (they compile only with a special flag) and become final a release or two later. This module dates each feature by the release where it became final:

ReleaseBecame final
9List.of/Set.of/Map.of, the module system, JShell
10var for local variables
11new String methods, HttpClient, Files.readString, running java Main.java directly
14switch expressions and arrow labels, helpful NullPointerException messages
15text blocks
16records, pattern matching for instanceof, Stream.toList
17sealed classes, strong encapsulation of JDK internals

This skill covers the syntax that makes everyday code shorter and safer: var, switch expressions, text blocks, instanceof patterns, and better null-pointer messages. Each removes boilerplate, and each has one rule worth knowing. The next two skills cover records and sealed classes, then the new APIs and the platform.

Here's the path, from easiest to most technical:

  1. var: letting the compiler write the type.
  2. Switch expressions: no more forgotten break.
  3. Text blocks: multi-line strings without the mess.
  4. Pattern matching for instanceof.
  5. NullPointerException messages that say what was null.

var

var (Java 10) lets the compiler work out a local variable's type from the value you assign:

var orders = new ArrayList<Order>();                  // ArrayList<Order>
var byCity = new HashMap<String, List<Order>>();      // HashMap<String, List<Order>>
for (var entry : byCity.entrySet()) { ... }

The variable still has one fixed type: var n = 1; n = "one"; doesn't compile. A few rules:

  • Local variables only (including loop variables and try-with-resources). Not fields, parameters or return types.
  • It needs an initializer with a clear type. var x;, var x = null; and var f = () -> 1; don't compile.
  • Don't combine it with a bare diamond. var list = new ArrayList<>(); has nothing to infer the element type from, so it becomes ArrayList<Object>, and anything can be added to it. Put the type on the right: new ArrayList<Order>().
  • Use it when the type is obvious from the right-hand side. var result = service.process(x); makes the reader guess.

Switch expressions

The old switch has a famous trap. This computes the days in a month:

switch (month) {
    case 2: days = 28;
    case 4: days = 30;
    default: days = 31;
}

For February it gives 31. Without a break, each case falls through into the next one, so February runs all three assignments. Java 14 added arrow labels and switch expressions, which fix this:

int days = switch (month) {
    case 2 -> 28;
    case 4, 6, 9, 11 -> 30;      // several labels on one arm
    default -> 31;
};
  • Arrow labels never fall through. Only the matching arm runs, and there's no break. Arrows work in ordinary switch statements too.
  • A switch expression produces a value, so you can assign or return it directly.
  • For a block arm, use yield to produce the value:
String label = switch (size) {
    case S -> "small";
    case M, L -> "regular";
    case XL -> {
        log.debug("large order");
        yield "large";
    }
};
  • A switch expression must cover every case. For an enum, list every constant or add default; missing one is a compile error. That's a feature: when someone adds a new constant to the enum, the compiler points at every switch that needs updating. A default arm gives up that check, so leave it out for enums you control.

Text blocks

A text block (Java 15) is a multi-line string literal, written between triple quotes:

String json = """
    {
      "city": "Austin"
    }
    """;
  • The opening """ must end its line; the text starts on the next line.
  • Indentation is trimmed for you. The compiler finds the least-indented line (the closing """ counts) and removes that much from every line. Here, { ends up at the start of the line, and "city" keeps its two extra spaces.
  • With the closing """ on its own line, the string ends with a newline. Put the quotes right after the last character (...}""") if you don't want one.
  • Trailing spaces on each line are removed; write \s to keep one. A \ at the end of a line joins it with the next.
  • Fill in values with formatted: "Hello %s".formatted(name).

Pattern matching for instanceof

Checking a type and then casting used to take two steps:

if (shape instanceof Circle) {
    Circle c = (Circle) shape;
    ...
}

Since Java 16, the check can declare the variable directly:

if (shape instanceof Circle c && c.radius() > 10) {
    return Math.PI * c.radius() * c.radius();
}

The variable c exists wherever the compiler knows the check succeeded: inside the if, after &&, and after an if that exits on failure (if (!(o instanceof String s)) return; and then s is usable). It does not exist after ||, because the right side of || only runs when the check failed: o instanceof String s || s.isEmpty() doesn't compile.

Helpful NullPointerException messages

Before Java 14, a NullPointerException on a line like order.customer().name() didn't tell you which part was null. Now the message says:

Cannot invoke "Customer.name()" because the return value of "Order.customer()" is null

It's on by default since Java 15, and it saves a lot of guessing when reading production logs.

Common mistakes

MistakeWhat happensDo this instead
var list = new ArrayList<>();an ArrayList<Object>new ArrayList<Order>() on the right
var where the type isn't obviousharder to readwrite the type
Colon-style case without breakfalls through into the next casearrow labels
default in a switch over your own enumnew constants aren't flaggedcover every constant, no default
return inside a switch-expression blockdoesn't compileyield
Misplaced closing """unexpected indentation or newlineline it up with the text you want flush left
A pattern variable after ||doesn't compileuse it after &&, or invert the test

Check yourself

Try to answer before opening each one. Recalling beats re-reading.

1. What type is list in var list = new ArrayList<>();?

Show answer

ArrayList<Object>: with an empty diamond there's nothing to infer the element type from. Write new ArrayList<Order>() on the right.

2. An old-style switch sets days for February with case 2: days = 28; and no break, followed by other cases. Why is the result wrong?

Show answer

Without break, execution falls through into the following cases, which overwrite days. Arrow labels (case 2 -> 28;) never fall through.

3. A switch expression over an enum has no default and misses one constant. What happens?

Show answer

It doesn't compile, because a switch expression must cover every value. That's useful: adding a constant to the enum makes every incomplete switch a compile error.

4. Why doesn't o instanceof String s || s.isEmpty() compile?

Show answer

s only exists where the check is known to have succeeded. The right side of || runs only when the check failed, so s isn't available there.

Key takeaways

  • LTS releases (11, 17, 21, 25) are what most code runs on; a feature "arrived in N" when it became final in N.
  • var is for local variables with an obvious type; never with an empty diamond.
  • Arrow labels don't fall through; switch expressions must cover every case, so let the compiler check your enums.
  • Text blocks trim indentation relative to the closing """.
  • instanceof patterns work after &&, not ||; NPE messages now name the null part.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

When things became final

ReleaseFeature
9List.of/Set.of/Map.of, modules, JShell
10var
11 (LTS)String.isBlank/strip/lines/repeat, HttpClient, Files.readString, java Main.java
14switch expressions, arrow labels, helpful NPEs
15text blocks
16records, instanceof patterns, Stream.toList
17 (LTS)sealed classes, strong encapsulation

Syntax at a glance

var orders = new ArrayList<Order>();              // not new ArrayList<>()

int days = switch (month) {                       // arrows: no fall-through
    case 2 -> 28;
    case 4, 6, 9, 11 -> 30;
    default -> 31;
};
String size = switch (s) { case XL -> { log(); yield "large"; } default -> "regular"; };

String json = """
    {"city": "Austin"}
    """;                                          // indentation trimmed to the closing """

if (shape instanceof Circle c && c.radius() > 10) { ... }   // c bound after &&, not ||

The traps

  • var + empty diamond = Object elements; var x = null doesn't compile.
  • Colon-style case without break falls through.
  • default in a switch over your own enum hides new constants.
  • return instead of yield in a switch-expression block.
  • The closing """ decides the indentation and the trailing newline.
  • Pattern variables aren't in scope after ||.

Staff-level points, one line each

  • Preview: --enable-preview --release N; preview class files (minor 0xFFFF) only run on that exact release; syntax can change (break value became yield).
  • --release N compiles against release N's API; -source/-target don't, so new methods slip through.
  • String switch = hashCode() + equals() (so null throws); enum switch = an ordinal lookup array in a synthetic class.
  • Text blocks: normalize line endings → strip indentation and trailing spaces → process escapes (\s survives). stripIndent(), translateEscapes().
  • var can hold non-denotable types: var c = new Object() { int hits; }; c.hits++.
  • Helpful NPE messages are computed from bytecode on demand; local names need javac -g (else a slot like <local4>); off with -XX:-ShowCodeDetailsInExceptionMessages.
  • Upgrade blockers: strong encapsulation (InaccessibleObjectException, --add-opens, jdeps --jdk-internals), removed Java EE/CORBA/Nashorn, G1 by default, removed flags.

Say it in an interview

  • "Switch expressions must be exhaustive, so for our own enums I leave out default and let the compiler flag new constants."
  • "I'd build with --release 17 so nothing newer than 17's API can sneak in."
  • "Most of our 8-to-17 work was upgrading libraries that reflected into JDK internals."

Practice

This skill comes with 13 multiple-choice questions that tell you which misconception a wrong answer reveals and 1 graded problem. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Java 17: var, Switch Expressions, Text Blocks and instanceof Patterns free

Shipping Rates and Labels

EasyFreeCoding problemJava

A fulfillment center in Columbus prices each parcel by destination zone and speed, then prints a label. Write Shipping.cost and Shipping.label. Switch expressions and a text block fit these perfectly.

Practice Java 17: var, Switch Expressions, Text Blocks and instanceof Patterns free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime