Records, sealed и pattern matching: современная Java без бойлерплейта

Языки3 мин чтения
  • #java
  • #records
  • #sealed
  • #pattern-matching
  • #modern-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-лагеря ближе к современным языкам — и это не косметика, а смена того, как думают о моделировании предметной области.

Что почитать