Records, sealed и pattern matching: современная Java без бойлерплейта
Java последних релизов изменилась сильнее, чем за предыдущее десятилетие. Records, sealed-классы и pattern matching — три фичи, которые вместе превращают язык из verbose-монстра в нечто похожее на Scala или Kotlin. Разберём, что каждая делает сама по себе и что даёт в комбинации.
Records: один класс в одну строку
Раньше класс-контейнер для пары полей выглядел так:
public 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) { /* 15 строк */ }
@Override
public int hashCode() { /* 5 строк */ }
@Override
public String toString() { /* 3 строки */ }
}
Теперь это:
public record Point(int x, int y) {}
Компилятор сам генерирует: финальные поля, канонический конструктор, accessor-методы x() и y() (именно так, без get), equals, hashCode, toString. Все — по канонам контракта.
Что важно знать про records:
Records — immutable. Поля финальные, изменить нельзя. Это сразу убирает целый класс багов и делает records потокобезопасными.
Компактный конструктор. Если надо валидировать аргументы, есть компактная форма — без параметров и присваиваний, компилятор сам подставит:
public record Age(int value) {
public Age {
if (value < 0 || value > 150) {
throw new IllegalArgumentException("age out of range: " + value);
}
}
}
Когда не record. Record — для прозрачных носителей данных (DTO, значение из БД, ответ API). Если объект имеет поведение, меняется со временем или его идентичность важнее содержимого — это обычный класс.
Sealed: закрытая иерархия
Обычный интерфейс или абстрактный класс в Java «открыт» — любой может добавить наследника. Sealed-тип закрывает иерархию явно:
public sealed interface Shape
permits Circle, Rectangle, Triangle {}
Теперь Shape могут реализовать только три перечисленных класса. Компилятор это знает, и это меняет следующее.
Pattern matching для switch
С Java 21 switch умеет работать по типам, и в связке с sealed-иерархией это даёт исчерпывающий (exhaustive) switch — если забыл вариант, компилятор ругается:
double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Rectangle r -> r.w() * r.h();
case Triangle t -> 0.5 * t.base() * t.height();
// default не нужен — компилятор видит, что все случаи покрыты
};
}
Это и есть алгебраический тип данных в стиле Scala — закрытое множество вариантов плюс обработка по типу. Раньше тот же эффект давал паттерн visitor, который занимал в три раза больше кода и читался тяжело.
Pattern matching для instanceof
Мелочь, но приятная. Раньше:
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
Теперь:
if (obj instanceof String s) {
System.out.println(s.length());
}
Приведение и связывание с переменной — в одном месте. Та же логика работает в switch:
switch (obj) {
case Integer i when i > 0 -> System.out.println("positive int: " + i);
case String s -> System.out.println("string of len " + s.length());
case null -> System.out.println("null");
default -> System.out.println("something else");
}
when добавляет guard-условие внутри кейса — раньше такого вообще не было.
Было/стало: обработка формы заявки
Сравните две реализации одной задачи — обработка поля формы, которое может быть текстом, числом или выбором из списка.
Было (visitor + abstract class):
abstract class FormField {
abstract <T> T accept(FieldVisitor<T> v);
}
class TextField extends FormField {
public <T> T accept(FieldVisitor<T> v) { return v.visitText(this); }
}
class NumberField extends FormField {
public <T> T accept(FieldVisitor<T> v) { return v.visitNumber(this); }
}
// + интерфейс FieldVisitor, + ещё классы, + отдельный visitor для валидации
String validate(FormField f) {
return f.accept(new FieldVisitor<>() {
public String visitText(TextField t) { return t.value().isBlank() ? "пусто" : "ок"; }
public String visitNumber(NumberField n) { return n.value() < 0 ? "отрицательно" : "ок"; }
});
}
Стало (sealed + record + pattern matching):
sealed interface FormField permits TextField, NumberField, ChoiceField {}
record TextField(String value) implements FormField {}
record NumberField(int value) implements FormField {}
record ChoiceField(String selected) implements FormField {}
String validate(FormField f) {
return switch (f) {
case TextField t when t.value().isBlank() -> "пусто";
case TextField t -> "ок";
case NumberField n when n.value() < 0 -> "отрицательно";
case NumberField n -> "ок";
case ChoiceField c when c.selected() == null -> "не выбрано";
case ChoiceField c -> "ок";
};
}
Меньше кода, понятнее, и компилятор проверит, что вы не забыли ни одного варианта. Если добавите четвёртый FormField — switch перестанет компилироваться, и это хорошо.
Чего пока нет
Java движется к полной деструктуризации (record patterns, JEP 440/441 уже умеют разбирать record на компоненты прямо в case), но в произвольных местах — в обычных присваиваниях — этого ещё нет. То есть нельзя написать var (x, y) = point();, как в Kotlin или JavaScript. Это следующий шаг языка.
Короткое summary
Records убивают бойлерплейт вокруг классов-значений. Sealed закрывает иерархию и даёт компилятору знание о полном множестве вариантов. Pattern matching для switch и instanceof позволяет писать код обработки в стиле «декларируй форму данных → обрабатывай по вариантам» вместо visitor-ов и каскадов if-instanceof-cast. Вместе три фичи переводят Java из verbose-лагеря ближе к современным языкам — и это не косметика, а смена того, как думают о моделировании предметной области.
Что почитать
- JEP 395: Records — первоисточник по records, с разбором ограничений.
- JEP 409: Sealed Classes — sealed-иерархии.
- JEP 441: Pattern Matching for switch — switch по типам, guards, exhaustiveness.