🔍 Просто о сложном: Sealed Classes
В Java 17 появились Sealed Classes — механизм явного контроля иерархии наследования. Теперь можно точно указать, какие классы могут наследоваться от вашего типа.
По сути, это золотая середина между публичными классами (наследовать может кто угодно) и final классами (наследовать нельзя вообще). Вы сами решаете, кто входит в "белый список" наследников.
🔹 Зачем они нужны
Обычное наследование имеет проблемы:
— невозможно гарантировать закрытость иерархии (кто-то может добавить свой подтип);
— компилятор не может проверить, что все случаи покрыты (switch требует default, даже если вы обработали все варианты);
— сложно моделировать ADT (Algebraic Data Types) из функционального программирования.
Sealed классы решают эти проблемы: компилятор знает все возможные подтипы и может проверить полноту обработки в pattern matching.
🔹 Ключевые моменты
▪️ sealed — ключевое слово для объявления запечатанного класса/интерфейса.
▪️ permits — явное перечисление разрешённых наследников.
▪️ Наследники должны быть: final, sealed, или non-sealed.
▪️ Если наследники в том же файле, permits можно опустить.
▪️ Отлично работает с pattern matching и switch expressions.
🔹 Под капотом
Компилятор создаёт специальный атрибут PermittedSubclasses в bytecode, который содержит список разрешённых наследников. При загрузке класса JVM проверяет, что все указанные подклассы действительно существуют и корректны.
Pattern matching с sealed types позволяет компилятору проверить полноту покрытия без default ветки:
🔹 Подводные камни
— Обратная совместимость
Если вы сделали класс sealed в новой версии библиотеки, старый код с кастомными наследниками перестанет компилироваться.
— Видимость подклассов
Все наследники должны быть доступны sealed классу на момент компиляции. Нельзя добавить подкласс из другого модуля или jar.
— Сериализация
При десериализации sealed иерархии нужна осторожность? можно получить подделанный подтип. Используйте validation или sealed интерфейсы с records.
— non-sealed подклассы
Если сделать наследника non-sealed, он открывает дыру в иерархии и от него можно наследоваться кому угодно. Используйте осторожно.
✔️ Когда использовать
— Моделирование состояний (State machines, FSM).
— Result/Either типы для обработки ошибок без exceptions.
— Domain-driven design с явными типами (Payment может быть Card, Cash, Crypto).
— Pattern matching в бизнес-логике с гарантией полноты.
— API, где важно контролировать расширяемость.
❌ Не подходит:
— Публичные библиотеки с plugin-архитектурой.
— Когда нужна расширяемость от пользователей.
— Legacy код с активным использованием наследования.
— Простые entity/DTO классы без полиморфизма.
🐸 Библиотека джависта
#CoreJava
В Java 17 появились Sealed Classes — механизм явного контроля иерархии наследования. Теперь можно точно указать, какие классы могут наследоваться от вашего типа.
По сути, это золотая середина между публичными классами (наследовать может кто угодно) и final классами (наследовать нельзя вообще). Вы сами решаете, кто входит в "белый список" наследников.
🔹 Зачем они нужны
Обычное наследование имеет проблемы:
— невозможно гарантировать закрытость иерархии (кто-то может добавить свой подтип);
— компилятор не может проверить, что все случаи покрыты (switch требует default, даже если вы обработали все варианты);
— сложно моделировать ADT (Algebraic Data Types) из функционального программирования.
Sealed классы решают эти проблемы: компилятор знает все возможные подтипы и может проверить полноту обработки в pattern matching.
🔹 Ключевые моменты
▪️ sealed — ключевое слово для объявления запечатанного класса/интерфейса.
▪️ permits — явное перечисление разрешённых наследников.
▪️ Наследники должны быть: final, sealed, или non-sealed.
▪️ Если наследники в том же файле, permits можно опустить.
▪️ Отлично работает с pattern matching и switch expressions.
public sealed interface Result<T>
permits Success, Failure {
}
public final record Success<T>(T value)
implements Result<T> {}
public final record Failure<T>(String error)
implements Result<T> {}
🔹 Под капотом
Компилятор создаёт специальный атрибут PermittedSubclasses в bytecode, который содержит список разрешённых наследников. При загрузке класса JVM проверяет, что все указанные подклассы действительно существуют и корректны.
Pattern matching с sealed types позволяет компилятору проверить полноту покрытия без default ветки:
return switch(result) {
case Success(var value) -> process(value);
case Failure(var error) -> handleError(error);
};🔹 Подводные камни
— Обратная совместимость
Если вы сделали класс sealed в новой версии библиотеки, старый код с кастомными наследниками перестанет компилироваться.
— Видимость подклассов
Все наследники должны быть доступны sealed классу на момент компиляции. Нельзя добавить подкласс из другого модуля или jar.
— Сериализация
При десериализации sealed иерархии нужна осторожность? можно получить подделанный подтип. Используйте validation или sealed интерфейсы с records.
— non-sealed подклассы
Если сделать наследника non-sealed, он открывает дыру в иерархии и от него можно наследоваться кому угодно. Используйте осторожно.
— Моделирование состояний (State machines, FSM).
— Result/Either типы для обработки ошибок без exceptions.
— Domain-driven design с явными типами (Payment может быть Card, Cash, Crypto).
— Pattern matching в бизнес-логике с гарантией полноты.
— API, где важно контролировать расширяемость.
— Публичные библиотеки с plugin-архитектурой.
— Когда нужна расширяемость от пользователей.
— Legacy код с активным использованием наследования.
— Простые entity/DTO классы без полиморфизма.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥8❤4👍4👏1
JEP 431 добавил в Java то, чего не хватало с самого начала, а именно единый интерфейс для упорядоченных коллекций. Разберем, что изменилось под капотом.
🔹 Проблема, которую игнорировали
До Java 21 List и Deque имеют порядок обхода, но их общий суперинтерфейс Collection - нет. Set не гарантирует порядок, но LinkedHashSet и SortedSet — гарантируют. Единого API для "дай первый/последний элемент" или "обход в обратном порядке" не существовало.
Результат? Костыли:
// До Java 21
list.get(0) // List
deque.getFirst() // Deque
sortedSet.first() // SortedSet
linkedHashSet.iterator().next() // LinkedHashSet
🔹 Три новых интерфейса
JEP 431 внедрил в иерархию коллекций три интерфейса:
interface SequencedCollection<E> extends Collection<E> {
SequencedCollection<E> reversed();
void addFirst(E);
void addLast(E);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}
interface SequencedSet<E> extends SequencedCollection<E>, Set<E> {
SequencedSet<E> reversed();
}
interface SequencedMap<K,V> extends Map<K,V> {
SequencedMap<K,V> reversed();
Map.Entry<K,V> firstEntry();
Map.Entry<K,V> lastEntry();
Map.Entry<K,V> pollFirstEntry();
Map.Entry<K,V> pollLastEntry();
// ... и другие методы
}
Все методы (кроме reversed()) - это default методы, промоутнутые из Deque. Это обеспечило обратную совместимость.
▪️ Важнейший нюанс: reversed() возвращает view, а не копию.
Изменения в оригинале видны в reversed view. Под капотом это lightweight wrapper, который инвертирует индексы при обращении.
🔹 Интеграция в иерархию
Collection<E>
└─ SequencedCollection<E>
├─ List<E>
├─ Deque<E>
└─ Set<E>
└─ SequencedSet<E>
└─ SortedSet<E>
└─ NavigableSet<E>
Map<K,V>
└─ SequencedMap<K,V>
└─ SortedMap<K,V>
└─ NavigableMap<K,V>
🔹 SortedSet и SortedMap
SortedSet и SortedMap теперь имплементируют sequenced интерфейсы, но есть нюанс: методы addFirst()/addLast()/putFirst()/putLast() существуют лишь для того, чтобы бросить UnsupportedOperationException.
Liskov Substitution Principle: "Am I a joke to you?"
Но справедливости ради — Java Collections делает так уже давно:
— map.keySet().add() → UnsupportedOperationException
— Arrays.asList().add() → UnsupportedOperationException
— Collections.unmodifiableList().set() → UnsupportedOperationException
Это называется optional operations — когда методы есть, но работать не обязаны. Документация прямо говорит:
Many methods will throw UnsupportedOperationException if the operation cannot be performed.
Альтернативы были хуже: либо создавать зоопарк из ReadOnlySequencedSet, MutableSequencedSet, PartiallyMutableSequencedSet, либо оставить SortedSet за бортом единообразного API.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17🔥4❤2
Чистый, масштабируемый и поддерживаемый код — не просто идеал, а необходимость. SOLID помогает писать архитектуру, которая выдерживает рост и изменения без боли 👇
🧩 Каждый класс отвечает за что-то одно.
📍 Пример: Employee хранит данные сотрудника, но не считает зарплату — этим занимается PayrollService.
🧱 Класс открыт для расширения, но закрыт для изменения.
📍 Пример: интерфейс Shape с методом calculateArea().
Новые фигуры (Circle, Rectangle) добавляются без правки существующего кода.
🦆 Подкласс должен полностью заменять родителя, не ломая логику.
📍 Пример: если Bird умеет fly(), то Sparrow должен уметь летать.
Но Penguin не должен наследовать fly() — нарушает LSP.
🔌 Не заставляй клиентов реализовывать лишние методы.
📍 Пример: вместо интерфейса Worker с work() и eat(),
разделите его на Workable и Eatable.
Робот реализует только Workable, человек — оба.
⚙️ Зависим от абстракций, а не от реализаций.
📍 Пример: Switch работает с интерфейсом Switchable.
Ему всё равно, включает ли он лампу (LightBulb) или вентилятор (Fan).
💡 Освоив SOLID, вы начнёте проектировать системы,
которые не боятся изменений и масштабируются без боли.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17🔥5❤1
Record-классы появились в Java 14 (вначале как preview, позже стабилизированы) и стали настоящим спасением от шаблонного кода.
Это лаконичный способ описать неизменяемые data-классы — без десятков строк с конструкторами, equals(), hashCode() и toString().
🔹 Зачем они нужны
Раньше, чтобы описать простой объект вроде User, нужно было писать шаблонный код:
class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
// геттеры, equals, hashCode, toString...
}
С record это выглядит так:
record User(String name, int age) {}И всё — у вас уже есть:
— конструктор, геттеры, equals(), hashCode(), toString();
— неизменяемость полей;
— компактность и читаемость.
🔹 Ключевые моменты
▪️ record — это специальный вид класса, унаследованный от java.lang.Record.
▪️ Все поля — final, сеттеров нет.
▪️ Можно добавить собственные методы и статические фабрики.
▪️ Можно переопределить канонический конструктор для валидации.
▪️ Можно объявлять вложенные рекорды и использовать их в switch или pattern matching.
🔹 Под капотом
Record — это не просто “синтаксический сахар”. JVM видит его как финальный класс с приватными финальными полями и стандартными методами, но запрещает наследование (final) и предполагает неизменность.
Так JVM и JIT могут делать агрессивные оптимизации — объекты рекордов живут меньше, быстрее создаются и не требуют избыточных проверок на изменение состояния.
🔹 Подводные камни
— Record ≠ DTO везде
Если вы сериализуете/десериализуете через фреймворки (Jackson, JPA), убедитесь, что они поддерживают record (современные версии — да).
— Проблемы с неймингом
Для рекордов автоматически создаются методы-геттеры без префикса get. Например, для record User(String name) будет метод name(), а не getName().
Это ломает привычные JavaBean-паттерны и может вызвать проблемы с библиотеками, которые ожидают именно getName().
— Не подходит, если нужен мутабельный объект.
Для билдера или ORM-энтити используйте обычный класс.
— Не добавляйте бизнес-логику внутрь record.
Это data-контейнер, а не доменная сущность.
— DTO между слоями;
— Результаты запросов к БД (projection);
— Ответы REST API;
— Ключи в Map и Set;
— В тестах и утилитах для временных структур.
— На практике редко используется из-за проблем с неймингом.
— Для ORM-сущностей, билдера, и изменяемых структур.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17❤1🔥1👏1
Как вы обычно обновляете одно поле в неизменяемом объекте? Создаёте копию с нужным значением?
Муторно. Lombok умеет делать это просто и элегантно.
🔹 Аннотация @With
Генерирует методы withX(...), которые создают копию объекта с изменённым полем. Подходит для immutable-моделей и паттерна builder. Класс при этом должен быть final (например, через @Value или вручную).
🔹 Пример
@Value
@With
public class User {
String name;
int age;
}
Теперь можно:
User user1 = new User("Alice", 25);
User user2 = user1.withAge(30); // создаётся новый объект с новым ageОбъекты остаются неизменяемыми, но при этом легко "обновляемыми".
🔹 Зачем это нужно
— Удобно при работе с immutable-классами.
— Простой способ "копировать с изменением".
— Чистый, декларативный стиль без boilerplate.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍21❤4🔥3🥱1
🎯 Record Patterns в Java 21: как компилятор превращает деструктуризацию в bytecode
После двух preview (Java 19-20), Record Patterns стали финальной фичей в Java 21. Разберем механику работы под капотом.
🔹 Что такое Record Patterns
Это расширение pattern matching для деструктуризации record классов:
🔹 Nested patterns — вложенная деструктуризация
Мощь раскрывается с вложенными record'ами:
Компилятор генерирует код примерно так:
Для вложенных patterns компилятор создает каскад instanceof проверок и вызовов accessor методов.
🔹 Pattern Matching для switch
Record patterns работают в switch (JEP 441 - тоже финализирован в Java 21):
🔹 Guarded patterns
Можно добавлять условия:
🔹 Важное изменение с preview
В финальной версии убрали поддержку record patterns в enhanced for:
Причина: семантическая неоднозначность и возможные проблемы с нулевыми элементами.
📌 Record Patterns + Pattern Matching for switch — это огромный шаг к функциональному стилю в Java. Код становится декларативным, компактным и безопасным.
🐸 Библиотека джависта
#CoreJava
После двух preview (Java 19-20), Record Patterns стали финальной фичей в Java 21. Разберем механику работы под капотом.
🔹 Что такое Record Patterns
Это расширение pattern matching для деструктуризации record классов:
record Point(int x, int y) {}
// До Java 21
if (obj instanceof Point point) {
int x = point.x();
int y = point.y();
// use x, y
}
// Java 21
if (obj instanceof Point(int x, int y)) {
// x и y автоматически в scope
System.out.println(x + y);
}
🔹 Nested patterns — вложенная деструктуризация
Мощь раскрывается с вложенными record'ами:
record Point(int x, int y) {}
record Rectangle(Point upperLeft, Point lowerRight) {}
// Одна строка вместо цепочки вызовов
if (shape instanceof Rectangle(Point(int x1, int y1),
Point(int x2, int y2))) {
int area = Math.abs((x2-x1) * (y2-y1));
}
Компилятор генерирует код примерно так:
// Исходный код
if (obj instanceof Point(int x, int y)) {
process(x, y);
}
// Что генерирует компилятор (упрощенно)
if (obj instanceof Point __temp) {
int x = __temp.x();
int y = __temp.y();
process(x, y);
}
Для вложенных patterns компилятор создает каскад instanceof проверок и вызовов accessor методов.
🔹 Pattern Matching для switch
Record patterns работают в switch (JEP 441 - тоже финализирован в Java 21):
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double a, double b, double c) implements Shape {}
double area(Shape shape) {
return switch(shape) {
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
case Triangle(double a, double b, double c) -> {
double s = (a + b + c) / 2;
yield Math.sqrt(s * (s-a) * (s-b) * (s-c));
}
};
}
🔹 Guarded patterns
Можно добавлять условия:
String classify(Object obj) {
return switch(obj) {
case Point(int x, int y) when x == y -> "diagonal";
case Point(int x, int y) when x > y -> "above diagonal";
case Point(int x, int y) -> "below diagonal";
default -> "not a point";
};
}
🔹 Важное изменение с preview
В финальной версии убрали поддержку record patterns в enhanced for:
// Работало в preview
for (Point(int x, int y) : points) { }
// Больше не работает в Java 21 final
Причина: семантическая неоднозначность и возможные проблемы с нулевыми элементами.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5❤2👍2👏2
Testcontainers — это Java-библиотека, которая позволяет запускать Docker-контейнеры прямо из тестов.
Забудьте про моки базы данных, embedded PostgreSQL и H2 "вместо настоящей БД" — тестируйте на реальных зависимостях.
Библиотека предоставляет лёгкий API для управления жизненным циклом контейнеров: автоматический запуск перед тестом, проброс портов, ожидание готовности сервиса и гарантированная очистка после завершения.
Больше не нужно поднимать локальный PostgreSQL, настраивать Kafka кластер или держать Redis на машине разработчика — всё запускается изолированно в Docker и удаляется после тестов.
🔹 Ключевые моменты
▪️ Поддержка 50+ готовых модулей: PostgreSQL, MySQL, MongoDB, Redis, Kafka, Elasticsearch, Localstack и др.
▪️ Можно использовать любой Docker-образ через GenericContainer.
▪️ Автоматическая очистка — контейнеры удаляются после тестов.
▪️ Реальная изоляция — каждый тест может иметь свежее окружение.
▪️ Интеграция с JUnit 5, Spring Boot Test, Spock.
🔹 Под капотом
Testcontainers общается с Docker через docker-java клиент. При запуске теста библиотека:
1. Создаёт контейнер с нужным образом.
2. Ждёт готовности (health check или проверка порта).
3. Пробрасывает рандомный порт на localhost.
4. Передаёт connection URL в тест.
5. После теста останавливает и удаляет контейнер.
Есть механизм Ryuk (контейнер-уборщик) — он следит, что все тестовые контейнеры будут убиты, даже если JVM упала.
🔹 Подводные камни
— Нужен Docker
— Медленнее unit-тестов
— Потребление ресурсов
— Проблемы с сетью на CI
— Не забывайте фиксировать версии образов
— Интеграционные тесты с БД (JPA, JDBC, jOOQ);
— Тестирование Kafka consumers/producers;
— Проверка работы с Redis, Elasticsearch;
— E2E тесты с реальным окружением;
— Локальная разработка (docker-compose замена);
— Тестирование миграций БД (Flyway, Liquibase);
— Проверка совместимости с разными версиями зависимостей.
❌ Не подходит:
— Unit-тесты (слишком тяжело);
— Когда Docker недоступен;
— Performance-тесты (overhead на контейнеры);
— Простые CRUD операции (можно обойтись H2);
— CI с ограниченными ресурсами.
🔧 Бонус-трюк: используйте @ServiceConnection в Spring Boot 3.1+ — он автоматически сконфигурирует DataSource из TestContainer без ручного прописывания URL.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥4👍3❤1👏1
🎯 Optional: используй правильно
Optional появился в Java 8, но многие до сих пор используют его неэффективно. Короткий чеклист 👇
❌ Антипаттерны
✔️ Полезные комбинации
❌ Когда НЕ использовать
— Не используй Optional в полях класса
— Не передавай Optional как параметр метода
— Не создавай Optional для коллекций (верни пустую коллекцию)
— Не используй Optional для примитивов (есть OptionalInt, OptionalLong, OptionalDouble)
✔️ Когда использовать
— Возвращаемое значение метода, которое может отсутствовать
— Stream API operations
— Замена null в return
Ставь → 🔥, если полезно и хочешь короткие разборы других фич?
🐸 Библиотека джависта
#CoreJava
Optional появился в Java 8, но многие до сих пор используют его неэффективно. Короткий чеклист 👇
// Плохо #1: проверка через isPresent()
if (optional.isPresent()) {
doSomething(optional.get());
}
// ✔️ Правильно:
optional.ifPresent(this::doSomething);
// Плохо #2: get() без проверки
User user = userOptional.get(); // NoSuchElementException
// ✔️ Правильно:
User user = userOptional.orElseThrow(() ->
new UserNotFoundException("User not found"));
// Плохо #3: orElse с вычислением
return optional.orElse(repository.findDefault()); // ВСЕГДА вызовется!
// ✔️ Правильно:
return optional.orElseGet(() -> repository.findDefault());
// map + orElse
String name = userOptional
.map(User::getName)
.orElse("Anonymous");
// flatMap для вложенных Optional
Optional<String> email = userOptional
.flatMap(User::getEmail); // User::getEmail возвращает Optional<String>
// filter + ifPresent
userOptional
.filter(u -> u.getAge() > 18)
.ifPresent(this::sendAdultContent);
// or (Java 9+) - fallback к другому Optional
return cache.get(id)
.or(() -> database.find(id))
.orElseThrow();
— Не используй Optional в полях класса
— Не передавай Optional как параметр метода
— Не создавай Optional для коллекций (верни пустую коллекцию)
— Не используй Optional для примитивов (есть OptionalInt, OptionalLong, OptionalDouble)
— Возвращаемое значение метода, которое может отсутствовать
— Stream API operations
— Замена null в return
Ставь → 🔥, если полезно и хочешь короткие разборы других фич?
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥16👍3🤔2❤1