Skip to content

Java 17 (September 2021)

Java 17 Status

Release Information

Release Date: September 14, 2021 End of Support: September 2029 Theme: Type Safety & Expressiveness

Overview

Java 17 represents a major leap in type safety and code expressiveness. It finalizes several preview features that make Java code more concise while maintaining type safety. This is currently the most popular LTS version for new projects.


Records

Interview Favorite

Know that records are immutable, have auto-generated methods, and when to use them vs classes.

The Problem

Simple data classes required massive amounts of boilerplate code.

Before/After

// 50+ lines for a simple data class
public final class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String name() { return name; }
    public int age() { return age; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "Person[name=" + name + ", age=" + age + "]";
    }
}
// One line!
public record Person(String name, int age) { }

// Usage is identical
var person = new Person("Alice", 30);
System.out.println(person.name());    // "Alice"
System.out.println(person.age());     // 30
System.out.println(person);           // Person[name=Alice, age=30]

// Records can have:
public record Person(String name, int age) {
    // Compact constructor for validation
    public Person {
        if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
        name = name.trim(); // Can modify before assignment
    }

    // Additional methods
    public String greeting() {
        return "Hello, " + name;
    }

    // Static fields and methods
    public static Person unknown() {
        return new Person("Unknown", 0);
    }
}

Record Characteristics

Feature Description
Immutable All fields are final
Transparent State is fully described by components
Auto-generated constructor, accessors, equals(), hashCode(), toString()
Cannot extend Implicitly final, cannot extend other classes
Can implement Can implement interfaces

Sealed Classes

The Problem

No way to restrict which classes could extend a class/interface.

Before/After

// Anyone can extend Shape
public abstract class Shape {
    public abstract double area();
}

// In another package, someone creates...
public class MysteryShape extends Shape {
    public double area() { return -1; } // Unexpected!
}

// You can't write exhaustive switch
// because you don't know all subclasses
// Only permitted classes can extend
public sealed abstract class Shape
    permits Circle, Rectangle, Triangle {

    public abstract double area();
}

public final class Circle extends Shape {
    private final double radius;

    public Circle(double radius) { this.radius = radius; }

    @Override
    public double area() { return Math.PI * radius * radius; }
}

public final class Rectangle extends Shape {
    private final double width, height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() { return width * height; }
}

public non-sealed class Triangle extends Shape {
    // non-sealed allows further extension
    @Override
    public double area() { /* ... */ }
}

Sealed Class Modifiers

Modifier Meaning
sealed Only permitted subclasses can extend
final Cannot be extended further
non-sealed Opens up for extension (escapes sealing)

Combined with Pattern Matching

// Compiler knows all cases - exhaustive!
double calculateArea(Shape shape) {
    return switch (shape) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
        case Triangle t -> t.base() * t.height() / 2;
        // No default needed - compiler verifies exhaustiveness
    };
}

Pattern Matching for instanceof

Interview Favorite

This eliminates the cast after instanceof check.

The Problem

After instanceof check, explicit cast was still required.

Before/After

// Redundant cast after instanceof
if (obj instanceof String) {
    String s = (String) obj;  // Redundant!
    System.out.println(s.toUpperCase());
}

// Nested checks even worse
if (obj instanceof List) {
    List<?> list = (List<?>) obj;
    if (!list.isEmpty()) {
        Object first = list.get(0);
        if (first instanceof String) {
            String s = (String) first;
            // Finally use s...
        }
    }
}
// Pattern variable in same expression
if (obj instanceof String s) {
    System.out.println(s.toUpperCase());  // s already cast
}

// With conditions
if (obj instanceof String s && s.length() > 5) {
    System.out.println(s.toUpperCase());
}

// In complex conditions
if (obj instanceof List<?> list && !list.isEmpty()
        && list.get(0) instanceof String s) {
    System.out.println(s.toUpperCase());
}

// Negation pattern
if (!(obj instanceof String s)) {
    return; // s not in scope here
}
// s is in scope here!
System.out.println(s.toUpperCase());

Scope Rules

// Variable scope follows flow analysis
if (obj instanceof String s) {
    // s in scope
} else {
    // s NOT in scope
}

// Works with && but not ||
if (obj instanceof String s && s.length() > 0) { } // OK
// if (obj instanceof String s || s.length() > 0) { } // ERROR

