↓ Skip to main content

Things changed from Java 11 - 17

·2595 words·13 mins
Table of Contents

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 explicit break and 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 switch and assigning values inside each case.

    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 yield or Expression Form: The new switch can return a value, naturally handling scope.

    • Using yield (with :): The yield keyword is used to produce a value from the switch expression.
    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;
            };
    

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 switch can now be used as an expression, where its result can be directly used in a statement like System.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() or getCity() 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 kept
    
  • No 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"
    
  • \s for trailing spaces: The compiler also strips spaces at the end of each line. If you need them, \s puts 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 if already proved obj is a String. You still had to cast it and declare a new variable, and String shows up three times in two lines.

  • Solution (Type pattern):

    if (obj instanceof String s) {
        System.out.println(s.length());
    }
    

    s is a pattern variable. It only gets a value when the test passes, so you can’t use it by mistake when obj is 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 if handles 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 here
    
  • With || it doesn’t compile: On the right of || the match may have failed, so s isn’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, hashCode and toString.

  • Solution (Record):

    public record Point(int x, int y) { }
    

    From this one line the compiler generates a private final field and an accessor for each component, a constructor that takes all of them, and equals, hashCode and toString that 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 extends java.lang.Record) and nothing can extend it.
  • Fields are final, so there are no setters.
  • You can’t add instance fields beyond the components. static fields are fine.
  • It can implement interfaces and have its own methods, static factory methods and nested types.
  • It’s only shallowly immutable. A List component can still be changed by whoever holds the list. Copy it with List.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 Shape can’t know the full list, so it always ends with an else that throws.

  • Solution (sealed and permits):

    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#

JEPLink
JEP-354https://openjdk.org/jeps/354
JEP-325https://openjdk.org/jeps/325
JEP 361https://openjdk.org/jeps/361
JEP 355https://openjdk.org/jeps/355
JEP 368https://openjdk.org/jeps/368
JEP 378https://openjdk.org/jeps/378
JEP 358https://openjdk.org/jeps/358
JEP 305https://openjdk.org/jeps/305
JEP 375https://openjdk.org/jeps/375
JEP 394https://openjdk.org/jeps/394
JEP 359https://openjdk.org/jeps/359
JEP 384https://openjdk.org/jeps/384
JEP 395https://openjdk.org/jeps/395
JEP 360https://openjdk.org/jeps/360
JEP 397https://openjdk.org/jeps/397
JEP 409https://openjdk.org/jeps/409
JEP 406https://openjdk.org/jeps/406
Amar Singh
Author
Amar Singh
I build stuff

Related