Перейти к основному содержимому

Современные конструкции Java

Разработчику Архитектору

Современные конструкции Java

Язык после Java 8 получил конструкции, которые сокращают шаблонный код и делают модели данных и иерархии типов явными. Ниже — обзор с Java 14 по 21, без привязки к конкретному LTS: проверяйте версию JDK в структуре проектов.

Связанные темы: ООП, ключевые слова, Stream API.


Критерий выбора синтаксиса

Новые конструкции Java 14–21 сокращают шаблонный код и делают модель явной. Берите их, если код становится короче и читается яснее; если конструкция прячет смысл — оставляйте классический стиль.


Локальный вывод типа var (Java 10+)

var list = new ArrayList<String>(); // ArrayList<String>
var path = Path.of("config.yml"); // Path

Разбор:

  • var включает вывод типа компилятором для локальной переменной и убирает повторение длинного типа слева.
  • new ArrayList<String>() однозначно задаёт тип ArrayList<String>, поэтому итоговый тип list читается без двусмысленности.
  • Path.of("config.yml") даёт переменную типа Path, что удобно для файловых операций API java.nio.file.
  • На уровне байткода никакой "динамической типизации" здесь нет: тип остаётся статическим и проверяется компилятором.

var — только для локальных переменных с инициализатором. Тип выводится компилятором, в байт-коде — обыный явный тип.

Не используйте var, если тип неочевиден из правой части (var x = service.find() — плохо).


record (Java 16+)

Неизменяемый носитель данных с автоматическими equals, hashCode, toString, геттерами:

public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) throw new IllegalArgumentException();
}
}

Разбор:

  • record Point(int x, int y) объявляет неизменяемый value-тип с автоматически сгенерированными x(), y(), equals, hashCode, toString.
  • Компактный конструктор public Point { ... } позволяет валидировать входные значения до завершения создания объекта.
  • Проверка if (x < 0 || y < 0) задаёт доменное ограничение "координаты неотрицательные".
  • throw new IllegalArgumentException() прерывает создание некорректного экземпляра и защищает инварианты модели.
recordКласс с полями
Мало кода, семантика "значение"Гибкость, изменяемость, наследование
Нельзя наследовать классыОбычная иерархия
Подходит для DTO, команд, событийСущности с поведением

Record может реализовывать интерфейсы и содержать статические члены; компактный конструктор валидирует поля.

В Spring / JPA records часто используют как DTO; сущности БД по-прежнему часто — классы с прокси Hibernate.

Типичный пример для API:

public record UserResponse(long id, String email, String role) {}

Разбор:

  • Этот record моделирует DTO ответа API в одну строку с фиксированным набором полей.
  • long id, String email, String role становятся компонентами и доступны через аксессоры id(), email(), role().
  • Пустое тело {} означает, что дефолтной генерации методов достаточно и дополнительная логика не нужна.
  • Формат отлично подходит для границ API, где ценится компактность и предсказуемая сериализация.

Для такого типа record подходит идеально: минимум шаблонного кода и предсказуемое поведение equals/hashCode.


sealed-классы и интерфейсы (Java 17+)

Ограничивают, кто может наследовать или реализовать тип:

public sealed interface Shape permits Circle, Rectangle { }

public record Circle(double radius) implements Shape { }

public record Rectangle(double width, double height) implements Shape { }

Разбор:

  • sealed interface Shape ограничивает множество допустимых реализаций закрытым списком.
  • permits Circle, Rectangle явно перечисляет разрешённые подтипы и фиксирует границы модели на уровне компилятора.
  • Circle и Rectangle как record делают варианты лёгкими и неизменяемыми, что удобно для pattern matching.
  • Такая иерархия гарантирует, что новые типы фигур не появятся "случайно" вне согласованного контракта.
Модификатор подтипаСмысл
finalДальнейшее наследование запрещено
sealedСнова ограниченный список permits
non-sealedОткрыть ветку иерархии

Польза — исчерпываемые варианты (результат операции, тип сообщения), безопасный switch по типам.


Pattern matching

instanceof (Java 16+):

if (obj instanceof String s && !s.isBlank()) {
System.out.println(s.toUpperCase());
}

