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
Ставь → 🔥, если полезно и хочешь короткие разборы других фич?
🔹 Курс «Алгоритмы и структуры данных»
🔹 Получить консультацию менеджера
🔹 Сайт Академии 🔹 Сайт Proglib
🐸 Библиотека джависта
#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
Ставь → 🔥, если полезно и хочешь короткие разборы других фич?
🔹 Курс «Алгоритмы и структуры данных»
🔹 Получить консультацию менеджера
🔹 Сайт Академии 🔹 Сайт Proglib
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥39👍3❤2🤔2
👀 Внутреннее устройство Map.computeIfAbsent()
computeIfAbsent() — это не просто «get или put». Это атомарная операция с ленивым вычислением, которая решает классическую проблему check-then-act в многопоточном коде.
📦 Что такое computeIfAbsent()
— Поведение
1. Если ключ существует и value != null → вернуть value
2. Если ключа нет или value == null → вызвать mappingFunction
3. Результат функции put в map
4. Вернуть computed value
— Классический use case:
🔍 Упрощённый код из JDK:
📊 Performance
Benchmark: 1M операций
✅ Делайте
— Используйте для lazy initialization
— Используйте ConcurrentHashMap для thread-safety
— Держите mappingFunction быстрым и простым
❌ Не делайте
— Не вызывайте computeIfAbsent рекурсивно на том же ключе
— Не модифицируйте map внутри mappingFunction
— Не возвращайте null если хотите кэшировать отсутствие
— Не используйте для побочных эффектов (только для вычисления value)
🔗 Документация
Ставьте 🔥, если интересны другие Map методы!
✨ Бонусы для подписчиков:
— Скидка 40% на все курсы Академии
— Розыгрыш Apple MacBook
— Бесплатный тест на знание математики
🐸 Библиотека джависта
#CoreJava
computeIfAbsent() — это не просто «get или put». Это атомарная операция с ленивым вычислением, которая решает классическую проблему check-then-act в многопоточном коде.
📦 Что такое computeIfAbsent()
— Поведение
1. Если ключ существует и value != null → вернуть value
2. Если ключа нет или value == null → вызвать mappingFunction
3. Результат функции put в map
4. Вернуть computed value
— Классический use case:
// ❌ Старый способ — race condition!
if (!map.containsKey(key)) {
map.put(key, expensiveOperation());
}
// ✅ Новый способ — атомарно
map.computeIfAbsent(key, k -> expensiveOperation());
🔍 Упрощённый код из JDK:
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (mappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab = table;
Node<K,V> first = tab[index];
// Поиск существующего entry
if (first != null) {
Node<K,V> e = first;
do {
if (e.hash == hash &&
Objects.equals(key, e.key)) {
V v = e.value;
if (v != null) {
return v; // Найден, не вызываем функцию!
}
}
} while ((e = e.next) != null);
}
// Ключа нет — вызов mappingFunction
V newValue = mappingFunction.apply(key);
if (newValue != null) {
putVal(hash, key, newValue, true, true);
}
return newValue;
}
📊 Performance
Benchmark: 1M операций
// Старый способ: containsKey + put
if (!map.containsKey(key)) {
map.put(key, new ArrayList<>());
}
// Time: ~45ms, 2 hash lookups
// computeIfAbsent
map.computeIfAbsent(key, k -> new ArrayList<>());
// Time: ~30ms, 1 hash lookup
computeIfAbsent() на 33% быстрее!
✅ Делайте
— Используйте для lazy initialization
— Используйте ConcurrentHashMap для thread-safety
— Держите mappingFunction быстрым и простым
❌ Не делайте
— Не вызывайте computeIfAbsent рекурсивно на том же ключе
— Не модифицируйте map внутри mappingFunction
— Не возвращайте null если хотите кэшировать отсутствие
— Не используйте для побочных эффектов (только для вычисления value)
Ставьте 🔥, если интересны другие Map методы!
✨ Бонусы для подписчиков:
— Скидка 40% на все курсы Академии
— Розыгрыш Apple MacBook
— Бесплатный тест на знание математики
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥7👍4❤2👏1