Опирается на правила:
JS-8.1…JS-8.7из Java Style Guide → раздел 8. Современные фичи Java.
Важно знать
- Раздел применяется если Java 21+. На Java 17 — только record + sealed без patterns. Java 11 — не применим.
switchexpression на sealed — exhaustiveness check от компилятора.- Record patterns в
case— деконструкция:case Created(UUID id) -> id.- Record patterns в
instanceof— деконструкция без binding+getter.- Exhaustive switch без
default—defaultпревращает compile-time error в runtime.- Sealed interfaces — closed-набор альтернатив (
None/Reused/Created).String.formatted()вместоString.format().recordдля in-class data carriers — 3-5 полей, только в этом классе.
Java 21 — текущая LTS-версия для UCP-проектов. Sealed interfaces + record patterns + exhaustive switch дают компилятор-enforced полноту обработки вариантов. Это в разы лучше runtime-pattern matching через instanceof + if/else.
Switch expression на sealed
JS-8.1: вместо if-else цепочек.
public sealed interface ParkingResolution {
record None() implements ParkingResolution {}
record Reused(UUID id) implements ParkingResolution {}
record Created(UUID id) implements ParkingResolution {}
}
// ✓ — switch expression, exhaustiveness check
public UUID extractId(ParkingResolution parking) {
return switch (parking) {
case ParkingResolution.None ignored -> null;
case ParkingResolution.Reused(UUID id) -> id;
case ParkingResolution.Created(UUID id) -> id;
};
}
// ✗ — instanceof chain, exhaustiveness не гарантируется
public UUID extractId(ParkingResolution parking) {
if (parking instanceof ParkingResolution.None) return null;
if (parking instanceof ParkingResolution.Reused r) return r.id();
if (parking instanceof ParkingResolution.Created c) return c.id();
throw new IllegalStateException("Unhandled: " + parking);
}
Что даёт switch:
- Compile-time проверка покрытия всех вариантов sealed-иерархии.
- При добавлении
record Pending(UUID id) implements ParkingResolution— компилятор сломает сборку: «не обработан Pending». - Без unreachable
throw new IllegalStateException(...)— компилятор знает, что после exhaustive switch return unreachable.
Record patterns в case
JS-8.2: деконструкция.
// ✓ — деконструкция в case
case ParkingResolution.Created(UUID id) -> id;
// ✗ — binding + геттер
case ParkingResolution.Created c -> c.id();
Record pattern достаёт поля прямо в case. Меньше кода, нет промежуточной переменной.
Когда поле есть, но не нужно:
case ParkingResolution.None ignored -> null;
ignored (Java convention) — binding нужен для type matching, но значение не используется.
Record patterns в instanceof
JS-8.3: то же для if.
// ✓ — record pattern
if (parking instanceof ParkingResolution.Created(UUID parkingSessionId)) {
parkingSessionService.cancelParking(parkingSessionId);
}
// ✗ — binding + геттер
if (parking instanceof ParkingResolution.Created created) {
parkingSessionService.cancelParking(created.id());
}
Тот же принцип: достаём поля прямо в instanceof.
Exhaustive switch без default
JS-8.4: critical для sealed.
// ✓ — компилятор сломает сборку, если добавить новый ParkingResolution
return switch (parking) {
case ParkingResolution.None ignored -> null;
case ParkingResolution.Reused(UUID id) -> id;
case ParkingResolution.Created(UUID id) -> id;
};
// ✗ — default превращает compile-time error в runtime
return switch (parking) {
case ParkingResolution.None ignored -> null;
case ParkingResolution.Reused(UUID id) -> id;
case ParkingResolution.Created(UUID id) -> id;
default -> throw new IllegalStateException("Unhandled: " + parking);
};
Сценарий с default:
- Добавили
record Pending(UUID id) implements ParkingResolution. - Компилятор не ругается —
defaultобрабатывает. - При первом
Pendingв runtime —IllegalStateException. - Инцидент в проде.
Без default:
- Добавили
Pending. - Компилятор сразу ругается во всех местах switch.
- Не получится merge PR без обработки.
- Compile-time → runtime прокидывание удалено.
default оправдан только для open-иерархий (switch по String, int, enum, который может расширяться).
Sealed interfaces
JS-8.5: closed-набор альтернатив.
// ✓ — sealed: компилятор знает полный список implements
public sealed interface ParkingResolution {
record None() implements ParkingResolution {}
record Reused(UUID id) implements ParkingResolution {}
record Created(UUID id) implements ParkingResolution {}
static ParkingResolution none() { return new None(); }
}
public sealed interface OrderResult {
record Success(Order order) implements OrderResult {}
record Failure(String reason) implements OrderResult {}
}
Преимущества:
- Exhaustiveness check в switch (
JS-8.1). - Документирует возможные результаты — читатель видит «3 варианта resolution».
- Refactoring safety — добавил вариант, компилятор показывает все switch-места требующие обновления.
sealed не для:
- Open-расширяемых иерархий (доменные базовые классы для extension в other modules/services). Используем обычный
interface.
String.formatted()
JS-8.6: вместо String.format.
// ✓ — линейное чтение
"User(id=%s) default card not found".formatted(userId);
log.info("Order created: orderId={} amount={}".formatted(orderId, amount));
// ✗ — формат в начале, аргументы в конце
String.format("User(id=%s) default card not found", userId);
log.info(String.format("Order created: orderId=%s amount=%s", orderId, amount));
Чтение слева-направо: «строка → подставить аргументы». String.format(...) заставляет глаз бегать туда-обратно. Особенно заметно в длинных exception-сообщениях.
Альтернатива в Spring (через slf4j placeholders) — обычно лучше для logging:
log.info("Order created: orderId={} amount={}", orderId, amount);
{}-placeholders ленивы (не выполняют toString при выключенном уровне). Используем для логов. .formatted() — для exception messages и других eager-форматов.
Records для in-class data carriers
JS-8.7: 3-5 полей, только в одном классе.
@UseCase
@RequiredArgsConstructor
public class StartChargingHandler implements UseCaseHandler<StartChargingCommand, Session> {
private final ConnectorsRepository connectorsRepository;
private final ChargeStationsRepository chargeStationsRepository;
private final LocationsRepository locationsRepository;
// ✓ — private record для группировки данных
private record ChargingContext(
ConnectorsPojo connector,
ChargeStationsPojo chargeStation,
LocationsPojo location
) {}
public Session handle(StartChargingCommand command) {
var context = loadContext(command.connectorId());
return start(context, command);
}
private ChargingContext loadContext(Long connectorId) {
var connector = connectorsRepository.findById(connectorId).orElseThrow();
var station = chargeStationsRepository.findById(connector.getStationId()).orElseThrow();
var location = locationsRepository.findById(station.getLocationId()).orElseThrow();
return new ChargingContext(connector, station, location);
}
}
// ✗ — Lombok-класс ради тех же геттеров
@Getter
@RequiredArgsConstructor
private static class ChargingContext {
private final ConnectorsPojo connector;
private final ChargeStationsPojo chargeStation;
private final LocationsPojo location;
}
Когда private record подходит:
- 3-5 полей, связанных по смыслу.
- Используется только в этом классе (private).
- Нет custom behaviour (только data).
Когда VO (отдельный класс в domain/):
- Используется в нескольких классах.
- Имеет доменные методы (
Money.add(),OrderId.next()). - Часть бизнес-словаря.
Lombok поверх records запрещён (JS-6.4).
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
if/else if/else chain для sealed | JS-8.1 | switch expression |
case X x -> x.field() (binding + getter) | JS-8.2 | record pattern case X(field) |
instanceof X x + x.field() | JS-8.3 | instanceof X(field) |
default -> в exhaustive switch на sealed | JS-8.4 | без default |
String.format("...", args) | JS-8.6 | "...".formatted(args) |
@Getter @RequiredArgsConstructor private data class | JS-8.7 | private record |
| Lombok поверх record | JS-6.4 | record даёт всё сам |
| Sealed для open-расширяемой иерархии | JS-8.5 | обычный interface |
Куда дальше
- Java → раздел 8. Современные фичи Java — нормативные формулировки.
- Именование — record accessor
name()(безget). - Выражения — guard expression vs switch.
- Lombok — Lombok НЕ на record.
- Комментарии — sealed switch устраняет необходимость в комментариях о вариантах.
- DDD → value-objects — record для domain VO.
- Distributed → compensation — sealed
Result.Success/Result.Failure.