Java 12#
Switch Expressions (Preview) JEP 325#
More on this in Switch Expression
Java 13#
Switch Expressions (Second Preview) JEP 354#
More on this in Switch Expression
Text Blocks (Preview) JEP 355#
More on this in Text Blocks
Java 14#
Switch Expression#
Below are the changes in the switch construct introduced in Java 14, formalized by JEP 361, JEP-354, and JEP-325. The new Switch Expression addresses three main issues with the traditional switch statement: verbosity, variable scope issues, and being limited to a statement.
1. Its unnecessarily verbose and error prone#
The traditional switch required break statements, leading to repetitive code and the potential for fall-through errors if a break was missed.
Problem Example (Traditional Switch Statement):
switch (day) { case MONDAY: case FRIDAY: case SUNDAY: System.out.println(6); break; case TUESDAY: System.out.println(7); break; case THURSDAY: case SATURDAY: System.out.println(8); break; case WEDNESDAY: System.out.println(9); break; }Solution (Switch Expression with
->):The new syntax uses
case L ->, which eliminates the need for an explicitbreakand allows multiple labels to be grouped.switch (weekDay) { case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); case TUESDAY -> System.out.println(7); case THURSDAY, SATURDAY -> System.out.println(8); case WEDNESDAY -> System.out.println(9); }
2. Scope of variable within Switch#
A variable declared in one case block in the traditional switch is scoped to the entire switch block, causing errors if the same variable name is used in another arm.
Problem Example (Variable Scope):
switch (day) { case MONDAY: case TUESDAY: int temp = ...; break; case WEDNESDAY: case THURSDAY: int temp = ...; // Can't call this variable 'temp' break; default: int temp = ...; // Can't call this variable 'temp' }Error: Variable ’temp’ is already defined in the scope
Workaround: The common workaround was declaring a variable outside the
switchand assigning values inside eachcase.int temp; switch (day) { case MONDAY: case FRIDAY: case SUNDAY: temp = 6; break; case TUESDAY: temp = 7; break; case THURSDAY: case SATURDAY: temp = 8; break; case WEDNESDAY: temp = 9; break; default: throw new IllegalStateException("Wat: " + day); }Solution: Using
yieldor Expression Form: The new switch can return a value, naturally handling scope.- Using
yield(with:): Theyieldkeyword is used to produce a value from theswitchexpression.
Integer temp = switch (weekDay) { case MONDAY, FRIDAY, SUNDAY: yield 6; case TUESDAY: yield 7; case THURSDAY, SATURDAY: yield 8; case WEDNESDAY: yield 9; };- Using
->(as expression): This concise syntax implicitly yields the result.
Integer temp = switch (weekDay) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> 9; };- Using
3. Limited to a statement not expression#
The traditional switch was limited to a statement and could not compute and return a single value directly.
As Statement (Original):
int numLetters; switch (day) { case MONDAY: case FRIDAY: case SUNDAY: numLetters = 6; System.out.println(6); break; case TUESDAY: numLetters = 7; System.out.println(6); break; case THURSDAY: case SATURDAY: numLetters = 8; System.out.println(6); break; case WEDNESDAY: numLetters = 9; System.out.println(6); break; default: throw new IllegalStateException("Wat: " + day); }As Expression (New Feature): The
switchcan now be used as an expression, where its result can be directly used in a statement likeSystem.out.println().System.out.println( switch (weekDay) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> 9; } );
Helpful NullPointerExceptions#
JEP 358 changed what a NullPointerException tells you. Before Java 14 you got a line number and nothing else. If that line had a chain of calls on it, you had to work out yourself which one returned null.
Problem Example (Which one is null?):
String city = order.getCustomer().getAddress().getCity().toUpperCase();Exception in thread "main" java.lang.NullPointerException at Main.main(Main.java:12)order,getCustomer(),getAddress()orgetCity()could each be the null one. The usual fix was to split the line into four and run it again, or attach a debugger.Solution (The JVM names the null):
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Address.getCity()" because the return value of "Customer.getAddress()" is null at Main.main(Main.java:12)In Java 14 this was off by default and needed
-XX:+ShowCodeDetailsInExceptionMessages. From Java 15 it’s on by default.
Text Blocks (Second Preview) JEP 368#
More on this in Text Blocks
Records (Preview) JEP 359#
More on this in Records
Pattern Matching for instanceof (Preview) JEP 305#
More on this in Pattern Matching for instanceof
Java 15#
Text Blocks#
Text blocks became final in Java 15 with JEP 378, after two previews (JEP 355 and JEP 368). A text block is a multi-line string literal. It opens with """ followed by a line break and closes with another """. It fixes three problems with putting multi-line text in a normal string.
1. Escapes everywhere#
JSON, SQL and HTML are full of quotes and line breaks. In a normal string every quote needs \" and every line needs \n, so what you read in the code looks nothing like the string you get at runtime.
Problem Example (JSON in a String):
String json = "{\n" + " \"name\": \"Amar\",\n" + " \"role\": \"engineer\"\n" + "}\n";Solution (Text Block):
String json = """ { "name": "Amar", "role": "engineer" } """;Quotes don’t need escaping, and line breaks stay where you typed them. Both snippets produce the same string.
2. How much of the indentation is part of the string?#
The text block above is indented to line up with the code around it. Those leading spaces are not in the result. The compiler finds the line with the least indentation (the closing """ counts as a line) and strips that much from every line. JEP 378 calls this incidental whitespace.
Keep some indentation: Move the closing
"""to the left. Everything to the right of it stays in the string.String html = """ <p>Hello</p> """; // " <p>Hello</p>\n" -> 4 spaces keptNo trailing newline: Put the closing
"""at the end of the last line instead of on its own line.String greeting = """ Hello"""; // "Hello"
3. Long lines you want to wrap in code but not in the string#
A long SQL query reads better split over several lines, but sometimes the string itself has to stay on one line.
Problem Example (Concatenation):
String query = "SELECT id, name, email " + "FROM users " + "WHERE active = true";Solution (
\at the end of a line): A backslash at the end of a line removes the line break.String query = """ SELECT id, name, email \ FROM users \ WHERE active = true\ """; // "SELECT id, name, email FROM users WHERE active = true"\sfor trailing spaces: The compiler also strips spaces at the end of each line. If you need them,\sputs in one space and stops the stripping on that line.String colors = """ red \s green\s """; // "red \ngreen \n"
Sealed Classes (Preview) JEP 360#
More on this in Sealed Classes
Records (Second Preview) JEP 384#
More on this in Records
Pattern Matching for instanceof (Second Preview) JEP 375#
More on this in Pattern Matching for instanceof
Java 16#
Pattern Matching for instanceof#
JEP 394 made this final in Java 16, after previews in 14 (JEP 305) and 15 (JEP 375). It fixes two things about instanceof.
1. Check, cast, then use#
Problem Example (Test and cast):
if (obj instanceof String) { String s = (String) obj; System.out.println(s.length()); }The
ifalready provedobjis aString. You still had to cast it and declare a new variable, andStringshows up three times in two lines.Solution (Type pattern):
if (obj instanceof String s) { System.out.println(s.length()); }sis a pattern variable. It only gets a value when the test passes, so you can’t use it by mistake whenobjis something else.
2. Scope of the pattern variable#
A pattern variable is in scope wherever the compiler can prove the match succeeded. That covers &&, and code after an early return.
Problem Example (
equals()):@Override public boolean equals(Object o) { if (!(o instanceof Point)) { return false; } Point other = (Point) o; return x == other.x && y == other.y; }Solution (Pattern with
&&):@Override public boolean equals(Object o) { return o instanceof Point other && x == other.x && y == other.y; }Early return: When the
ifhandles the failed match and returns, the variable is usable after it.if (!(obj instanceof String s)) { return; } System.out.println(s.length()); // s is in scope hereWith
||it doesn’t compile: On the right of||the match may have failed, sosisn’t defined there.if (obj instanceof String s || s.isEmpty()) { }Error: cannot find symbol ’s'
Records#
JEP 395 made records final in Java 16, after previews in 14 (JEP 359) and 15 (JEP 384). A record is a class that just carries data. You declare the fields and the compiler writes the rest.
1. Too much code for a data class#
Problem Example (Plain class):
public final class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Point)) return false; Point other = (Point) o; return x == other.x && y == other.y; } @Override public int hashCode() { return Objects.hash(x, y); } @Override public String toString() { return "Point[x=" + x + ", y=" + y + "]"; } }That’s around 30 lines for two
ints. Add a field later and you have to remember to update the constructor,equals,hashCodeandtoString.Solution (Record):
public record Point(int x, int y) { }From this one line the compiler generates a
private finalfield and an accessor for each component, a constructor that takes all of them, andequals,hashCodeandtoStringthat use all of them.Point p = new Point(1, 2); p.x(); // 1 (x(), not getX()) System.out.println(p); // Point[x=1, y=2] new Point(1, 2).equals(new Point(1, 2)); // true
2. Validation in the constructor#
Problem Example (Full constructor): To check the arguments in a normal class you write the whole constructor and repeat every assignment.
public Range(int start, int end) { if (start > end) { throw new IllegalArgumentException(start + " > " + end); } this.start = start; this.end = end; }Solution (Compact constructor): Leave out the parameter list and the assignments. The compiler assigns the fields after your code runs.
public record Range(int start, int end) { public Range { if (start > end) { throw new IllegalArgumentException(start + " > " + end); } } }You can also reassign a parameter inside it, for example
name = name.trim();, and the field gets the new value.
3. What a record can’t do#
- A record is
final. It can’t extend another class (it already extendsjava.lang.Record) and nothing can extend it. - Fields are
final, so there are no setters. - You can’t add instance fields beyond the components.
staticfields are fine. - It can implement interfaces and have its own methods, static factory methods and nested types.
- It’s only shallowly immutable. A
Listcomponent can still be changed by whoever holds the list. Copy it withList.copyOf()in the compact constructor if that matters.
Stream.toList()#
A small one without a JEP. Java 16 added toList() directly on Stream.
Before:
List<String> names = users.stream() .map(User::name) .collect(Collectors.toList());After:
List<String> names = users.stream() .map(User::name) .toList();
They don’t return the same kind of list. toList() returns an unmodifiable list, so names.add(...) throws UnsupportedOperationException. Collectors.toList() gives you an ArrayList in practice, though the spec doesn’t promise any type. If later code adds to the list, swapping one for the other will break it at runtime.
Sealed Classes (Second Preview) JEP 397#
More on this in Sealed Classes
Java 17#
Sealed Classes#
JEP 409 made sealed classes final in Java 17, after previews in 15 (JEP 360) and 16 (JEP 397). A sealed class or interface lists exactly which classes are allowed to extend it.
1. Inheritance was all or nothing#
Before Java 17 a class was either final, and nobody could extend it, or open to everyone. A package-private constructor kept subclasses inside one package, but that trick doesn’t work for interfaces, and nothing in the code told the reader or the compiler what the full list of subclasses was.
Problem Example (Open hierarchy):
public abstract class Shape { } public class Circle extends Shape { } public class Square extends Shape { } // Nothing stops someone adding this in another package: public class Triangle extends Shape { }Code that handles every
Shapecan’t know the full list, so it always ends with anelsethat throws.Solution (
sealedandpermits):public sealed abstract class Shape permits Circle, Square { } public final class Circle extends Shape { } public final class Square extends Shape { } public class Triangle extends Shape { }Error: class is not allowed to extend sealed class: Shape (as it is not listed in its ‘permits’ clause)
2. Every permitted subclass has to pick a modifier#
Each class in permits must say how the hierarchy continues below it:
final: no more subclasses.sealed: subclasses again, but only the ones it lists.non-sealed: open again, anyone can extend it.
public sealed abstract class Shape permits Circle, Rectangle, Polygon { }
public final class Circle extends Shape { }
public sealed class Rectangle extends Shape permits Square { }
public final class Square extends Rectangle { }
public non-sealed class Polygon extends Shape { }
public class Hexagon extends Polygon { } // allowed, Polygon is non-sealed
The permitted classes have to be in the same module as the sealed class, or in the same package if you’re not using modules. If they’re all in the same file you can leave out permits and the compiler works it out.
3. Sealed interfaces and records#
Records are implicitly final, so they work as permitted subclasses with no extra modifier.
public sealed interface Shape permits Circle, Square { }
public record Circle(double radius) implements Shape { }
public record Square(double side) implements Shape { }
With pattern matching for instanceof from Java 16, handling each case looks like this:
static double area(Shape shape) {
if (shape instanceof Circle c) {
return Math.PI * c.radius() * c.radius();
} else if (shape instanceof Square s) {
return s.side() * s.side();
}
throw new IllegalStateException("Unknown shape: " + shape);
}
The last throw is still needed in Java 17, because the compiler doesn’t check whether an if/else chain covers every permitted subclass. Pattern matching for switch is the feature that lets the compiler do that check and drop the throw. It’s a preview in Java 17 and final in Java 21, so it belongs in the 17 to 21 post.
Pattern Matching for switch (Preview) JEP 406#
Final in Java 21 (JEP 441).
Some important links#
| JEP | Link |
|---|---|
| JEP-354 | https://openjdk.org/jeps/354 |
| JEP-325 | https://openjdk.org/jeps/325 |
| JEP 361 | https://openjdk.org/jeps/361 |
| JEP 355 | https://openjdk.org/jeps/355 |
| JEP 368 | https://openjdk.org/jeps/368 |
| JEP 378 | https://openjdk.org/jeps/378 |
| JEP 358 | https://openjdk.org/jeps/358 |
| JEP 305 | https://openjdk.org/jeps/305 |
| JEP 375 | https://openjdk.org/jeps/375 |
| JEP 394 | https://openjdk.org/jeps/394 |
| JEP 359 | https://openjdk.org/jeps/359 |
| JEP 384 | https://openjdk.org/jeps/384 |
| JEP 395 | https://openjdk.org/jeps/395 |
| JEP 360 | https://openjdk.org/jeps/360 |
| JEP 397 | https://openjdk.org/jeps/397 |
| JEP 409 | https://openjdk.org/jeps/409 |
| JEP 406 | https://openjdk.org/jeps/406 |
