FFlauron
Java Pro
Безкоштовний урок-прев'ю

Java 21: records, sealed, pattern matching, virtual threads, text blocks

Java 21 — LTS-релиз (2023), на 2026 — стандарт для нових проєктів. Це не той Java, який ти бачив у 2015: тепер є records, sealed-класи, pattern matching, virtual threads. Менше boilerplate, більше виразності.

Records — імутабельні data-класи

До Java 14 для простого DTO писали 50 рядків (getters, equals, hashCode, toString). Тепер — один рядок:

public record User(String name, String email, int age) {}

Компілятор згенерує:

  • name(), email(), age() — акцесори
  • equals / hashCode за всіма полями
  • toString()User[name=Olena, email=o@x.com, age=25]
  • Канонічний конструктор
var u = new User("Olena", "o@x.com", 25);
System.out.println(u.name());      // Olena
System.out.println(u);             // User[name=Olena, ...]

Можна додавати методи й валідацію:

public record Money(long amount, String currency) {
    public Money {
        if (amount < 0) throw new IllegalArgumentException("negative");
        if (currency.length() != 3) throw new IllegalArgumentException("ISO-4217");
    }

    public Money plus(Money other) {
        if (!currency.equals(other.currency)) throw new IllegalStateException();
        return new Money(amount + other.amount, currency);
    }
}

Use cases: DTO, value objects, парсинг JSON, результати методів (record Result(int status, String body)).

Sealed classes — закриті ієрархії

public sealed interface Shape permits Circle, Square, Triangle {}

public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}
public record Triangle(double base, double height) implements Shape {}

Компілятор знає всі можливі підтипи Shape. У switch не треба default — Java перевіряє exhaustiveness:

double area = switch (shape) {
    case Circle c   -> Math.PI * c.radius() * c.radius();
    case Square s   -> s.side() * s.side();
    case Triangle t -> 0.5 * t.base() * t.height();
};

Якщо хтось додасть Pentagon implements Shape — компіляція зламається в усіх switch без Pentagon. Це safety by design.

Pattern matching

// instanceof + cast одним рухом
if (obj instanceof String s && s.length() > 0) {
    System.out.println(s.toUpperCase());
}

// switch з деструктуризацією records
String describe(Shape s) {
    return switch (s) {
        case Circle(double r) when r > 10 -> "great circle r=" + r;
        case Circle(double r)             -> "circle r=" + r;
        case Square(double a)             -> "square " + a;
        case Triangle t                   -> "triangle";
    };
}

when — guard-clause. Деструктуризація розкладає record на компоненти.

Virtual threads (Project Loom)

До Java 21: 1 OS-потік = ~1 МБ стеку, до 5000 потоків на сервер. Тепер:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1_000_000; i++) {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return null;
        });
    }
}

Мільйон потоків на одному сервері. Virtual thread — не OS thread, а легка структура в JVM, що паркуєтьсья на події (I/O, sleep) і шедулиться на пул carrier-потоків.

Що це міняє: писати Thread.sleep, блокуючий I/O — і це ОК. Не потрібен reactive (Project Reactor) для масштабу. Прощавай CompletableFuture-кошмар.

Thread.startVirtualThread(() -> {
    var response = httpClient.send(request, BodyHandlers.ofString());
    process(response);
});

Text blocks

String json = """
    {
        "name": "Olena",
        "age": 25
    }
    """;

String sql = """
    SELECT u.id, u.name, COUNT(o.id) AS orders
    FROM users u
    LEFT JOIN orders o ON o.user_id = u.id
    GROUP BY u.id
    """;

Більше ніяких "line1\n" + "line2\n".

var — local type inference (Java 10+)

var users = new ArrayList<User>();     // ArrayList<User>
var map = new HashMap<String, List<User>>();
var stream = users.stream().filter(u -> u.age() > 18);

Тільки для локальних змінних. У публічному API завжди явний тип.

Switch expressions

int days = switch (month) {
    case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> 31;
    case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30;
    case FEBRUARY -> isLeapYear ? 29 : 28;
};

Без break, без падіння крізь cases, можна повертати значення.

Що ще нового (огляд)

  • HttpClient (Java 11) — нативний HTTP/2 клієнт
  • Files.readString / writeString (Java 11) — короткі read/write
  • String.repeat(n) (Java 11)
  • Stream.toList() (Java 16) — замість .collect(toList())
  • Math.clamp (Java 21)
  • Sequenced collections (Java 21) — getFirst(), getLast(), reversed()

Тест: перевір себе

  1. Що генерує компілятор для record User(String name, int age) {}?
  2. Навіщо sealed?
  3. Чому virtual threads — революція?
  4. Коли НЕ можна використати var?
  5. Що дає pattern matching у switch?
Відповіді
  1. Канонічний конструктор User(String, int), акцесори name() / age(), equals/hashCode за всіма полями, toString() у форматі User[name=..., age=...]. Record імплементить Record, не може мати mutable state.
  2. Exhaustive switch — компілятор знає всі підтипи, перевіряє повноту. Безпечна еволюція ієрархії: додав підтип — побачив усі місця, які треба оновити.
  3. Можна писати простий синхронний код (sleep, блокуючий I/O), а JVM сама шедулить мільйони таких «потоків» на жменьку OS-потоків. Не треба reactive, callback-hell, async/await — той самий API Thread.sleep, але дешево.
  4. У полях класу, параметрах методу, методах публічного API. var — лише для локальних змінних з ініціалізацією, де тип очевидний з правої частини.
  5. Деструктуризація: case Circle(double r) -> .... Перевірка типу + cast + витяг компонентів одним кроком. Можна додавати when-guards. Discriminated unions як у Rust/Kotlin/Scala.

Сподобався урок?

Придбайте повний курс, щоб отримати доступ до всіх уроків.

Перейти до курсу