Разбор:

  • obj instanceof String s одновременно проверяет тип и создаёт типизированную переменную s в той же ветке.
  • Дополнительное условие && !s.isBlank() сразу отсекает пустые/пробельные строки.
  • s.toUpperCase() безопасен без приведения (String), потому что s уже точно String.
  • Такой синтаксис уменьшает шаблонный код и снижает риск ClassCastException.

switch по типу (Java 17+, расширяется в 21+):

static double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
};
}

Разбор:

  • switch (shape) выполняет сопоставление по типам sealed-иерархии Shape.
  • case Circle c -> ... и case Rectangle r -> ... связывают подтип с локальной переменной нужного типа.
  • Формула Math.PI * r^2 вычисляет площадь круга через компонент radius(), для прямоугольника — width() * height().
  • return switch (...) делает switch выражением, которое возвращает значение, а не просто управляет потоком.
  • Для закрытой иерархии компилятор проверяет полноту обработки вариантов, что повышает надёжность при рефакторинге.

Компилятор проверяет полноту для sealed-иерархий: если все подтипы известны, default не обязателен.

switch-выражение (Java 14+) возвращает значение:

String level = switch (code) {
case 1, 2 -> "low";
case 3 -> "medium";
default -> "high";
};

Разбор:

  • switch здесь вычисляет значение и присваивает его переменной level.
  • case 1, 2 -> "low" объединяет несколько значений в одну ветку без fall-through.
  • default -> "high" задаёт безопасный путь для всех остальных кодов.
  • Такой стиль читается как таблица правил маппинга и снижает количество ошибок с пропущенным break.

Стрелки -> без fall-through; классический case X: с break по-прежнему доступен.


Частые ошибки при переходе на новый синтаксис

ОшибкаПочему возникаетКак исправить
Использовать record для mutable-сущностиУ record семантика "значения", а не "изменяемого агрегата"Для изменяемого состояния оставлять обычный класс
Ставить var везде подрядТип справа не читается с первого взглядаИспользовать var только при очевидном типе
Смешивать старый switch и новый без необходимостиПолучается неоднородный стильДля новых ветвлений предпочитать switch-выражение
Делать sealed-иерархию без понятной доменной границыОграничение подтипов становится искусственнымПрименять sealed там, где набор вариантов реально конечный

Текстовые блоки и formatted

См. строки: """ ... """ и "{}".formatted(args).


Вспомогательные API

  • Optional — см. Stream API.
  • List.of, Map.of, Set.of — неизменяемые коллекции фиксированного размера.
  • Objects.requireNonNullElse — null-safe значения по умолчанию.

Что выбрать в новом коде

ЗадачаРекомендация
DTO ответа APIrecord
Закрытый набор вариантовsealed + switch
Богатая доменная сущностьобычный класс, инкапсуляция
Анонимный коллбэклямбда вместо анонимного класса (особенности)
Много полей с наследованиемкласс, не record

Небольшой практический кейс

Ниже минимальный пример закрытой модели событий:

public sealed interface PaymentEvent permits PaymentCreated, PaymentFailed {}

public record PaymentCreated(String paymentId, long amount) implements PaymentEvent {}
public record PaymentFailed(String paymentId, String reason) implements PaymentEvent {}

public String toLog(PaymentEvent event) {
return switch (event) {
case PaymentCreated e -> "created: " + e.paymentId();
case PaymentFailed e -> "failed: " + e.reason();
};
}

Разбор:

  • PaymentEvent задаёт закрытый набор доменных событий платежа, что удобно для надёжной обработки бизнес-сценариев.
  • PaymentCreated и PaymentFailed фиксируют разные структуры данных для успешного и неуспешного исхода.
  • В toLog(PaymentEvent event) switch по типу формирует корректное сообщение под конкретный вариант события.
  • case PaymentCreated e -> ... и case PaymentFailed e -> ... дают типобезопасный доступ к полям через аксессоры record.
  • При добавлении нового подтипа компилятор потребует обновить switch, поэтому логирование не "забывается" при развитии модели.

Главный плюс такого подхода — компилятор контролирует полноту обработки всех вариантов PaymentEvent.


Версии и совместимость

  • Укажите release / source / target в Maven/Gradle под LTS вашей команды.
  • Multi-release JAR — если нужны разные реализации под разные JDK (конфигурации).
  • Не используйте preview-фичи в production без --enable-preview и плана миграции.

Связанные материалы