Современные конструкции 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, что удобно для файловых операций APIjava.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 ответа API | record |
| Закрытый набор вариантов | 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и плана миграции.
Связанные материалы
- Объектно-ориентированное программирование
- Ключевые слова в Java
- Аннотации и рефлексия
- История языка Java
- Чек-лист самопроверки
- Вопросы на собеседовании — Core Java
- JVM в проде — jcmd, дамп памяти и JFR