On this page
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:
| Release | Became final |
|---|---|
| 9 | List.of/Set.of/Map.of, the module system, JShell |
| 10 | var for local variables |
| 11 | new String methods, HttpClient, Files.readString, running java Main.java directly |
| 14 | switch expressions and arrow labels, helpful NullPointerException messages |
| 15 | text blocks |
| 16 | records, pattern matching for instanceof, Stream.toList |
| 17 | sealed 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:
var: letting the compiler write the type.- Switch expressions: no more forgotten
break. - Text blocks: multi-line strings without the mess.
- Pattern matching for
instanceof. NullPointerExceptionmessages 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;andvar 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 becomesArrayList<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
yieldto 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. Adefaultarm 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
\sto 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
| Mistake | What happens | Do this instead |
|---|---|---|
var list = new ArrayList<>(); | an ArrayList<Object> | new ArrayList<Order>() on the right |
var where the type isn't obvious | harder to read | write the type |
Colon-style case without break | falls through into the next case | arrow labels |
default in a switch over your own enum | new constants aren't flagged | cover every constant, no default |
return inside a switch-expression block | doesn't compile | yield |
Misplaced closing """ | unexpected indentation or newline | line it up with the text you want flush left |
A pattern variable after || | doesn't compile | use 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.
varis 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
""". instanceofpatterns work after&&, not||; NPE messages now name the null part.