Библиотека джависта | Java, Spring, Maven, Hibernate
23.4K subscribers
2.19K photos
45 videos
45 files
3.1K links
Все самое полезное для Java-разработчика в одном канале.

Список наших каналов: https://t.me/proglibrary/9197

Для обратной связи: @proglibrary_feeedback_bot

По рекламе: @proglib_adv

РКН: https://gosuslugi.ru/snet/67a5bbda1b17b35b6c1a55c4
Download Telegram
👀 Внутреннее устройство LinkedHashSet

LinkedHashSet — это реализация интерфейса Set из пакета java.util, которая сохраняет порядок вставки элементов. Это гибрид между HashSet (быстрый поиск) и списком (предсказуемый порядок итерации).

📦 Базовая структура


LinkedHashSet — это тонкая обёртка над LinkedHashMap. Внутри:

— Все элементы хранятся как ключи в LinkedHashMap.

— Значения — константа PRESENT (заглушка Object).

— Порядок поддерживается через двусвязный список узлов.

Главная особенность:

— O(1) для add, remove, contains (как у HashSet).

— Предсказуемый порядок итерации (порядок вставки).

— Немного больше памяти, чем HashSet (~25% overhead на связи).

🔍 Как устроено хранение

LinkedHashSet полностью делегирует работу LinkedHashMap, а тот устроен так:
Entry<K,V> — узел хранения:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before; // предыдущий в порядке вставки
Entry<K,V> after; // следующий в порядке вставки
}
```

### **Двойная структура:**

1. **Хэш-таблица** (массив бакетов):
- Быстрый доступ по хэшу → O(1).
- Разрешение коллизий через цепочки/деревья.

2. **Двусвязный список**:
- Связывает все элементы в порядке добавления.
- Голова списка: `head` (первый добавленный).
- Хвост списка: `tail` (последний добавленный).

**Визуализация:**
```
Хэш-таблица:
Bucket[0]: null
Bucket[1]: Entry("B") ----→ Entry("F")
Bucket[2]: Entry("A")
Bucket[3]: Entry("C")

Двусвязный список (порядок вставки):
head → Entry("A") ⇄ Entry("B") ⇄ Entry("C") ⇄ Entry("F") ← tail


⚡️ Операции добавления, удаления и поиска

add(E element) — добавление

1. Вычисляется хэш элемента: hash(e).
2. Определяется индекс бакета: index = hash & (n-1).
3. Проверяется наличие элемента в бакете:
— если есть → возвращается false (дубликат).
— если нет → создаётся новый Entry.
4. Новый Entry:
— добавляется в бакет хэш-таблицы.
— связывается с tail в двусвязном списке.
— обновляется tail = newEntry.
5. При необходимости таблица расширяется (load factor > 0.75).

Сложность: O(1) в среднем.

🔎 contains(Object o) — проверка наличия

1. Вычисляется хэш объекта.
2. Проверяется соответствующий бакет.
3. Сравнивается через equals().
4. Двусвязный список НЕ используется для поиска.

Сложность: O(1) в среднем.

remove(Object o) — удаление

1. Находится Entry в хэш-таблице по хэшу.
2. Узел удаляется из бакета.
3. Узел отсоединяется от двусвязного списка.
4. Обновляются ссылки head/tail при необходимости.

Сложность: O(1) в среднем.

⚖️ Важные нюансы

1. Наследование от HashSet

Наследует поведение HashSet, но меняет внутреннюю реализацию. Конструкторы создают LinkedHashMap вместо HashMap.

2. Null элементы

Один null может быть добавлен (как в HashSet).

3. Не потокобезопасен

Для многопоточного доступа требуется внешняя синхронизация. Альтернатива: CopyOnWriteArraySet (но без хэш-таблицы).

4. Equals и hashCode

Сравнивает содержимое, игнорируя порядок:

5. Capacity и Load Factor

Начальные значения: Capacity 100, load factor 0.75

Начальная ёмкость должна учитывать ожидаемый размер.
При достижении threshold (capacity × load factor) происходит resize.

Не использовать если

1. Порядок не важен

Используйте HashSet — проще и немного быстрее (меньше overhead).

2. Нужна сортировка

Используйте TreeSet — автоматическая сортировка по Comparator/Comparable.

3. Многопоточный доступ

Используйте ConcurrentHashMap.newKeySet() или CopyOnWriteArraySet. Или оборачивайте: Collections.synchronizedSet().

4. Критична минимизация памяти:

HashSet использует меньше памяти (~20% экономии).

🔗 Документация: JavaDoc (Java 17)

Ставьте 🔥, если хотите разбор TreeSet!

🐸 Библиотека джависта

#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11👍42🎉1
🔍 Просто о сложном: 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.

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
🔥84👍4👏1
🆕 Sequenced Collections — решение 20-летней проблемы

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.

🔼 Sequenced Collections — это не революция, но эволюция. Они заполняют пробел, который был в Java с первой версии, делая API консистентным и предсказуемым (хотя не без подводных камней).

💬 Что думаете о новом API?

🐸 Библиотека джависта

#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17🔥42
🤫 SOLID-принципы простыми словами

Чистый, масштабируемый и поддерживаемый код — не просто идеал, а необходимость. SOLID помогает писать архитектуру, которая выдерживает рост и изменения без боли 👇

1️⃣ SRP — Single Responsibility Principle
🧩 Каждый класс отвечает за что-то одно.
📍 Пример: Employee хранит данные сотрудника, но не считает зарплату — этим занимается PayrollService.

2️⃣ OCP — Open/Closed Principle
🧱 Класс открыт для расширения, но закрыт для изменения.
📍 Пример: интерфейс Shape с методом calculateArea().
Новые фигуры (Circle, Rectangle) добавляются без правки существующего кода.

3️⃣ LSP — Liskov Substitution Principle
🦆 Подкласс должен полностью заменять родителя, не ломая логику.
📍 Пример: если Bird умеет fly(), то Sparrow должен уметь летать.
Но Penguin не должен наследовать fly() — нарушает LSP.

4️⃣ ISP — Interface Segregation Principle
🔌 Не заставляй клиентов реализовывать лишние методы.
📍 Пример: вместо интерфейса Worker с work() и eat(),
разделите его на Workable и Eatable.
Робот реализует только Workable, человек — оба.

5️⃣ DIP — Dependency Inversion Principle
⚙️ Зависим от абстракций, а не от реализаций.
📍 Пример: Switch работает с интерфейсом Switchable.
Ему всё равно, включает ли он лампу (LightBulb) или вентилятор (Fan).

💡 Освоив SOLID, вы начнёте проектировать системы,
которые не боятся изменений и масштабируются без боли.

🐸 Библиотека джависта

#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17🔥51
🔍 Просто о сложном: Record Class

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
👍171🔥1👏1
👑 Фишка Lombok: @With

Как вы обычно обновляете одно поле в неизменяемом объекте? Создаёте копию с нужным значением?

Муторно. 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
👍194🔥3🥱1
🎯 Record Patterns в Java 21: как компилятор превращает деструктуризацию в bytecode

После двух 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


Причина: семантическая неоднозначность и возможные проблемы с нулевыми элементами.

📌 Record Patterns + Pattern Matching for switch — это огромный шаг к функциональному стилю в Java. Код становится декларативным, компактным и безопасным.

🐸 Библиотека джависта

#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥52👍2👏2