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/writeString.repeat(n)(Java 11)Stream.toList()(Java 16) — замість.collect(toList())Math.clamp(Java 21)- Sequenced collections (Java 21) —
getFirst(),getLast(),reversed()
Тест: перевір себе
- Що генерує компілятор для
record User(String name, int age) {}? - Навіщо
sealed? - Чому virtual threads — революція?
- Коли НЕ можна використати
var? - Що дає pattern matching у
switch?
Відповіді
- Канонічний конструктор
User(String, int), акцесориname()/age(),equals/hashCodeза всіма полями,toString()у форматіUser[name=..., age=...]. Record імплементитьRecord, не може мати mutable state. - Exhaustive switch — компілятор знає всі підтипи, перевіряє повноту. Безпечна еволюція ієрархії: додав підтип — побачив усі місця, які треба оновити.
- Можна писати простий синхронний код (sleep, блокуючий I/O), а JVM сама шедулить мільйони таких «потоків» на жменьку OS-потоків. Не треба reactive, callback-hell, async/await — той самий API
Thread.sleep, але дешево. - У полях класу, параметрах методу, методах публічного API.
var— лише для локальних змінних з ініціалізацією, де тип очевидний з правої частини. - Деструктуризація:
case Circle(double r) -> .... Перевірка типу + cast + витяг компонентів одним кроком. Можна додаватиwhen-guards. Discriminated unions як у Rust/Kotlin/Scala.