Раздел 8. Stream API и функциональный стиль в Java
Глава 4: Искусство агрегации. Коллекторы и группировки
Коллектор — это рецепт агрегации
Предыдущие главы посвящены преобразованиям потоков: фильтрации, отображению, уплощению. Но поток — эфемерная абстракция. Чтобы извлечь из него ценность, нужно свернуть его в конкретную структуру данных: список, множество, карту, число, строку. Эту свёртку выполняет Collector — центральный интерфейс терминальных операций агрегации.
Collector — не просто утилита для создания List. Это декларативный рецепт, описывающий, как создать контейнер для результатов, как наполнять его элементами, как объединять частичные результаты при параллельном выполнении, и как выполнить финальное преобразование. Понимание его компонентов раскрывает полную мощь Stream API.
Анатомия Collector
Интерфейс Collector<T, A, R> параметризован тремя типами: T — тип элементов входного потока, A — тип промежуточного контейнера аккумуляции, R — тип конечного результата.
Четыре функциональных компонента определяют поведение:
Supplier<A> — фабрика начального состояния. Вызывается один раз для каждого сегмента обработки (при параллельном выполнении — для каждого потока). Создаёт пустой контейнер, готовый к наполнению.
BiConsumer<A, T> accumulator — функция накопления. Добавляет элемент потока в контейнер. Вызывается для каждого элемента, не требует возврата значения — мутирует контейнер.
BinaryOperator<A> — функция слияния. Объединяет два контейнера в один при параллельном выполнении. Критична для корректности parallelStream: без неё частичные результаты останутся разрозненными.
Function<A, R> finisher — финальное преобразование. Превращает контейнер аккумуляции в конечный результат. Для toList() это тождественное преобразование (list -> list), но для сложных коллекторов может быть нетривиальным: например, collectingAndThen оборачивает результат в unmodifiable коллекцию.
Характеристики коллектора: оптимизационные подсказки
Третий параметр Collector — Set<Characteristics> — метаданные, влияющие на исполнение:
CONCURRENT — сигнализирует, что один контейнер может безопасно наполняться из нескольких потоков одновременно. При наличии этой характеристики фреймворк создаёт единственный контейнер и вызывает accumulator из множества потоков без предварительного разделения. Пример: Collector.of(ConcurrentHashMap::new, ..., ..., Characteristics.CONCURRENT).
UNORDERED — гарантия, что порядок элементов в результате не важен. Позволяет оптимизировать параллельное выполнение, устраняя накладные расходы на сохранение порядка при слиянии сегментов. toSet() имеет эту характеристику, toList() — нет.
IDENTITY_FINISH — утверждение, что finisher является тождественным преобразованием (A совпадает с R). Фреймворк может пропустить вызов finisher, возвращая контейнер напрямую. Это оптимизация, но также контракт: если объявить эту характеристику и предоставить нетривиальный finisher, получим ClassCastException при попытке привести A к R.
#Java #для_новичков #beginner #stream_api #Collectors #toMap
Глава 4: Искусство агрегации. Коллекторы и группировки
Коллектор — это рецепт агрегации
Предыдущие главы посвящены преобразованиям потоков: фильтрации, отображению, уплощению. Но поток — эфемерная абстракция. Чтобы извлечь из него ценность, нужно свернуть его в конкретную структуру данных: список, множество, карту, число, строку. Эту свёртку выполняет Collector — центральный интерфейс терминальных операций агрегации.
Collector — не просто утилита для создания List. Это декларативный рецепт, описывающий, как создать контейнер для результатов, как наполнять его элементами, как объединять частичные результаты при параллельном выполнении, и как выполнить финальное преобразование. Понимание его компонентов раскрывает полную мощь Stream API.
Анатомия Collector
Интерфейс Collector<T, A, R> параметризован тремя типами: T — тип элементов входного потока, A — тип промежуточного контейнера аккумуляции, R — тип конечного результата.
Четыре функциональных компонента определяют поведение:
Supplier<A> — фабрика начального состояния. Вызывается один раз для каждого сегмента обработки (при параллельном выполнении — для каждого потока). Создаёт пустой контейнер, готовый к наполнению.
// Для toList(): () -> new ArrayList<T>()
// Для toSet(): () -> new HashSet<T>()
// Для toMap(): () -> new HashMap<K, V>()
BiConsumer<A, T> accumulator — функция накопления. Добавляет элемент потока в контейнер. Вызывается для каждого элемента, не требует возврата значения — мутирует контейнер.
// Для toList(): (list, element) -> list.add(element)
// Для toMap(): (map, element) -> map.put(keyExtractor.apply(element), valueExtractor.apply(element))
BinaryOperator<A> — функция слияния. Объединяет два контейнера в один при параллельном выполнении. Критична для корректности parallelStream: без неё частичные результаты останутся разрозненными.
// Для toList(): (left, right) -> { left.addAll(right); return left; }
// Для toSet(): (left, right) -> { left.addAll(right); return left; }Function<A, R> finisher — финальное преобразование. Превращает контейнер аккумуляции в конечный результат. Для toList() это тождественное преобразование (list -> list), но для сложных коллекторов может быть нетривиальным: например, collectingAndThen оборачивает результат в unmodifiable коллекцию.
// collectingAndThen(toList(), Collections::unmodifiableList)
// finisher: list -> Collections.unmodifiableList(list)
Характеристики коллектора: оптимизационные подсказки
Третий параметр Collector — Set<Characteristics> — метаданные, влияющие на исполнение:
CONCURRENT — сигнализирует, что один контейнер может безопасно наполняться из нескольких потоков одновременно. При наличии этой характеристики фреймворк создаёт единственный контейнер и вызывает accumulator из множества потоков без предварительного разделения. Пример: Collector.of(ConcurrentHashMap::new, ..., ..., Characteristics.CONCURRENT).
UNORDERED — гарантия, что порядок элементов в результате не важен. Позволяет оптимизировать параллельное выполнение, устраняя накладные расходы на сохранение порядка при слиянии сегментов. toSet() имеет эту характеристику, toList() — нет.
IDENTITY_FINISH — утверждение, что finisher является тождественным преобразованием (A совпадает с R). Фреймворк может пропустить вызов finisher, возвращая контейнер напрямую. Это оптимизация, но также контракт: если объявить эту характеристику и предоставить нетривиальный finisher, получим ClassCastException при попытке привести A к R.
#Java #для_новичков #beginner #stream_api #Collectors #toMap
👍4
Конструирование собственного коллектора
Понимание компонентов позволяет создавать специализированные коллекторы. Рассмотрим задачу: собрать строки в единую строку с ограничением длины, добавляя "[truncated]" при превышении.
Этот коллектор демонстрирует все компоненты: мутабельный контейнер (StringBuilderWithFlag), накопление с условной логикой, сложное слияние при параллелизме, финальное преобразование. Характеристика UNORDERED разрешает оптимизации, но требует корректной реализации combiner для любого порядка элементов.
toMap: мощь и ловушки
Коллектор toMap — один из наиболее полезных и опасных. Он превращает поток пар ключ-значение в Map, но содержит скрытые предположения, нарушение которых ведёт к исключениям.
Ловушка первая: коллизии ключей
При отсутствии функции разрешения коллизий toMap бросает IllegalStateException, если встречает дубликат ключа:
Решение — явная функция разрешения (oldValue, newValue) -> ...:
Последний паттерн настолько распространён, что имеет специализированную реализацию: groupingBy(Book::author, toList()).
#Java #для_новичков #beginner #stream_api #Collectors #toMap
Понимание компонентов позволяет создавать специализированные коллекторы. Рассмотрим задачу: собрать строки в единую строку с ограничением длины, добавляя "[truncated]" при превышении.
public static Collector<String, ?, String> joiningWithLimit(int maxLength) {
class StringBuilderWithFlag {
StringBuilder builder = new StringBuilder();
boolean truncated = false;
}
return Collector.of(
StringBuilderWithFlag::new, // supplier
(container, str) -> { // accumulator
if (container.truncated) return;
if (container.builder.length() + str.length() > maxLength) {
container.builder.append("[truncated]");
container.truncated = true;
} else {
if (container.builder.length() > 0) container.builder.append(", ");
container.builder.append(str);
}
},
(left, right) -> { // combiner
if (left.truncated) return left;
if (right.truncated) {
left.builder.append("[truncated]");
left.truncated = true;
return left;
}
if (left.builder.length() + right.builder.length() > maxLength) {
left.builder.append(", ").append(right.builder).append("[truncated]");
left.truncated = true;
} else {
if (left.builder.length() > 0) left.builder.append(", ");
left.builder.append(right.builder);
}
return left;
},
container -> container.builder.toString(), // finisher
Characteristics.UNORDERED // порядок не важен
);
}
// Использование
String summary = tags.stream()
.collect(joiningWithLimit(100));Этот коллектор демонстрирует все компоненты: мутабельный контейнер (StringBuilderWithFlag), накопление с условной логикой, сложное слияние при параллелизме, финальное преобразование. Характеристика UNORDERED разрешает оптимизации, но требует корректной реализации combiner для любого порядка элементов.
toMap: мощь и ловушки
Коллектор toMap — один из наиболее полезных и опасных. Он превращает поток пар ключ-значение в Map, но содержит скрытые предположения, нарушение которых ведёт к исключениям.
Ловушка первая: коллизии ключей
При отсутствии функции разрешения коллизий toMap бросает IllegalStateException, если встречает дубликат ключа:
// Опасно: бросит исключение при повторяющемся авторе
Map<String, Book> bookByAuthor = books.stream()
.collect(toMap(Book::author, book -> book)); // Crash на второй книге того же автора
Решение — явная функция разрешения (oldValue, newValue) -> ...:
// Сохраняем первую встреченную книгу автора
Map<String, Book> firstByAuthor = books.stream()
.collect(toMap(
Book::author,
book -> book,
(existing, replacement) -> existing // Игнорируем повторы
));
// Сохраняем последнюю, обновляем
Map<String, Book> lastByAuthor = books.stream()
.collect(toMap(
Book::author,
book -> book,
(oldBook, newBook) -> newBook // Перезаписываем
));
// Агрегируем: список всех книг автора
Map<String, List<Book>> allByAuthor = books.stream()
.collect(toMap(
Book::author,
book -> new ArrayList<>(List.of(book)), // Создаём список из одной книги
(existingList, newList) -> {
existingList.addAll(newList); // Добавляем к существующему
return existingList;
}
));
Последний паттерн настолько распространён, что имеет специализированную реализацию: groupingBy(Book::author, toList()).
#Java #для_новичков #beginner #stream_api #Collectors #toMap
👍3🔥2
Ловушка вторая: null-значения
Стандартные реализации Map в Java (HashMap, TreeMap) не поддерживают null в качестве ключа или значения при определённых операциях, а toMap внутри использует Map.merge, который отклоняет null-значения выбросом NullPointerException.
Решения: фильтрация null до коллектора, обёртка в Optional, использование специального значения-заполнителя:
Ловушка третья: изменяемые ключи
Если ключом Map становится объект, чей hashCode или equals зависят от изменяемого состояния, целостность Map разрушается при модификации ключа после вставки. Это не специфика toMap, но частая ошибка при потоковой агрегации:
Правило: ключи в toMap должны быть неизменяемыми (immutable), с стабильными hashCode и equals.
Композиция коллекторов: building blocks
Коллекторы проектируются для композиции.
Collectors предоставляет адаптеры, оборачивающие базовые коллекторы в более сложные:
collectingAndThen — применяет функцию к результату коллектора:
filtering — предварительная фильтрация перед коллектором (Java 9+):
mapping — трансформация элемента перед передачей downstream коллектору:
Эти примитивы композиции позволяют строить сложные агрегации без явного создания собственных коллекторов, сохраняя декларативность и читаемость.
#Java #для_новичков #beginner #stream_api #Collectors #toMap
Стандартные реализации Map в Java (HashMap, TreeMap) не поддерживают null в качестве ключа или значения при определённых операциях, а toMap внутри использует Map.merge, который отклоняет null-значения выбросом NullPointerException.
// Опасно: NPE если book.getDescription() возвращает null
Map<String, String> descriptions = books.stream()
.collect(toMap(Book::title, Book::description)); // Crash на null
Решения: фильтрация null до коллектора, обёртка в Optional, использование специального значения-заполнителя:
// Фильтрация
Map<String, String> validDescriptions = books.stream()
.filter(b -> b.description() != null)
.collect(toMap(Book::title, Book::description));
// Обёртка в Optional (требует адаптации типа)
Map<String, Optional<String>> optionalDescriptions = books.stream()
.collect(toMap(
Book::title,
b -> Optional.ofNullable(b.description())
));
// Заполнитель с последующей фильтрацией
String NULL_MARKER = "\u0000";
Map<String, String> markedDescriptions = books.stream()
.collect(toMap(
Book::title,
b -> b.description() != null ? b.description() : NULL_MARKER
));
// При использовании: if (!value.equals(NULL_MARKER))
Ловушка третья: изменяемые ключи
Если ключом Map становится объект, чей hashCode или equals зависят от изменяемого состояния, целостность Map разрушается при модификации ключа после вставки. Это не специфика toMap, но частая ошибка при потоковой агрегации:
// Опасно: ключ — изменяемый объект
record MutableKey(String name) {
public void setName(String name) { this.name = name; } // Mutable!
}
Map<MutableKey, Book> map = books.stream()
.collect(toMap(book -> new MutableKey(book.author()), book -> book));
// Последующая модификация ключа делает Map некорректным
map.keySet().iterator().next().setName("Changed"); // Неопределённое поведение
Правило: ключи в toMap должны быть неизменяемыми (immutable), с стабильными hashCode и equals.
Композиция коллекторов: building blocks
Коллекторы проектируются для композиции.
Collectors предоставляет адаптеры, оборачивающие базовые коллекторы в более сложные:
collectingAndThen — применяет функцию к результату коллектора:
// Немодифицируемый список
List<Book> unmodifiable = books.stream()
.collect(collectingAndThen(toList(), Collections::unmodifiableList));
// Строка из списка с префиксом и суффиксом
String joined = books.stream()
.map(Book::title)
.collect(collectingAndThen(
joining(", "),
str -> "Titles: " + str + "."
));
filtering — предварительная фильтрация перед коллектором (Java 9+):
// Группировка только ненулевых описаний по жанру
Map<Genre, List<String>> descriptionsByGenre = books.stream()
.collect(groupingBy(
Book::genre,
filtering(
b -> b.description() != null,
mapping(Book::description, toList())
)
));
mapping — трансформация элемента перед передачей downstream коллектору:
// Средняя цена по авторам (автор -> средняя цена его книг)
Map<String, Double> avgPriceByAuthor = books.stream()
.collect(groupingBy(
Book::author,
mapping(Book::price, averagingDouble(BigDecimal::doubleValue))
));
Эти примитивы композиции позволяют строить сложные агрегации без явного создания собственных коллекторов, сохраняя декларативность и читаемость.
#Java #для_новичков #beginner #stream_api #Collectors #toMap
👍5
Что выведет код?
#Tasks
import java.util.*;
import java.util.stream.*;
public class Task020326 {
public static void main(String[] args) {
List<String> items = Arrays.asList("apple", "banana", "apricot", "cherry");
Map<Character, String> map = items.stream()
.collect(Collectors.toMap(
s -> s.charAt(0),
String::toUpperCase,
(v1, v2) -> v1 + "&" + v2
));
System.out.println(map.get('a'));
}
}
#Tasks
👍3
👍2🆒1
В чем разница между Fail-Fast и Fail-Safe итераторами? 🤓
Ответ:
Fail-Fast итераторы (например, в ArrayList, HashMap) при обнаружении изменений в структуре коллекции во время итерации (кроме методов самого итератора) немедленно выбрасывают ConcurrentModificationException.
Они работают быстро, но небезопасны в многопоточной среде.
Fail-Safe итераторы (например, в ConcurrentHashMap, CopyOnWriteArrayList) работают с копией структуры данных или снэпшотом на момент создания итератора.
Поэтому они не выбрасывают исключений при изменениях, но не видят этих изменений и потребляют больше памяти.
#собеседование
Ответ:
Они работают быстро, но небезопасны в многопоточной среде.
Fail-Safe итераторы (например, в ConcurrentHashMap, CopyOnWriteArrayList) работают с копией структуры данных или снэпшотом на момент создания итератора.
Поэтому они не выбрасывают исключений при изменениях, но не видят этих изменений и потребляют больше памяти.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥4👍2
История технологий сегодня — 03 марта
ℹ️ Кто родился в этот день
Влади́мир Ио́сифович Ве́кслер (19 февраля [4 марта] 1907, Житомир, Волынская губерния, Российская империя — 22 сентября 1966, Москва, СССР) — советский физик-экспериментатор, профессор. Основоположник ускорительной техники в СССР, создатель синхрофазотрона ОИЯИ.
Гео́рг Фе́рдинанд Лю́двиг Фи́липп Ка́нтор (нем. Georg Ferdinand Ludwig Philipp Cantor, 3 марта 1845, Санкт-Петербург — 6 января 1918, Галле (Заале)) — немецкий математик, ученик Карла Вейерштрасса. Наиболее известен как создатель теории множеств. Кантор впервые определил сравнение произвольных множеств, включая бесконечные, по их «мощности» (обобщению понятия количества) через понятие взаимно-однозначного соответствия между множествами. Он классифицировал множества по их мощности, определил понятия кардинальных и порядковых чисел, арифметику кардинальных и порядковых чисел.
Алекса́ндр Гре́йам Белл (англ. Alexander Graham Bell; 3 марта 1847, Эдинбург, Шотландия — 2 августа 1922, Баддек, провинция Новая Шотландия, Канада) — американский и канадский учёный, изобретатель и предприниматель шотландского происхождения, один из основоположников телефонии, основатель компании «American Telephone and Telegraph Company» («AT&T»), определившей всё дальнейшее развитие телекоммуникационной отрасли в США.
🌐 Знаковые события
1987 — Steve Wilhite разрабатывает GIF (Graphics Interchange Format) в CompuServe. Первый популярный формат анимированных изображений для интернета; его алгоритм LZW‑сжатия стал стандартом для веба, мемов и ранней графики.
#Biography #Birth_Date #Events #03марта
Влади́мир Ио́сифович Ве́кслер (19 февраля [4 марта] 1907, Житомир, Волынская губерния, Российская империя — 22 сентября 1966, Москва, СССР) — советский физик-экспериментатор, профессор. Основоположник ускорительной техники в СССР, создатель синхрофазотрона ОИЯИ.
Гео́рг Фе́рдинанд Лю́двиг Фи́липп Ка́нтор (нем. Georg Ferdinand Ludwig Philipp Cantor, 3 марта 1845, Санкт-Петербург — 6 января 1918, Галле (Заале)) — немецкий математик, ученик Карла Вейерштрасса. Наиболее известен как создатель теории множеств. Кантор впервые определил сравнение произвольных множеств, включая бесконечные, по их «мощности» (обобщению понятия количества) через понятие взаимно-однозначного соответствия между множествами. Он классифицировал множества по их мощности, определил понятия кардинальных и порядковых чисел, арифметику кардинальных и порядковых чисел.
Алекса́ндр Гре́йам Белл (англ. Alexander Graham Bell; 3 марта 1847, Эдинбург, Шотландия — 2 августа 1922, Баддек, провинция Новая Шотландия, Канада) — американский и канадский учёный, изобретатель и предприниматель шотландского происхождения, один из основоположников телефонии, основатель компании «American Telephone and Telegraph Company» («AT&T»), определившей всё дальнейшее развитие телекоммуникационной отрасли в США.
1987 — Steve Wilhite разрабатывает GIF (Graphics Interchange Format) в CompuServe. Первый популярный формат анимированных изображений для интернета; его алгоритм LZW‑сжатия стал стандартом для веба, мемов и ранней графики.
#Biography #Birth_Date #Events #03марта
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
[Совет по Java #011]
Тема: Random — потоконебезопасный и медленный для многопоточки. Используйте ThreadLocalRandom для параллельных потоков.
Проблема: Класс java.util.Random в целом является потокобезопасным, но его безопасность достигается за счет внутренней синхронизации (атомарного обновления seed'а через CAS-операции в современных реализациях).
В высококонкурентной среде это создает конкуренцию за общий ресурс — несколько потоков пытаются обновить одно и то же состояние, что приводит к кеш-промахам, простоям и значительному падению производительности. Кроме того, использование одного экземпляра Random несколькими потоками может привести к предсказуемости генерации (если seed обновляется неатомарно в старых версиях) или просто к узкому месту.
Решение: Для многопоточных сценариев предназначен класс java.util.concurrent.ThreadLocalRandom.
Он использует технику ThreadLocal: каждый поток получает свой собственный экземпляр генератора с собственным seed'ом, что полностью устраняет конкуренцию. Метод ThreadLocalRandom.current() возвращает генератор, привязанный к текущему потоку, без создания новых объектов при каждом вызове.
Это не только потокобезопасно, но и значительно быстрее в многопоточной среде (ускорение может достигать десятков раз).
Объяснение: ThreadLocalRandom использует более быстрый алгоритм генерации (на основе смешивания) по сравнению с классическим Random.
Важно вызывать именно ThreadLocalRandom.current() при каждой операции генерации — это дешевый вызов, возвращающий ссылку на генератор текущего потока, а не создающий новый объект. Для однопоточных сценариев разница между Random и ThreadLocalRandom незначительна, но в многопоточных ThreadLocalRandom всегда предпочтительнее.
В Java 8+ появился также класс SplittableRandom для параллельных stream'ов и fork/join пулов, который еще быстрее, если задача может быть разделена на независимые подзадачи.
#Java #советы
Тема: Random — потоконебезопасный и медленный для многопоточки. Используйте ThreadLocalRandom для параллельных потоков.
Проблема: Класс java.util.Random в целом является потокобезопасным, но его безопасность достигается за счет внутренней синхронизации (атомарного обновления seed'а через CAS-операции в современных реализациях).
В высококонкурентной среде это создает конкуренцию за общий ресурс — несколько потоков пытаются обновить одно и то же состояние, что приводит к кеш-промахам, простоям и значительному падению производительности. Кроме того, использование одного экземпляра Random несколькими потоками может привести к предсказуемости генерации (если seed обновляется неатомарно в старых версиях) или просто к узкому месту.
Решение: Для многопоточных сценариев предназначен класс java.util.concurrent.ThreadLocalRandom.
Он использует технику ThreadLocal: каждый поток получает свой собственный экземпляр генератора с собственным seed'ом, что полностью устраняет конкуренцию. Метод ThreadLocalRandom.current() возвращает генератор, привязанный к текущему потоку, без создания новых объектов при каждом вызове.
Это не только потокобезопасно, но и значительно быстрее в многопоточной среде (ускорение может достигать десятков раз).
import java.util.*;
import java.util.concurrent.*;
public class RandomExample {
private static final Random SHARED_RANDOM = new Random();
//Антипаттерн: общий Random в многопоточке
public static class BadTask implements Callable<Integer> {
@Override
public Integer call() {
// Конкуренция за общий Random
return SHARED_RANDOM.nextInt(100);
}
}
//Правильно: ThreadLocalRandom
public static class GoodTask implements Callable<Integer> {
@Override
public Integer call() {
// Каждый поток использует свой генератор
return ThreadLocalRandom.current().nextInt(100);
}
}
//Антипаттерн: создание Random в каждом потоке
public static class WastefulTask implements Callable<Integer> {
@Override
public Integer call() {
// Новый объект при каждом вызове — лишняя аллокация
return new Random().nextInt(100);
}
}
// Демонстрация производительности
public static void main(String[] args) throws Exception {
int threads = 10;
int tasksPerThread = 100000;
ExecutorService executor = Executors.newFixedThreadPool(threads);
// Плохой вариант
long start = System.nanoTime();
List<Callable<Integer>> badTasks = Collections.nCopies(threads * tasksPerThread, new BadTask());
executor.invokeAll(badTasks);
long end = System.nanoTime();
System.out.println("Shared Random: " + TimeUnit.NANOSECONDS.toMillis(end - start) + " ms");
// Хороший вариант
start = System.nanoTime();
List<Callable<Integer>> goodTasks = Collections.nCopies(threads * tasksPerThread, new GoodTask());
executor.invokeAll(goodTasks);
end = System.nanoTime();
System.out.println("ThreadLocalRandom: " + TimeUnit.NANOSECONDS.toMillis(end - start) + " ms");
executor.shutdown();
}
}
Объяснение: ThreadLocalRandom использует более быстрый алгоритм генерации (на основе смешивания) по сравнению с классическим Random.
Важно вызывать именно ThreadLocalRandom.current() при каждой операции генерации — это дешевый вызов, возвращающий ссылку на генератор текущего потока, а не создающий новый объект. Для однопоточных сценариев разница между Random и ThreadLocalRandom незначительна, но в многопоточных ThreadLocalRandom всегда предпочтительнее.
В Java 8+ появился также класс SplittableRandom для параллельных stream'ов и fork/join пулов, который еще быстрее, если задача может быть разделена на независимые подзадачи.
#Java #советы
🔥4👍2
Что выведет код?
#Tasks
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Task030326 {
public static void main(String[] args) throws InterruptedException {
Random random = new Random();
int[] results = new int[1000];
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
final int index = i;
executor.submit(() -> {
results[index] = random.nextInt(100);
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
long nonZeroCount = Arrays.stream(results).filter(x -> x != 0).count();
System.out.println(nonZeroCount);
}
}
#Tasks
👍3
👍2
Что такое wildcard (<?>) в Generics? 🤓
Ответ:
Wildcards (символы подстановки) в Generics — это знак ?, обозначающий неизвестный тип.
Используются для большей гибкости: ? extends T (upper bounded) — любой тип, являющийся подтипом T (можно читать объекты как T).
? super T (lower bounded) — любой тип, являющийся супертипом T (можно добавлять объекты типа T). ? (unbounded) — любой тип.
PECS (Producer Extends, Consumer Super) — правило, помогающее запомнить, когда какой использовать.
#собеседование
Ответ:
Используются для большей гибкости: ? extends T (upper bounded) — любой тип, являющийся подтипом T (можно читать объекты как T).
? super T (lower bounded) — любой тип, являющийся супертипом T (можно добавлять объекты типа T). ? (unbounded) — любой тип.
PECS (Producer Extends, Consumer Super) — правило, помогающее запомнить, когда какой использовать.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
История технологий сегодня — 04 марта
ℹ️ Кто родился в этот день
Джеффри («Джефф») Колин Тутилл (4 марта 1922 г. – 26 октября 2017 г.) — английский компьютерный учёный который был инженером-электронщиком и специалистом по информатике, работавшим на кафедре электротехники Манчестерского университета с Фредди Уильямсом и Томом Килберном над разработкой Manchester Baby, «первого в мире полностью электронного компьютера с хранимой программой».
Гео́ргий Анто́нович Га́мов (также известен как Джордж Гамов, англ. George Gamow; 20 февраля (4 марта) 1904 года, Одесса — 19 августа 1968, Боулдер) — советский и американский физик-теоретик, астрофизик и популяризатор науки. Гамов известен своими работами по квантовой механике, атомной и ядерной физике, астрофизике, космологии, биологии, создатель уравнения, объясняющего теорию туннельного эффекта. Он является автором первой количественной теории альфа-распада, одним из основоположников теории «горячей Вселенной» и одним из пионеров применения ядерной физики к вопросам эволюции звёзд. Он впервые чётко сформулировал проблему генетического кода. Широкую известность Гамову принесли его научно-популярные произведения, в которых живым и доступным языком рассказывается о современных научных представлениях.
🌐 Знаковые события
1977 — в Лос-Аламосе установлен суперкомпьютер Cray-1.
#Biography #Birth_Date #Events #04марта
Джеффри («Джефф») Колин Тутилл (4 марта 1922 г. – 26 октября 2017 г.) — английский компьютерный учёный который был инженером-электронщиком и специалистом по информатике, работавшим на кафедре электротехники Манчестерского университета с Фредди Уильямсом и Томом Килберном над разработкой Manchester Baby, «первого в мире полностью электронного компьютера с хранимой программой».
Гео́ргий Анто́нович Га́мов (также известен как Джордж Гамов, англ. George Gamow; 20 февраля (4 марта) 1904 года, Одесса — 19 августа 1968, Боулдер) — советский и американский физик-теоретик, астрофизик и популяризатор науки. Гамов известен своими работами по квантовой механике, атомной и ядерной физике, астрофизике, космологии, биологии, создатель уравнения, объясняющего теорию туннельного эффекта. Он является автором первой количественной теории альфа-распада, одним из основоположников теории «горячей Вселенной» и одним из пионеров применения ядерной физики к вопросам эволюции звёзд. Он впервые чётко сформулировал проблему генетического кода. Широкую известность Гамову принесли его научно-популярные произведения, в которых живым и доступным языком рассказывается о современных научных представлениях.
1977 — в Лос-Аламосе установлен суперкомпьютер Cray-1.
#Biography #Birth_Date #Events #04марта
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
Раздел 8. Stream API и функциональный стиль в Java
Глава 4: Искусство агрегации. Коллекторы и группировки
groupingBy и многоуровневая агрегация
Коллектор groupingBy — один из наиболее выразительных инструментов Stream API. Он трансформирует поток элементов в Map, где ключи — значения классификатора, а значения — списки элементов, соответствующих каждому ключу. Это операция GROUP BY из SQL, перенесённая в мир коллекций, но с большей гибкостью благодаря композиции downstream коллекторов.
Простейшая форма группировки создаёт Map<K, List<T>>:
Здесь Book::author — функция классификации, извлекающая ключ. Результат: ассоциативный массив, где каждому автору соответствует список его книг. Порядок книг в списках сохраняет порядок встречи в исходном потоке (если поток ORDERED).
Downstream коллекторы: агрегация внутри группы
Вторая перегрузка groupingBy принимает downstream коллектор — рецепт обработки элементов каждой группы. Это позволяет заменить список на любую другую свёртку: счётчик, множество, статистику, опциональный экстремум.
Подсчёт элементов:
counting() — коллектор, возвращающий Long с количеством элементов. Результат не хранит сами книги, только их число по авторам.
Трансформация перед агрегацией:
mapping — адаптер коллектора, применяющий функцию к каждому элементу перед передачей downstream коллектору. Здесь Book превращается в String (название), и результаты собираются в список. Результат: автор → список названий, без промежуточных объектов Book.
Поиск экстремума:
maxBy возвращает Optional<T>, потому что группа может быть пустой (теоретически, при фильтрации до группировки). comparing(Book::year) создаёт компаратор для извлечения максимума. Для получения "самой старой книги" используется minBy или comparing(Book::year).reversed().
Обработка Optional в результате требует осторожности:
```
// Извлечение значения с дефолтом
Map<String, Book> newestOrNullByAuthor = library.stream()
.collect(groupingBy(
Book::author,
collectingAndThen(
maxBy(comparing(Book::year)),
opt -> opt.orElse(null) // или orElseThrow, orElseGet
)
));
collectingAndThen оборачивает downstream коллектор, применяя финишную функцию к его результату. Здесь Optional<Book> превращается в Book или null.
#Java #для_новичков #beginner #stream_api #Collectors #groupingBy
Глава 4: Искусство агрегации. Коллекторы и группировки
groupingBy и многоуровневая агрегация
Коллектор groupingBy — один из наиболее выразительных инструментов Stream API. Он трансформирует поток элементов в Map, где ключи — значения классификатора, а значения — списки элементов, соответствующих каждому ключу. Это операция GROUP BY из SQL, перенесённая в мир коллекций, но с большей гибкостью благодаря композиции downstream коллекторов.
Простейшая форма группировки создаёт Map<K, List<T>>:
Map<String, List<Book>> booksByAuthor = library.stream()
.collect(groupingBy(Book::author));
Здесь Book::author — функция классификации, извлекающая ключ. Результат: ассоциативный массив, где каждому автору соответствует список его книг. Порядок книг в списках сохраняет порядок встречи в исходном потоке (если поток ORDERED).
Downstream коллекторы: агрегация внутри группы
Вторая перегрузка groupingBy принимает downstream коллектор — рецепт обработки элементов каждой группы. Это позволяет заменить список на любую другую свёртку: счётчик, множество, статистику, опциональный экстремум.
Подсчёт элементов:
Map<String, Long> bookCountByAuthor = library.stream()
.collect(groupingBy(Book::author, counting()));
counting() — коллектор, возвращающий Long с количеством элементов. Результат не хранит сами книги, только их число по авторам.
Трансформация перед агрегацией:
Map<String, List<String>> titlesByAuthor = library.stream()
.collect(groupingBy(
Book::author,
mapping(Book::title, toList())
));
mapping — адаптер коллектора, применяющий функцию к каждому элементу перед передачей downstream коллектору. Здесь Book превращается в String (название), и результаты собираются в список. Результат: автор → список названий, без промежуточных объектов Book.
Поиск экстремума:
Map<String, Optional<Book>> newestByAuthor = library.stream()
.collect(groupingBy(
Book::author,
maxBy(comparing(Book::year))
));
maxBy возвращает Optional<T>, потому что группа может быть пустой (теоретически, при фильтрации до группировки). comparing(Book::year) создаёт компаратор для извлечения максимума. Для получения "самой старой книги" используется minBy или comparing(Book::year).reversed().
Обработка Optional в результате требует осторожности:
```
// Извлечение значения с дефолтом
Map<String, Book> newestOrNullByAuthor = library.stream()
.collect(groupingBy(
Book::author,
collectingAndThen(
maxBy(comparing(Book::year)),
opt -> opt.orElse(null) // или orElseThrow, orElseGet
)
));
collectingAndThen оборачивает downstream коллектор, применяя финишную функцию к его результату. Здесь Optional<Book> превращается в Book или null.
#Java #для_новичков #beginner #stream_api #Collectors #groupingBy
👍5
Многоуровневая группировка: вложенные Map
groupingBy можно вкладывать сам в себя, создавая иерархические структуры Map<K1, Map<K2, V>>:
Читаемость страдает от глубокой вложенности, но структура данных точно отражает бизнес-логику: жанр содержит авторов, автор содержит книги.
Альтернатива — составной ключ:
Выбор между вложенными Map и составными ключами зависит от паттерна доступа: если часто нужны все книги жанра независимо от автора, вложенность предпочтительнее; если доступ всегда по паре жанр-автор, составной ключ проще.
Сложные downstream агрегации
Комбинация адаптеров коллекторов позволяет строить изощрённые агрегации без промежуточных коллекций:
Этот пример демонстрирует глубину композиции: groupingBy → collectingAndThen → teeing → вложенный teeing. Читаемость низкая, но вычисление эффективно: один проход по потоку, без промежуточных структур.
teeing: параллельная агрегация
Коллектор teeing (Java 12+) решает задачу вычисления нескольких независимых метрик за один проход. Он принимает два downstream коллектора и функцию слияния их результатов.
Вложенность teeing позволяет агрегировать более двух метрик, но быстро становится громоздкой. Для трёх и более метрик предпочтительнее кастомный коллектор или несколько проходов (если поток позволяет повторное чтение).
Преимущество teeing в ленивости и параллелизме: оба downstream коллектора получают один и тот же поток элементов, но поддерживают независимое внутреннее состояние. При параллельном выполнении их комбайнеры работают независимо, финальное слияние происходит только на уровне teeing.
#Java #для_новичков #beginner #stream_api #Collectors #groupingBy
groupingBy можно вкладывать сам в себя, создавая иерархические структуры Map<K1, Map<K2, V>>:
// Группировка сначала по жанру, затем по автору
Map<Genre, Map<String, List<Book>>> byGenreThenAuthor = library.stream()
.collect(groupingBy(
Book::genre,
groupingBy(Book::author)
));
Читаемость страдает от глубокой вложенности, но структура данных точно отражает бизнес-логику: жанр содержит авторов, автор содержит книги.
Альтернатива — составной ключ:
record GenreAuthor(Genre genre, String author) {}
Map<GenreAuthor, List<Book>> byCompositeKey = library.stream()
.collect(groupingBy(
b -> new GenreAuthor(b.genre(), b.author())
));Выбор между вложенными Map и составными ключами зависит от паттерна доступа: если часто нужны все книги жанра независимо от автора, вложенность предпочтительнее; если доступ всегда по паре жанр-автор, составной ключ проще.
Сложные downstream агрегации
Комбинация адаптеров коллекторов позволяет строить изощрённые агрегации без промежуточных коллекций:
// Для каждого автора: множество жанров, средняя цена, самая старая книга
record AuthorStats(Set<Genre> genres, double avgPrice, Book oldest) {}
Map<String, AuthorStats> statsByAuthor = library.stream()
.collect(groupingBy(
Book::author,
collectingAndThen(
teeing(
teeing(
mapping(Book::genre, toSet()), // Набор жанров
averagingDouble(Book::price), // Средняя цена
Pair::new // Временная пара
),
minBy(comparing(Book::year)), // Самая старая книга
(pair, oldestOpt) -> new AuthorStats(
pair.first(),
pair.second(),
oldestOpt.orElseThrow()
)
),
Function.identity()
)
));
Этот пример демонстрирует глубину композиции: groupingBy → collectingAndThen → teeing → вложенный teeing. Читаемость низкая, но вычисление эффективно: один проход по потоку, без промежуточных структур.
teeing: параллельная агрегация
Коллектор teeing (Java 12+) решает задачу вычисления нескольких независимых метрик за один проход. Он принимает два downstream коллектора и функцию слияния их результатов.
record PriceStats(double min, double max, double average) {}
PriceStats stats = products.stream()
.collect(teeing(
minBy(comparing(Product::price)),
teeing(
maxBy(comparing(Product::price)),
averagingDouble(Product::price),
(maxOpt, avg) -> new Pair<>(maxOpt, avg)
),
(minOpt, pair) -> new PriceStats(
minOpt.map(Product::price).orElse(0.0),
pair.first().map(Product::price).orElse(0.0),
pair.second()
)
));Вложенность teeing позволяет агрегировать более двух метрик, но быстро становится громоздкой. Для трёх и более метрик предпочтительнее кастомный коллектор или несколько проходов (если поток позволяет повторное чтение).
Преимущество teeing в ленивости и параллелизме: оба downstream коллектора получают один и тот же поток элементов, но поддерживают независимое внутреннее состояние. При параллельном выполнении их комбайнеры работают независимо, финальное слияние происходит только на уровне teeing.
#Java #для_новичков #beginner #stream_api #Collectors #groupingBy
👍5
partitionBy: группировка по предикату
Специализированный родственник groupingBy — partitioningBy. Он разделяет поток ровно на две группы по булевому предикату, возвращая Map<Boolean, List<T>>:
В отличие от groupingBy, partitioningBy гарантирует наличие обеих ключей в Map (даже если одна группа пуста).
Это удобно для алгоритмов, требующих обе ветви:
Производительность и аллокации
Многоуровневая агрегация через groupingBy создаёт значительное давление на GC: каждая группа — отдельный список, каждый уровень вложенности — дополнительные Map. Для больших потоков (миллионы элементов, тысячи групп) это критично.
Оптимизации:
Использование примитивных специализаций: groupingByInt, groupingByLong для ключей-примитивов (через mapToInt + boxed, если необходимо).
Предварительная фильтрация: уменьшение входного потока до группировки снижает число создаваемых контейнеров.
Кастомные коллекторы с примитивными аккумуляторами: вместо List<Book> использовать IntSummaryStatistics для подсчёта суммарных страниц, если сами объекты не нужны.
parallelStream с осторожностью: группировка требует слияния Map от разных потоков, что дорого при большом числе групп. Эффективна только при тяжёлой downstream агрегации и редких ключах.
#Java #для_новичков #beginner #stream_api #Collectors #groupingBy
Специализированный родственник groupingBy — partitioningBy. Он разделяет поток ровно на две группы по булевому предикату, возвращая Map<Boolean, List<T>>:
Map<Boolean, List<Book>> partitioned = library.stream()
.collect(partitioningBy(b -> b.year() >= 2000));
List<Book> modernBooks = partitioned.get(true); // 2000 и позже
List<Book> classicBooks = partitioned.get(false); // До 2000
В отличие от groupingBy, partitioningBy гарантирует наличие обеих ключей в Map (даже если одна группа пуста).
Это удобно для алгоритмов, требующих обе ветви:
Map<Boolean, Long> counts = library.stream()
.collect(partitioningBy(
b -> b.year() >= 2000,
counting() // downstream коллектор поддерживается
));
long modernCount = counts.get(true);
long classicCount = counts.get(false); // Никогда null, минимум 0
Производительность и аллокации
Многоуровневая агрегация через groupingBy создаёт значительное давление на GC: каждая группа — отдельный список, каждый уровень вложенности — дополнительные Map. Для больших потоков (миллионы элементов, тысячи групп) это критично.
Оптимизации:
Использование примитивных специализаций: groupingByInt, groupingByLong для ключей-примитивов (через mapToInt + boxed, если необходимо).
Предварительная фильтрация: уменьшение входного потока до группировки снижает число создаваемых контейнеров.
Кастомные коллекторы с примитивными аккумуляторами: вместо List<Book> использовать IntSummaryStatistics для подсчёта суммарных страниц, если сами объекты не нужны.
parallelStream с осторожностью: группировка требует слияния Map от разных потоков, что дорого при большом числе групп. Эффективна только при тяжёлой downstream агрегации и редких ключах.
#Java #для_новичков #beginner #stream_api #Collectors #groupingBy
👍4
Что выведет код?
#Tasks
import java.util.*;
import java.util.stream.*;
public class Task040326 {
public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "apricot", "blueberry", "cherry");
Map<Integer, Long> result = words.stream()
.collect(Collectors.groupingBy(
String::length,
Collectors.counting()
));
System.out.println(result);
}
}
#Tasks
👍3
Варианты ответа:
Anonymous Quiz
78%
{5=1, 6=2, 7=1, 9=1}
17%
{5=2, 6=1, 7=1, 9=1}
6%
{5=1, 6=1, 7=2, 9=1}
0%
{5=1, 6=2, 7=2, 9=0}
👍2
Какие существуют виды ClassLoader'ов и как работает делегирование? 🤓
Ответ:
В Java три встроенных ClassLoader'а, работающих по принципу делегирования (parent-delegation model).
1) Bootstrap ClassLoader (загрузчик начальной загрузки) — самый верхний, загружает ядро JDK из rt.jar.
2) Extension ClassLoader — загружает классы из jre/lib/ext.
3) System/Application ClassLoader — загружает классы из classpath приложения. Когда запрашивается загрузка класса, загрузчик сначала делегирует запрос родителю. Если родитель не может найти класс, только тогда загрузчик пытается загрузить его сам. Это обеспечивает безопасность (системные классы не подменяются)..
#собеседование
Ответ:
1) Bootstrap ClassLoader (загрузчик начальной загрузки) — самый верхний, загружает ядро JDK из rt.jar.
2) Extension ClassLoader — загружает классы из jre/lib/ext.
3) System/Application ClassLoader — загружает классы из classpath приложения. Когда запрашивается загрузка класса, загрузчик сначала делегирует запрос родителю. Если родитель не может найти класс, только тогда загрузчик пытается загрузить его сам. Это обеспечивает безопасность (системные классы не подменяются)..
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
8. Логирование и ELK-стек: как расследуют инциденты в production
Мы прошли огромный путь по созданию Observability для микросервисов: у нас уже есть метрики в Prometheus/Grafana (чтобы знать, ЧТО происходит) и трассировка в Jaeger (чтобы знать, ГДЕ искать проблему). Но когда случается инцидент, мы всё ещё «тыкаемся как слепые котята», потому что не знаем главного — ПОЧЕМУ это произошло.
В этом видео мы закроем последний, самый важный пробел — научимся работать с логами.
Из этого видео вы узнаете:
🔵 Почему чтение логов по контейнерам — это боль и архаизм. Чем структурированные логи отличаются от текстовых и почему без них вы не расследуете инциденты, а гадаете на кофейной гуще.
🔵 MDC — сердце структурированных логов. Что такое Mapped Diagnostic Context, как он работает (спойлер: это ThreadLocal) и как наступить на грабли с асинхронностью. Разберем паттерн TaskDecorator, который спасет ваши нервы и данные.
🔵 JSON-логи без боли. Настройка logstash-logback-encoder, правильное форматирование стектрейсов (ShortenedThrowableConverter) и два способа доставки логов: TCP против stdout + Filebeat.
🔵 ELK-стек под капотом. Разворачиваем Elasticsearch, Logstash и Kibana в Docker. Схема работы, компоненты и их роль.
🔵 Расследование инцидента. Живая демонстрация: от графика в Grafana до конкретной строчки лога с ошибкой через Jaeger и Kibana. Увидите, как traceId связывает три столпа Observability воедино.
Исходный код проекта на GitHub очень ждет Ваших звезд☺️
Ссылка на Youtube
Ссылка на Рутьюб
Смотрите, ставьте лайки, подписывайтесь на каналы!✌️
❗️ ❗️ ❗️ Огромная просьба - если Вам понравилась моя работа, распространите эту серию по всем доступным вам местам: телеграм, discord и прочим каналам. Буду крайне благодарен ❗️ ❗️ ❗️
Мы прошли огромный путь по созданию Observability для микросервисов: у нас уже есть метрики в Prometheus/Grafana (чтобы знать, ЧТО происходит) и трассировка в Jaeger (чтобы знать, ГДЕ искать проблему). Но когда случается инцидент, мы всё ещё «тыкаемся как слепые котята», потому что не знаем главного — ПОЧЕМУ это произошло.
В этом видео мы закроем последний, самый важный пробел — научимся работать с логами.
Из этого видео вы узнаете:
Исходный код проекта на GitHub очень ждет Ваших звезд
Ссылка на Youtube
Ссылка на Рутьюб
Смотрите, ставьте лайки, подписывайтесь на каналы!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5👍2
История технологий сегодня — 05 марта
ℹ️ Кто родился в этот день
Владимир Константинович Ле́вин (5 марта 1929, Москва — 4 февраля 2026) — советский и российский учёный в области кибернетики, академик РАН (2003), специалист в области вычислительной техники и элементной базы вычислительных машин. Научный руководитель Федерального государственного унитарного предприятия «Научно-исследовательский институт „КВАНТ“». Лауреат Ленинской премии и Государственной премии СССР.
Момофуку Андо (яп. 安藤 百福 Андо: Момофуку, при рождении Го Пек-Хок, 5 марта 1910, Пуцзы, Японская империя — 5 января 2007, Икеда, Осака) — изобретатель и бизнесмен, основавший компанию Nissin Foods. Он известен как изобретатель лапши быстрого приготовления и создатель торговых марок Top Ramen и Cup Noodles. В опросе общественного мнения в Японии, проведённом в 2000 году, изобретение Момофуку Андо лапши быстрого приготовления назвали главным японским изобретением XX века.
🌐 Знаковые события
1979 — космический аппарат «Вояджер-1» достиг планеты Юпитер.
#Biography #Birth_Date #Events #05марта
Владимир Константинович Ле́вин (5 марта 1929, Москва — 4 февраля 2026) — советский и российский учёный в области кибернетики, академик РАН (2003), специалист в области вычислительной техники и элементной базы вычислительных машин. Научный руководитель Федерального государственного унитарного предприятия «Научно-исследовательский институт „КВАНТ“». Лауреат Ленинской премии и Государственной премии СССР.
Момофуку Андо (яп. 安藤 百福 Андо: Момофуку, при рождении Го Пек-Хок, 5 марта 1910, Пуцзы, Японская империя — 5 января 2007, Икеда, Осака) — изобретатель и бизнесмен, основавший компанию Nissin Foods. Он известен как изобретатель лапши быстрого приготовления и создатель торговых марок Top Ramen и Cup Noodles. В опросе общественного мнения в Японии, проведённом в 2000 году, изобретение Момофуку Андо лапши быстрого приготовления назвали главным японским изобретением XX века.
1979 — космический аппарат «Вояджер-1» достиг планеты Юпитер.
#Biography #Birth_Date #Events #05марта
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4