Библиотека Java разработчика
📚 Лайфхаки, приёмы и лучшие практики для Java-разработчиков. Всё, что ускорит код и прокачает навыки. Java, Spring, Maven, Hibernate. По всем вопросам @evgenycarter РКН clck.ru/3KoGeP
Mostrar más📈 Análisis del canal de Telegram Библиотека Java разработчика
El canal Библиотека Java разработчика (@bookjava) en el segmento lingüístico de Ruso es un actor destacado. Actualmente la comunidad reúne a 10 118 suscriptores, ocupando la posición 11 683 en la categoría Tecnologías y Aplicaciones y el puesto 62 847 en la región Rusia.
📊 Métricas de audiencia y dinámica
Desde su creación el невідомо, el proyecto ha mostrado un crecimiento acelerado, reuniendo a 10 118 suscriptores.
Según los últimos datos del 26 agosto, 2026, el canal mantiene una actividad estable. En los últimos 30 días la variación de miembros fue de -73, y en las últimas 24 horas de -3, conservando un alto alcance.
- Estado de verificación: No verificado
- Tasa de interacción (ER): El promedio de interacción de la audiencia es 6.99%. Durante las primeras 24 horas tras publicar, el contenido suele obtener 3.71% de reacciones respecto al total de suscriptores.
- Alcance de las publicaciones: Cada publicación recibe en promedio 707 visualizaciones. En el primer día suele acumular 375 visualizaciones.
- Reacciones e interacción: La audiencia responde de forma activa: el promedio de reacciones por publicación es 7.
- Intereses temáticos: El contenido se centra en temas clave como string, интерфейс, строка, boot, api.
📝 Descripción y política de contenido
El autor describe el recurso como un espacio para expresar opiniones subjetivas:
“📚 Лайфхаки, приёмы и лучшие практики для Java-разработчиков. Всё, что ускорит код и прокачает навыки. Java, Spring, Maven, Hibernate.
По всем вопросам @evgenycarter
РКН clck.ru/3KoGeP”
Gracias a la alta frecuencia de actualizaciones (últimos datos recibidos el 27 agosto, 2026), el canal mantiene la vigencia y un amplio alcance. La analítica demuestra que la audiencia interactúa activamente con el contenido, lo que lo convierte en un punto de referencia dentro de la categoría Tecnologías y Aplicaciones.
Service s = new Service()). Вы - главный.
🔴Spring подход: Вы отдаете управление фреймворку. "Спринг, создай мне сервис и дай его, когда он понадобится". Spring - главный.
🏷 Как сделать Бин? (Аннотации)
Чтобы Spring узнал про ваши классы, их нужно пометить.
1. @Component — Самая базовая аннотация. "Эй, Спринг, это бин, управляй им!".
2. @Service - Тот же @Component, но семантически говорит: "Здесь бизнес-логика".
3. @Repository - Тот же @Component, но для работы с БД (ловит специфичные ошибки баз данных).
4. @Controller / @RestController - Для обработки HTTP-запросов.
5. @Configuration + @Bean - Используется, когда нужно создать бин из чужого класса (библиотеки), код которого вы не можете пометить аннотацией @Component.
💉 Dependency Injection (DI)
Главная фишка. Как один бин попадает внутрь другого?
Например, UserService нуждается в UserRepository.
❌ Способ 1: Через поле (Field Injection)
@Service
public class UserService {
@Autowired // ⚠️ Не рекомендуется!
private UserRepository repository;
}
Почему плохо: Невозможно протестировать (как подсунуть мок?), скрытые зависимости, возможен NullPointerException.
✅ Способ 2: Через конструктор (Constructor Injection)
Золотой стандарт современного Spring.
@Service
public class UserService {
private final UserRepository repository;
// @Autowired здесь не обязателен (в новых версиях Spring)
public UserService(UserRepository repository) {
this.repository = repository;
}
}
Плюсы: Поле final (неизменяемое), легко тестировать (можно передать любой репозиторий в конструктор), сразу видно все зависимости класса.
⚡ Лайфхак: Lombok
Чтобы не писать конструктор руками, используйте Lombok:
@Service
@RequiredArgsConstructor // Генерирует конструктор для final полей
public class UserService {
private final UserRepository repository; // Всё внедрится само!
}
🔥 Итог
Spring, это просто "мешок с объектами" (Context), которые он создает сам и связывает друг с другом (DI).
🔴Забудьте про new Service().
🔴Используйте внедрение через конструктор.
🔴Помечайте классы правильными аннотациями (@Service, @Repository).
#SpringBoot #Java #IoC #DI #SpringTips
📲 Мы в MAX
👉@BookJavaThread): Простой код, но один поток весит ~2 Мб памяти. Создадите 5,000 потоков - сервер упадет с OutOfMemoryError.
2. Асинхронность (WebFlux/Netty): Сервер держит 100k соединений, но код превращается в лапшу из callbacks и CompletableFuture, которую невозможно отлаживать.
В Java 21 появились Виртуальные потоки. Они объединяют простоту первого подхода и производительность второго.
🪶 В чем магия?
Классический поток Java (Platform Thread) привязан 1-к-1 к потоку операционной системы (OS Thread). Это дорогой ресурс.
Виртуальный поток, это просто объект в куче (heap) JVM. Он не привязан к ОС намертво.
🔴Вес: Несколько килобайт (вместо мегабайт).
🔴Количество: Можно создать миллион виртуальных потоков на обычном ноутбуке.
⚙️ Как это работает (Carrier Threads)
Под капотом работает схема Mount/Unmount:
1. JVM запускает небольшой пул обычных потоков ОС (называются Carrier Threads, обычно их число = числу ядер CPU).
2. Ваш виртуальный поток "садится верхом" на Carrier-поток и выполняет код.
3. ⚠️ Самое важное: Как только ваш код блокируется (ждет ответа от БД, читает файл, делает Thread.sleep), JVM снимает виртуальный поток с ядра.
4. Поток ОС освобождается и тут же берет в работу другой виртуальный поток.
Итог: Ядра процессора молотят на 100%, никогда не простаивая в ожидании ввода-вывода.
💻 Код: Найди 1 отличие
API практически не изменился. Вам не нужно учить новые фреймворки.
// Старый способ (Тяжелый поток ОС)
Thread.ofPlatform().start(() -> {
System.out.println("Я ем много памяти!");
});
// Новый способ (Легкий виртуальный поток)
Thread.ofVirtual().start(() -> {
System.out.println("Я ничего не вешу!");
});
// Использование с ExecutorService (для старого кода)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 1_000_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(1000); // Блокировка теперь БЕСПЛАТНАЯ
return i;
});
});
}
// Этот код запустит миллион задач за секунду, не положив сервер.
⚰️ Конец Reactive Programming?
Многие эксперты говорят: Да.
Смысл использования сложных реактивных библиотек (RxJava, Reactor) был в том, чтобы не блокировать потоки. Виртуальные потоки делают блокировку дешевой.
Теперь вы можете писать простой, последовательный код:
var user = db.findUser();
var data = http.sendRequest(user);
...и он будет работать так же эффективно, как сложный асинхронный код.
⚠️ Когда НЕ использовать?
Виртуальные потоки идеальны для I/O задач (ждать сеть, ждать диск).
Они бесполезны для CPU-Intensive задач (майнинг, шифрование, обработка видео). Если поток не ждет, а считает, он занимает поток ОС, и виртуалка тут не поможет.
🔥 Итог
Project Loom вернул нам девиз: "Один запрос - Один поток".
Больше никаких пулов потоков с ограниченным размером. Просто создавайте новый виртуальный поток для каждой задачи.
#Java #Loom #VirtualThreads #Concurrency #HighLoad
📲 Мы в MAX
👉@BookJavafinal).
Но что, если я хочу, чтобы мой класс Shape (Фигура) могли наследовать только Circle и Square, и больше никто?
Раньше это решалось костылями (пакетная видимость, скрытые конструкторы). В Java 17 появился официальный механизм: Sealed Classes.
🚧 Как это работает?
Вы помечаете класс (или интерфейс) ключевым словом sealed и после слова permits перечисляете тех, кому "можно".
// Родитель: Разрешает наследование ТОЛЬКО этим двум классам
public sealed interface Payment
permits CreditCard, Cash {
}
Теперь, если Вася из соседнего отдела попытается написать class Crypto extends Payment, компилятор ударит его по рукам. 🚫
🚦 Правило трех дорог
Наследники запечатанного класса обязаны выбрать свою судьбу. Они должны иметь один из трех модификаторов:
1. final - Цепочка наследования обрывается. Дальше наследовать нельзя.
public final class Cash implements Payment { ... }
2. sealed - Иерархия продолжается, но снова под контролем.
public sealed class CreditCard implements Payment permits Visa, MasterCard { ... }
3. non-sealed - "Открываем шлюзы". Дальше от этого класса может наследоваться кто угодно (возврат к старому поведению Java).
public non-sealed class DebitCard implements Payment { ... }
🧩 Главная фишка: Комбо со Switch
Зачем это нужно, кроме запретов? Ради безопасности.
Когда вы используете sealed иерархию в новом switch, компилятор знает все возможные варианты.
Вам больше не нужно писать ветку default!
// Метод обработки платежа
String process(Payment p) {
return switch (p) {
case CreditCard c -> "Processing card: " + c.getNumber();
case Cash c -> "Processing cash amount: " + c.getAmount();
// default НЕ НУЖЕН! Компилятор знает, что третьго не дано.
};
}
Если завтра вы добавите в permits новый класс Crypto, код перестанет компилироваться, пока вы не обработаете этот новый кейс в свитче.
Это спасает от багов, когда в бизнес-логику добавили новый тип, а обработчики обновить забыли.
🔥 Итог
Sealed Classes + Records + Switch Pattern Matching = 💎
Это "Святая Троица" современной Java. Используйте запечатанные классы для моделирования доменной области, где количество вариантов конечно и известно заранее (статусы заказа, типы платежей, виды пользователей).
#Java17 #SealedClasses #Architecture #CleanCode
📲 Мы в MAX
👉@BookJavaObject, и нужно понять, что внутри?
Раньше мы писали "лестницу" из if-else и instanceof с кучей ручного кастинга (приведения типов).
❌ Было (Боль и слезы):
Object obj = getUnknownObject();
if (obj instanceof String) {
String s = (String) obj; // Ручной каст
System.out.println("Строка: " + s.length());
} else if (obj instanceof Integer) {
Integer i = (Integer) obj; // Опять каст
System.out.println("Число: " + i);
} else if (obj instanceof Long) {
// ... и так до бесконечности
}
✅ Стало (Java 21 LTS):
Теперь switch умеет проверять типы! Больше никаких instanceof и ручных приведений. Переменная создается прямо в кейсе.
switch (obj) {
case String s -> System.out.println("Строка: " + s.length());
case Integer i -> System.out.println("Число: " + i);
case Long l -> System.out.println("Длинное число: " + l);
default -> System.out.println("Непонятно что");
}
🛡 Guarded Patterns (Охрана в кейсах)
Но это еще не всё. Часто бывает, что тип нам подходит, но нужно проверить еще и значение.
Раньше пришлось бы ставить if внутри case. Теперь у нас есть ключевое слово when.
switch (obj) {
// Попадет сюда, ТОЛЬКО если это строка И она длиннее 10 символов
case String s when s.length() > 10 ->
System.out.println("Длинная строка: " + s);
// Любая другая строка
case String s ->
System.out.println("Короткая строка: " + s);
case Integer i ->
System.out.println("Число: " + i);
default -> {}
}
👻 А как же Null?
В старом switch, если передать null, мы мгновенно получали NullPointerException.
В новом switch можно (и нужно!) обрабатывать null легально:
switch (obj) {
case String s -> System.out.println("Это строка");
case null -> System.out.println("Пришел null!"); // Никакого NPE
default -> System.out.println("Что-то другое");
}
⚡Это не просто "сахар". Это изменение подхода к полиморфизму.
Если раньше логика часто размазывалась по методам классов (animal.makeSound()), то теперь можно собирать логику обработки разных типов в одном месте, что часто бывает удобнее при написании бизнес-логики (например, обработка разных типов ивентов или DTO).
🔥 Итог
🔴Switch теперь принимает любые объекты.
🔴Кастинг происходит автоматически (case String s).
🔴Можно уточнять условия через when.
🔴Можно безопасно ловить null.
#PatternMatching #NewJava #CleanCode
📲 Мы в MAX
👉@BookJavabreak!
Помните это чувство, когда забыл написать break в switch-case, и код пошел выполняться дальше, создав неуловимый баг? 😫
В современных версиях Java (стандарт с Java 14) оператор switch прокачали. Теперь это не просто управляющая конструкция, а выражение, возвращающее результат.
💀 Было (Statement)
Громоздко, опасно (fall-through), переменная вынесена наружу.
DayOfWeek day = DayOfWeek.SATURDAY;
int numLetters;
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
numLetters = 6;
break; // Забыл break? Получи баг!
case TUESDAY:
numLetters = 7;
break;
default:
throw new IllegalStateException("Wat: " + day);
}
✅ Стало (Expression)
Лаконично, безопасно, функционально.
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
default -> throw new IllegalStateException("Wat: " + day);
};
🚀 Главные фишки
1. Стрелочный синтаксис (->)
Если используется стрелка, переход к следующему кейсу (fall-through) отключен по умолчанию. Никаких break больше писать не надо!
2. Возврат значения
switch теперь работает как формула. Вы можете сразу присвоить результат переменной или вернуть его из метода (return switch(...)).
3. Несколько условий в одну строку
case MONDAY, FRIDAY, SUNDAY — просто перечисляем через запятую.
🛑 Сложная логика и yield
Что делать, если в одном case нужно не просто вернуть число, а выполнить несколько строк кода (например, залоггировать)?
Для этого появились фигурные скобки и ключевое слово yield.
Важно: return использовать нельзя, так как он прервет выполнение всего метода, а не только свича.
String result = switch (day) {
case SATURDAY, SUNDAY -> "Выходной";
case MONDAY -> {
System.out.println("Тяжелый день...");
yield "Будни"; // yield возвращает значение из switch
}
default -> "Будни";
};
⚡ Exhaustiveness (Полнота)
Если вы делаете switch по Enum, компилятор проверит, все ли варианты вы обработали. Если добавите в Enum новый день, а в свич - нет, код просто не скомпилируется. Это отличная защита от забывчивости!
🔥 Итог
Новый switch это чистый кайф. Он делает код компактнее и убирает целый класс ошибок, связанных с пропущенными break.
🔴Используйте ->, когда нужно просто сопоставить значение.
🔴Используйте yield, если нужна логика внутри блока.
#Java #SwitchExpression #CleanCode #NewJava
📲 Мы в MAX
👉@BookJavaequals, hashCode, toString... 🤯
В Java 16+ этому положили конец. Встречайте Records.
📉 Было vs Стало
Допустим, нам нужен простой DTO для пользователя.
❌ Классический POJO (Java 1.0 - 15):
public class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
// + equals()
// + hashCode()
// + toString() ... еще 30 строк кода
}
✅ Record (Java 16+):
public record User(String name, int age) {}
Всё. Это одна строка. 🔥
⚙️ Что происходит под капотом?
Компилятор делает всю грязную работу за вас. Создавая record, вы автоматически получаете:
1. Приватные финальные поля (private final).
2. Конструктор со всеми аргументами.
3. Геттеры (без префикса get! Просто .name(), .age()).
4. equals() и hashCode() (идеально для ключей в Map или Set).
5. toString() (красивый вывод: User[name=Alex, age=25]).
🛠 Кастомизация (Compact Constructor)
"А что, если мне нужна валидация? Нельзя же создать юзера с отрицательным возрастом!"
Для этого есть Компактный конструктор. Вам даже не нужно перечислять аргументы:
public record User(String name, int age) {
// Компактный конструктор
public User {
if (age < 0) {
throw new IllegalArgumentException("Возраст не может быть меньше 0");
}
// Присваивание this.age = age происходит автоматически!
}
}
⚠️ Ограничения (Важно знать)
Records, это не замена обычным классам во всем.
1. Они неизменяемы (Immutable). Сеттеров нет и не будет. Хотите поменять поле? Создавайте новый объект.
2. Нет наследования. Record не может наследовать (extends) другой класс (потому что он уже наследует java.lang.Record). Но имплементировать интерфейсы (implements) можно!
💡 Когда использовать?
🔴DTO (Data Transfer Objects): Ответы от API, запросы в БД.
🔴Ключи для Map: Благодаря гарантированному equals/hashCode.
🔴Внутри методов: Можно объявлять локальные рекорды прямо внутри метода, чтобы временно сгруппировать данные.
Records делают код чище и безопаснее. Они идеально сочетаются со Stream API, где данные постоянно передаются и преобразуются.
Если ваш класс это просто "мешок с данными", превращайте его в record.
#Java #Records #NewJava #CleanCode
📲 Мы в MAX
👉@BookJavanull своей "ошибкой на миллиард долларов". NullPointerException (NPE) - самый частый кошмар Java-разработчика.
В Java 8 появился Optional<T> - класс-обертка, который явно говорит: "Здесь значения может и не быть".
📦 Что внутри?
Представьте Optional как коробку.
🔴В ней может лежать объект (Non-empty).
🔴Или она может быть пустой (Empty).
Главное правило: Никогда не возвращайте null из метода, если можно вернуть Optional.empty().
🚫 Как делать НЕ надо
Самая частая ошибка новичка, использовать Optional как старый добрый if (x != null):
Optional<User> userOpt = findUser("Alex");
// ❌ ПЛОХО: Это тот же null-check, только сложнее
if (userOpt.isPresent()) {
System.out.println(userOpt.get().getName());
}
Метод .get() - это зло. Если коробка пуста, он бросит NoSuchElementException, и вы просто поменяли шило (NPE) на мыло.
✅ Как делать НАДО (Functional Style)
Вся мощь Optional раскрывается, когда вы строите цепочки вызовов, как в стримах.
1. Если значение есть, сделай что-то (ifPresent)
findUser("Alex").ifPresent(user -> System.out.println(user.getName()));
2. Если значения нет, верни дефолт (orElse)
// Вернет юзера или создаст нового "Guest", если не нашел
User user = findUser("Alex").orElse(new User("Guest"));
3. Если значения нет — брось ошибку (orElseThrow)
User user = findUser("Alex")
.orElseThrow(() -> new IllegalArgumentException("User not found"));
4. Преобразование внутри коробки (map)
Допустим, нам нужен не сам юзер, а его email. Если юзера нет, то и email нет.
String email = findUser("Alex")
.map(User::getEmail) // Достаем email (если юзер есть)
.map(String::toUpperCase) // В верхний регистр (если email был)
.orElse("UNKNOWN"); // Если хоть на одном этапе было пусто
⚡ Золотые правила использования
1. Только для возвращаемых значений! Не используйте Optional как тип поля в классе или аргумент метода. Это лишний оверхед и мусор в коде.
2. orElse() vs orElseGet():
🔴orElse(new Object()) - объект создается всегда, даже если он не нужен.
🔴orElseGet(() -> new Object()) - объект создается только если в коробке пусто (лениво). Используйте этот вариант для тяжелых объектов.
🔥 Итог
Optional спасает не тем, что убирает null, а тем, что заставляет вас явно обработать случай отсутствия значения.
Забудьте про .get(). Используйте .map(), .filter() и .orElse().
#Java #Optional #CleanCode #NoMoreNPE
📲 Мы в MAX
👉@BookJava.collect().
Внутрь нее мы передаем специальный объект - Collector. Чтобы не писать его вручную, в Java есть утилитный класс Collectors с готовыми решениями на все случаи жизни.
📦 Базовый набор (Must Have)
1. В список или множество
💙Collectors.toList() - собирает все в ArrayList (но тип не гарантирован).
💙Collectors.toSet() - собирает в HashSet (убирает дубликаты).
💙Java 16+ Update: Теперь можно просто писать .toList() прямо у стрима, без collect(...). Это создает неизменяемый список.
2. В строку (joining)
💙Больше не нужно мучиться с StringBuilder в циклах.
💙Collectors.joining(", ") - склеит строки через запятую.
🔑 Продвинутый уровень: Карты и Группировки
Самая мощная магия происходит здесь.
3. В Map (toMap)
Превращает список объектов в Map (ключ-значение).
💙Синтаксис: toMap(Function keyMapper, Function valueMapper)
💙Важно: Если ключи совпадут, вылетит IllegalStateException.
💙Лечение: Третий аргумент - "мердж-функция" (что делать при конфликте).
4. Группировка (groupingBy)
Аналог GROUP BY из SQL. Это киллер-фича Stream API.
💙Разделяет элементы на группы по какому-то признаку и кладет их в Map<Критерий, Список>.
💻 Примеры в коде
Представьте, у нас есть класс User(String name, String role).
List<User> users = List.of(
new User("Alex", "ADMIN"),
new User("Bob", "USER"),
new User("Charlie", "USER")
);
// 1. Склеиваем имена в одну строку
String names = users.stream()
.map(User::getName)
.collect(Collectors.joining(", "));
// Результат: "Alex, Bob, Charlie"
// 2. Превращаем в Map: Имя -> Роль
Map<String, String> userMap = users.stream()
.collect(Collectors.toMap(
User::getName, // Ключ
User::getRole // Значение
));
// Результат: {Alex=ADMIN, Bob=USER, Charlie=USER}
// 3. Группируем по Роли (SQL GROUP BY)
Map<String, List<User>> byRole = users.stream()
.collect(Collectors.groupingBy(User::getRole));
// Результат:
// {
// "ADMIN": [User(Alex)],
// "USER": [User(Bob), User(Charlie)]
// }
⚡ Полезный лайфхак
В groupingBy можно передать второй коллектор! Например, чтобы не просто сгруппировать пользователей, а сразу посчитать их количество в каждой группе:
Map<String, Long> countByRole = users.stream()
.collect(Collectors.groupingBy(
User::getRole,
Collectors.counting() // Downstream collector
));
// Результат: {ADMIN=1, USER=2}
🔥 Итог
Collectors позволяют превратить поток данных в любую удобную структуру: от простого списка до сложной многоуровневой мапы.
#Java #StreamAPI #Collectors #JavaTips
📲 Мы в MAX
👉@BookJavaHashMap и Hashtable в Java?
Если вы работаете с Java, то наверняка сталкивались с HashMap и Hashtable. Оба используются для хранения пар "ключ-значение", но между ними есть важные различия. Давайте разберемся!
1. Синхронизация (Потокобезопасность)
- Hashtable:
- Синхронизирован (потокобезопасен). Все его методы синхронизированы, то есть только один поток может работать с ним одновременно.
- Это делает Hashtable безопасным для многопоточных сред, но может снижать производительность в однопоточных сценариях.
- HashMap:
- Не синхронизирован (не потокобезопасен). Несколько потоков могут обращаться к нему одновременно, что может привести к проблемам в многопоточных средах.
- Для потокобезопасности можно использовать Collections.synchronizedMap(new HashMap<>()) или ConcurrentHashMap.
2. Null-ключи и Null-значения
- Hashtable:
- Не позволяет использовать null в качестве ключа или значения. Попытка добавить null вызовет NullPointerException.
- HashMap:
- Разрешает один null - ключ и множество null -значений.
3. Производительность
- Hashtable:
- Медленнее из-за накладных расходов на синхронизацию.
- HashMap:
- Быстрее в однопоточных средах, так как не синхронизирован.
4. Наследие
- Hashtable:
- Считается устаревшим классом (появился в Java 1.0). Не является частью Java Collections Framework.
- HashMap:
- Часть Java Collections Framework (появился в Java 1.2). Более современный и широко используемый.
5. Итерация
- Hashtable:
- Использует Enumeration для перебора ключей и значений.
- HashMap:
- Использует Iterator, который более гибкий и позволяет удалять элементы во время перебора.
6. Наследование
- Hashtable:
- Наследуется от класса Dictionary (абстрактный класс, который сейчас считается устаревшим).
- HashMap:
- Наследуется от AbstractMap, который является частью Java Collections Framework.
7. Рекомендации по использованию
- Используйте HashMap, если:
- Работаете в однопоточной среде.
- Нужна высокая производительность.
- Требуется поддержка null-ключей или значений.
- Используйте Hashtable, если:
- Нужна потокобезопасность в многопоточной среде.
- Однако в современной Java ConcurrentHashMap предпочтительнее, так как он обеспечивает лучшую производительность и масштабируемость.
Пример кода
Hashtable:
Hashtable<String, Integer> hashtable = new Hashtable<>();
hashtable.put("one", 1);
hashtable.put("two", 2);
// hashtable.put(null, 3); // Выбросит NullPointerException
System.out.println(hashtable);
HashMap:
HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("one", 1);
hashMap.put("two", 2);
hashMap.put(null, 3); // Разрешено
System.out.println(hashMap);
Итоговая таблица
| Особенность | Hashtable | HashMap |
|-------------------------|---------------------------------|-------------------------------|
| Синхронизация | Синхронизирован | Не синхронизирован |
| Null-ключи/значения | Запрещены | Разрешены |
| Производительность | Медленнее | Быстрее |
| Наследие | Устаревший (Java 1.0) | Современный (Java 1.2) |
| Итерация | Enumeration | Iterator |
| Наследование | Наследует Dictionary | Наследует AbstractMap |
💡Совет: В современной разработке на Java HashMap используется чаще. Если нужна потокобезопасность, лучше выбрать ConcurrentHashMap, а не Hashtable.
📲 Мы в MAX
👉@BookJavamap() vs flatMap(): Битва трансформеров
Оба метода находятся в конвейере (Intermediate operations) и нужны для преобразования данных. Но работают они с разной геометрией.
1️⃣ map() - Один к одному (1:1)
Самый простой случай. Вы берете элемент, делаете с ним что-то и возвращаете один измененный элемент. Структура потока не меняется.
💙Логика:
💙Пример: Есть список сотрудников (Employee), нужно получить список их имен (String).
💙Аналогия: У вас есть коробка с яблоками. Вы берете каждое яблоко, чистите его и кладете обратно. В итоге у вас столько же объектов, просто они изменились.
stream.map(employee -> employee.getName()) // Был Employee, стал String
2️⃣ flatMap() - Один ко многим (1:N) + Сплющивание
Этот метод нужен, когда элементы стрима - это контейнеры (списки, массивы), и вам нужно достать содержимое наружу, "сплющив" всё в один поток.
💙Логика:
💙Пример: Есть список отделов (Department), в каждом отделе - список сотрудников (List<Employee>). Вы хотите получить один общий список всех сотрудников компании.
💙Аналогия: У вас есть коробка, внутри которой лежат маленькие коробочки с конфетами.
💙Если сделать map, у вас будет поток коробочек.
💙Если сделать flatMap, вы высыпаете конфеты из всех коробочек в одну большую кучу.
💻 Код: Почувствуйте разницу
Допустим, у нас есть "Списков списков":
List<List<String>> nestedList = List.of(List.of("A", "B"), List.of("C", "D"));
❌ Попытка с map:
// Мы получим Stream из Списков: Stream<List<String>>
nestedList.stream()
.map(list -> list.stream())
.toList();
// Результат: [[A, B], [C, D]] — Матрёшка осталась!
✅ Решение с flatMap:
// Мы получим единый Stream строк: Stream<String>
nestedList.stream()
.flatMap(list -> list.stream()) // Превращаем каждый список в стрим и сливаем
.toList();
// Результат: [A, B, C, D] — То, что нужно!
⚡ Когда что использовать?
1. map: Используем в 90% случаев. Когда нужно просто превратить А в Б (Число в Строку, Объект в Поле объекта).
2. flatMap: Используем, когда нужно убрать вложенность.
💙List<List<T>> List<T>
💙Работа с файлами (список строк из списка файлов).
💙Работа с Order List<LineItem>.
🔥 Итог
💙 map преобразует элементы.
💙flatMap преобразует и разворачивает структуру.
#Java #StreamAPI #MapVsFlatMap #InterviewQuestions
📲 Мы в MAX
👉@BookJavaКласс::метод (без скобок!).
🛠 4 ситуации, когда это работает
Есть 4 основных способа использовать оператор ::. Важно понимать разницу, чтобы не путаться.
1. Ссылка на статический метод
💙 Лямбда: s -> Integer.parseInt(s)
💙 Reference: Integer::parseInt
💙 Суть: Просто перенаправляем входящий параметр в статический метод.
2. Ссылка на метод конкретного объекта
💙 Лямбда: obj -> System.out.println(obj)
💙 Reference: System.out::println
💙 Суть: У нас уже есть готовый объект (System.out), и мы вызываем его метод для каждого элемента.
3. Ссылка на метод произвольного объекта определенного типа (Самый хитрый пункт! 🤯)
💙 Лямбда: s -> s.toLowerCase()
💙 Reference: String::toLowerCase
💙 Суть: Здесь метод вызывается у самого объекта, который пришел в лямбду. Хотя синтаксис похож на статический вызов, это вызов инстанс-метода.
4. Ссылка на конструктор
💙 Лямбда: () -> new ArrayList<>()
💙 Reference: ArrayList::new
💙 Суть: Используется для создания новых объектов (часто в Stream API: Collectors.toCollection(ArrayList::new)).
💻 Пример: Было vs Стало
Смотрите, как очищается код. Допустим, у нас есть список имен:
List<String> names = Arrays.asList("Alex", "Bob", "Anna");
// ❌ Уровень 1: Анонимный класс (Олдскул)
names.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
// ⚠️ Уровень 2: Обычная лямбда
names.forEach(s -> System.out.println(s));
// ✅ Уровень 3: Method Reference (Красота)
names.forEach(System.out::println);
🧠 Как понять, когда использовать?
Если ваша лямбда выглядит так:
x -> Class.method(x)
или
x -> x.method()
...смело меняйте её на ::.
Но! Если вам нужно изменить аргумент перед передачей (например, x -> System.out.println("Name: " + x)), то Method Reference уже не подойдет, оставайтесь на лямбде.
#Java #CleanCode #MethodReference
📲 Мы в MAX
👉@BookJavaPredicate, Consumer, Supplier, Function). Но что делать, если нужно принять два аргумента? Или если тип входа и выхода совпадает, и лень писать лишний код?
Для этого в Java есть Bi-версии и Операторы.
👯 Семейство «Bi» (Два аргумента)
Стандартные интерфейсы принимают только один параметр. Если вам нужно обработать пару значений (например, ключ и значение из Map), используйте приставку Bi.
1. BiPredicate <T, U>
💙 Метод: boolean test(T t, U u)
💙 Пример: Проверить, что длина строки T больше числа U.
2. BiConsumer <T, U>
💙 Метод: void accept(T t, U u)
💙 Пример: map.forEach((k, v) -> ...) - классический пример использования.
3. BiFunction <T, U, R>
💙 Метод: R apply(T t, U u)
💙 Пример: Сложить число T и число U, получить результат R.
🔄 Семейство «Operator» (Один и тот же тип)
Часто бывает, что вы преобразуете объект, не меняя его тип (String -> String, int -> int). Писать Function<String, String> - слишком длинно.
1. UnaryOperator <T>
💙 Наследник Function<T, T>.
💙 Пример: str -> str.toUpperCase() (принимает строку, возвращает строку).
2. BinaryOperator <T>
💙 Наследник BiFunction<T, T, T>.
💙 Пример: (a, b) -> a + b (два числа на вход, одно число на выход). Именно он используется в Stream.reduce.
⚡ Осторожно с боксингом!
Дженерики (<T>) работают только с объектами. Если вы используете Function<Integer, Integer> для математики, Java будет постоянно распаковывать и запаковывать int в Integer, что бьет по производительности.
Для примитивов есть свои спецназовцы:
💙 IntPredicate, LongConsumer, DoubleFunction и т.д.
💙 Правило: Если работаете с числами - всегда ищите примитивный аналог интерфейса.
💻 Пример в коде
import java.util.function.*;
public class AdvancedLambdas {
public static void main(String[] args) {
// 1. BinaryOperator: объединяем две строки
BinaryOperator<String> concat = (s1, s2) -> s1 + " " + s2;
System.out.println(concat.apply("Hello", "Java"));
// 2. BiPredicate: проверяем, начинается ли строка с префикса
BiPredicate<String, String> startsWith = (str, prefix) -> str.startsWith(prefix);
System.out.println(startsWith.test("Telegram", "Tele")); // true
// 3. IntUnaryOperator: работаем с примитивами без лишних объектов
IntUnaryOperator square = x -> x * x;
System.out.println(square.applyAsInt(5)); // 25
}
}
🔥 Итог
💙 Нужно 2 аргумента? Ищите Bi.
💙 Тип входа и выхода совпадает? Ищите Operator.
💙 Работаете с int/long/double? Ищите интерфейсы с префиксом типа.
#Java #AdvancedJava #FunctionalProgramming
📲 Мы в MAX
👉@BookJavadefault или static методов. Главное - только один абстрактный.
📝 Аннотация @FunctionalInterface
Ставить её над интерфейсом не обязательно, но хорошим тоном считается ставить.
Зачем? Она работает как защита от дурака: если вы или ваш коллега случайно добавите второй абстрактный метод в интерфейс, компилятор сразу выдаст ошибку, не дожидаясь падения кода в местах использования лямбд.
🧰 Шпаргалка: "Великолепная четверка"
В пакете java.util.function уже есть готовые интерфейсы на 99% случаев жизни. Не пишите свои велосипеды, пока не выучите эти:
1. Predicate <T>
💙Что делает: Проверяет условие.
💙 Метод: boolean test(T t)
💙 Где нужен: Фильтрация стримов (`filter`), проверки.
2. Consumer <T>
💙 Что делает: "Потребляет" объект, ничего не возвращая.
💙 Метод: void accept(T t)
💙 Где нужен: Вывод на экран, запись в БД, forEach.
3. Supplier <T>
💙 Что делает: "Поставляет" объект (из ниоткуда), ничего не принимая.
💙 Метод: T get()
💙 Где нужен: Ленивая инициализация, генерация значений, orElseGet.
4. Function <T, R>
💙 Что делает: Превращает объект типа T в объект типа R.
💙 Метод: R apply(T t)
💙 Где нужен: Преобразование данных, map в стримах.
💻 Пример в коде
@FunctionalInterface
interface Converter {
int stringToInt(String s);
}
public class Main {
public static void main(String[] args) {
// 1. Создаем реализацию через лямбду
Converter converter = str -> Integer.parseInt(str);
// 2. Использование стандартного Consumer
java.util.function.Consumer<Integer> print = x -> System.out.println("Result: " + x);
int result = converter.stringToInt("123");
print.accept(result); // Вывод: Result: 123
}
}
🔥 Итог
Функциональные интерфейсы - это контракт для лямбда-выражений. Используйте стандартные из java.util.function, чтобы ваш код был понятен другим разработчикам без лишних документаций.
#Java #Core #Lambda #FunctionalProgramming
📲 Мы в MAX
👉@BookJava