Text Blocks

The Problem

Multi-line strings were painful with concatenation and escape characters.

Before/After

// JSON - nightmare of escapes and concatenation
String json = "{\n" +
    "  \"name\": \"Alice\",\n" +
    "  \"age\": 30,\n" +
    "  \"email\": \"alice@example.com\"\n" +
    "}";

// HTML - even worse
String html = "<html>\n" +
    "  <body>\n" +
    "    <h1>Hello World</h1>\n" +
    "  </body>\n" +
    "</html>";

// SQL - hard to read and maintain
String sql = "SELECT u.id, u.name, o.total " +
    "FROM users u " +
    "JOIN orders o ON u.id = o.user_id " +
    "WHERE o.status = 'COMPLETED' " +
    "ORDER BY o.total DESC";
// JSON - clean and readable
String json = """
    {
      "name": "Alice",
      "age": 30,
      "email": "alice@example.com"
    }
    """;

// HTML - exactly as you'd write it
String html = """
    <html>
      <body>
        <h1>Hello World</h1>
      </body>
    </html>
    """;

// SQL - readable queries
String sql = """
    SELECT u.id, u.name, o.total
    FROM users u
    JOIN orders o ON u.id = o.user_id
    WHERE o.status = 'COMPLETED'
    ORDER BY o.total DESC
    """;

// With string formatting
String greeting = """
    Hello %s,
    Welcome to %s!
    """.formatted(name, appName);

Text Block Features

Feature Example
Incidental whitespace Leading whitespace is trimmed based on closing """ position
Line terminators Always normalized to \n
Escape sequences \", \\, \n still work
New escapes \s (space), \ at end of line (no newline)
// Control trailing newline
String withNewline = """
    text
    """;    // Ends with newline

String noNewline = """
    text"""; // No trailing newline

// Escape end of line to avoid newline
String singleLine = """
    This is a \
    single line""";
// Result: "This is a single line"

Switch Expressions

Finalized in Java 14

Preview in Java 12-13, standard in Java 14+.

The Problem

Switch statements were procedural, error-prone (fall-through), and couldn't return values.

Before/After

// Must use variable and break statements
int numLetters;
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        numLetters = 6;
        break;         // Easy to forget!
    case TUESDAY:
        numLetters = 7;
        break;
    case THURSDAY:
    case SATURDAY:
        numLetters = 8;
        break;
    case WEDNESDAY:
        numLetters = 9;
        break;
    default:
        throw new IllegalArgumentException();
}
// Expression that returns a value
int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY -> 7;
    case THURSDAY, SATURDAY -> 8;
    case WEDNESDAY -> 9;
};

// With blocks when logic needed
String description = switch (day) {
    case MONDAY -> {
        logger.info("Week starting");
        yield "Start of work week";  // yield for blocks
    }
    case FRIDAY -> "End of work week";
    case SATURDAY, SUNDAY -> "Weekend!";
    default -> "Midweek";
};

// Can still use as statement
switch (command) {
    case "start" -> startServer();
    case "stop" -> stopServer();
    case "restart" -> {
        stopServer();
        startServer();
    }
    default -> System.out.println("Unknown command");
}

Switch Expression Rules

Aspect Arrow Form (->) Colon Form (:)
Fall-through No Yes (traditional)
Multiple labels case A, B -> case A: case B:
Return value Expression or yield Must use yield
Block -> { yield value; } { yield value; }

Helpful NullPointerException Messages

Before/After

// Code
user.getAddress().getCity().toUpperCase();

// Exception
Exception in thread "main" java.lang.NullPointerException
    at App.main(App.java:15)

// Which one was null? No idea!
// Same code
user.getAddress().getCity().toUpperCase();

// Exception - tells you exactly what was null
Exception in thread "main" java.lang.NullPointerException:
    Cannot invoke "Address.getCity()" because the return value
    of "User.getAddress()" is null
    at App.main(App.java:15)

Quick Reference

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

// Sealed class
public sealed class Shape permits Circle, Square { }

// Pattern matching instanceof
if (obj instanceof String s && s.length() > 0) {
    System.out.println(s.toUpperCase());
}

// Text block
String json = """
    {"name": "value"}
    """;

// Switch expression
int result = switch (x) {
    case 1, 2 -> 10;
    case 3 -> 20;
    default -> 0;
};