Многие недооценивают, насколько ArrayList может быть узким местом в производительности, особенно при работе с большими объёмами данных. Разберём, как оптимизировать его использование 👇
⚙️ Указывайте ёмкость при создании
По умолчанию:
List<String> list = new ArrayList<>();
ArrayList начинает с небольшой ёмкости (10 элементов) и каждый раз увеличивается в 1.5 раза, когда не хватает места. Это вызывает множество копирований массива, что замедляет работу при миллионах элементов.
List<String> list = new ArrayList<>(1_000_000);
Если вы знаете (или можете оценить) количество элементов заранее — выделите память сразу. Это уменьшает количество realocations и экономит до 30–40% времени при массовых вставках.
🧠 Очищайте, а не пересоздавайте
Многие делают так:
list = new ArrayList<>();
Это создаёт новый объект и выбрасывает старый в GC. При частых операциях — GC начинает тормозить систему.
list.clear();
Если список используется повторно, очистка быстрее и не требует новой аллокации памяти.
⚡️ Не используйте remove() в цикле
Удаление элементов в цикле вручную — частая и дорогая ошибка.
Плохо:
for (String s : list) {
if (s.startsWith("A")) list.remove(s);
}❌ Такой код приведёт к ConcurrentModificationException.
Даже если использовать Iterator, это безопасно, но медленно — каждый вызов remove() сдвигает все последующие элементы (O(n) на удаление).
list.removeIf(s -> s.startsWith("A"));Метод removeIf оптимизирован под внутренние операции и работает быстрее при массовом удалении.
* Кстати, у нас есть курс по Алгоритмам и структурам данных со скидкой.
Ставь → 🔥, если интересно почитать про внутреннюю реализацию стандартных методов коллекций.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥49👍9❤3
ConcurrentHashMap — это потокобезопасная реализация хэш-таблицы из пакета java.util.concurrent. В отличие от обычного HashMap, он допускает одновременные операции чтения и записи без глобальной блокировки всей таблицы.
📦 Базовая структура
Внутри ConcurrentHashMap хранит данные в массиве Node<K,V>[] table, где каждый элемент массива — это цепочка (связанный список или дерево).
Главная особенность:
— Вся таблица не блокируется целиком.
— Блокируется только нужный бакет при изменении.
— Для мелких структур используется synchronized на уровне ноды, для больших — tree bin locks (аналог TreeMap).
🔍 Как устроено хранение
— При вставке ключ хэшируется, чтобы равномерно распределить данные по бакетам.
— Каждая ячейка может содержать:
• Node — обычная запись (ключ/значение/ссылка).
• TreeBin — сбалансированное дерево при переполнении бакета (>8 элементов).
— Чтения (get) работают без блокировок, просто читают volatile-ссылки.
⚡️ Операции вставки и удаления
— put(K key, V value):
1. Высчитывается индекс бакета.
2. Если ячейка пустая, создаётся новая нода через CAS (Compare-And-Swap).
3. Если нет, блокируется только этот бакет (synchronized (f)), выполняется вставка.
— remove(Object key):
1. Определяется бакет.
2. Захватывается локальная блокировка на уровне бакета.
3. Узел удаляется, при необходимости структура перестраивается.
Сложность операций — O(1) в среднем случае, но с учётом блокировок.
🌊 Итераторы
Итераторы в ConcurrentHashMap — weakly consistent:
— Не выбрасывают ConcurrentModificationException.
— Видят часть изменений, сделанных другими потоками (в отличие от CopyOnWriteArrayList).
— Итерация не требует блокировок и не мешает параллельным вставкам или удалению.
📊 Производительность
— get() → практически O(1), без блокировок.
— put() / remove() → O(1) в среднем, но с локальной синхронизацией.
— Итерация → O(n), стабильная, но может не отражать все изменения.
⚖️ Важные нюансы
— С 8-й Java ConcurrentHashMap отказался от сегментов (Segment[]), теперь всё управляется атомарными операциями CAS и локальными синхронизациями.
— Не допускает null ключей и значений (в отличие от HashMap).
— Внутри используется LongAdder для счётчиков, чтобы избежать ложного sharing-а.
🧮 Когда использовать
— Часто читаемые и обновляемые словари (например, кэш, статистика, счётчики).
— Реализация пулов подключений, очередей задач, метрик и хранилищ состояний.
— Когда важно масштабирование под многоядерные системы без глобальных блокировок.
❗️ Не использовать, если:
— Обновления редки → Collections.synchronizedMap проще и дешевле.
— Нужен строгий порядок → лучше ConcurrentSkipListMap.
🔗 Документация: JavaDoc (Java 17)
Ставьте 🔥, если хотите разбор ConcurrentSkipListMap или LinkedBlockingQueue.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥21👍2👏1
В Java 8 появился CompletableFuture — это реализация паттерна Promise, которая позволяет строить декларативные цепочки асинхронных операций.
По сути, это обёртка над Future, которая может быть завершена вручную (отсюда "Completable") и предоставляет богатый API для композиции.
🔹 Зачем он нужен
Классический Future не позволяет:
— Комбинировать несколько асинхронных операций.
— Обрабатывать результат без блокировки.
— Реагировать на ошибки внутри цепочки.
CompletableFuture решает эти проблемы, предоставляя fluent API для композиции асинхронных вычислений.
🔹 Базовый пример
javaCompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> fetchUserFromDB(userId))
.thenApply(user -> user.getEmail())
.thenApply(String::toUpperCase)
.exceptionally(ex -> "default@example.com");
future.thenAccept(System.out::println); // не блокирует
Здесь каждый этап выполняется асинхронно. Если где-то произошла ошибка, сработает exceptionally().
🔹 Ключевые методы
▪️ supplyAsync() / runAsync() — запустить задачу асинхронно.
▪️ thenApply() — трансформировать результат.
▪️ thenAccept() — обработать результат (void).
▪️ thenCompose() — развернуть вложенный CompletableFuture.
▪️ thenCombine() — объединить результаты двух независимых future.
▪️ exceptionally() / handle() — обработка ошибок.
▪️ allOf() / anyOf() — дождаться завершения нескольких задач.
🔹 Пулы потоков
По умолчанию CompletableFuture использует ForkJoinPool.commonPool(). Для задач с блокирующими операциями (IO, БД) лучше передать свой Executor. Иначе можно заблокировать общий пул и замедлить всё приложение.
🔹 Подводные камни
— Отсутствие отмены
CompletableFuture.cancel() не останавливает выполнение задачи, а только меняет статус. Реальная отмена требует проверки Thread.interrupted() внутри задачи.
— Проглатывание исключений
Если не добавить exceptionally() или handle(), исключение останется внутри future до вызова get() или join().
— Цепочки могут выполняться синхронно
Методы без суффикса Async (например, thenApply) могут выполниться в том же потоке, где завершился предыдущий этап. Если нужна гарантия асинхронности, используйте thenApplyAsync().
— Для композиции нескольких асинхронных операций (API-вызовы, запросы в БД).
— Когда нужны неблокирующие обработчики результатов.
— В реактивных архитектурах (хотя там лучше Project Reactor или RxJava).
— Для CPU-bound задач с высокой конкуренцией (лучше использовать параллельные стримы или явное управление потоками).
— Когда важна отмена выполняющейся задачи.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
1👍9🔥3❤1
Когда проектируешь систему с email-уведомлениями, важно понимать не только Spring Boot Starter Mail, но и что происходит уровнем ниже.
SMTP — это то, на чём всё держится. И если знать его команды и коды ответов, можно быстрее находить проблемы: почему письмо ушло в спам, где потерялось, какой сервер отклонил.
Про сам протокол можно почитать подробнее тут.
А так это выглядит на пратике👇🏻
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.gmail.com");
props.put("mail.smtp.port", "587");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
Session session = Session.getInstance(props, new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress("from@example.com"));
message.setRecipients(Message.RecipientType.TO,
InternetAddress.parse("to@example.com"));
message.setSubject("Тема письма");
message.setText("Текст сообщения");
Transport.send(message); // Здесь и запускается SMTP-танец
@Service
public class EmailService {
@Autowired
private JavaMailSender mailSender;
public void sendEmail(String to, String subject, String text) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject(subject);
message.setText(text);
mailSender.send(message);
}
}
Зависимость:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
ИЛИ
implementation 'org.springframework.boot:spring-boot-starter-mail'
application.yml:
spring:
mail:
host: smtp.gmail.com
port: 587
username: your-email@gmail.com
password: your-app-password
properties:
mail:
smtp:
auth: true
starttls:
enable: true
🔹 starttls.enable — шифрование соединения (команда STARTTLS в SMTP)
🔹 mail.smtp.auth — аутентификация на сервере
🔹 Порты: 25 (обычный), 587 (TLS), 465 (SSL)
Когда понимаешь, что происходит на уровне протокола, легче дебажить: смотришь логи Transport, видишь SMTP-коды ответов (250 OK, 550 rejected и т.д.) и сразу понятно, где проблема.
#CoreJava
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7❤2🔥1