Java for Beginner
870 subscribers
1.01K photos
275 videos
14 files
1.69K links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

Наш YouTube канал - https://www.youtube.com/@Java_Beginner-Dev

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
С 21.02 по 27.02
Предыдущий пост(с 14.02 по 20.02)

Воскресный мотивационный пост:
ГЛОБАЛИЗАЦИЯ ЗАКАНЧИВАЕТСЯ

Запись встреч/видео:
7. Введение в распределённую трассировку. OpenTelemetry & Jaeger.

Обучающие статьи:
Раздел 8. Stream API и функциональный стиль

Глава 3: Чистые преобразования и борьба с исключениями
Side Effects — главный враг предсказуемости
(Практика): Чистая агрегация уникальных жанров

Советы по Java:
[Совет по Java #008]
Не ловите Exception или Throwable без крайней необходимости. Ловите максимально конкретные исключения.

[Совет по Java #009]
Используйте Collections.emptyList(), Collections.singletonList() вместо создания новых коллекций, когда нужно вернуть пустой или одноэлементный список.

[Совет по Java #010]
Ключевое слово finally выполняется всегда, даже если в try или catch был return.



Полезные статьи и видео:
JDBC для профи: пулы, batch, транзакции и скрытые риски


Как и всегда, задачи можно найти под тегом - #Tasks, вопросы с собеседований - #собеседование
👍2
История технологий сегодня — 29 Февраля/01 марта

ℹ️ Кто родился в этот день

Сеймур Пейперт (Seymour Papert [ˈsimɔɹ ˈpæpɚt]; 29 февраля 1928, Претория, Южная Африка — 31 июля 2016, Блу-Хилл, Мэн, США) — математик, программист, психолог и педагог. Один из основоположников теории искусственного интеллекта, создатель языка Logo (1968), автор конструкционистской теории обучения[6].

Герман Хо́ллерит (англ. Herman Hollerith; 29 февраля 1860 — 17 ноября 1929) — американский инженер и изобретатель. Считается отцом современных автоматических вычислений. Известен как создатель электрической табулирующей системы.


🌐 Знаковые события

1888 — американский инженер Герман Холлерит изобрёл табулятор, первую электрическую счётную машину.

1936 — Нильс Бор предложил теорию составного ядра атома.

1966 — советская автоматическая межпланетная станция Венера-3 впервые в мире достигла поверхности Венеры.


#Biography #Birth_Date #Events #29февраля #01марта
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2👍1
Сегодня день тишины))

Я целый день готовился к видео с elk… так и не успел))

Завтра продолжу))
🔥9
История технологий сегодня — 02 марта

ℹ️ Кто родился в этот день

Князь Бори́с Бори́сович Голи́цын (18 февраля [2 марта] 1862, Санкт-Петербург — 4 [17] мая 1916, Петергоф, Петроградская губерния) — русский физик и геофизик из рода Голицыных, один из основоположников сейсмологии, изобретатель первого электромагнитного сейсмографа (1906).


🌐 Знаковые события

1969 — Первый испытательный полёт французского сверхзвукового пассажирского самолёта «Конкорд».


#Biography #Birth_Date #Events #02марта
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2👍1
Раздел 8. Stream API и функциональный стиль в Java

Глава 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]" при превышении.
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.
// Опасно: 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
Что выведет код?

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
Варианты ответа:
Anonymous Quiz
63%
APPLE&APRICOT
13%
APPLE
8%
APRICOT
17%
null
👍2🆒1
В чем разница между Fail-Fast и Fail-Safe итераторами? 🤓

Ответ:

Fail-Fast
итераторы (например, в ArrayList, HashMap) при обнаружении изменений в структуре коллекции во время итерации (кроме методов самого итератора) немедленно выбрасывают ConcurrentModificationException.

Они работают быстро, но небезопасны в многопоточной среде.

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марта
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() возвращает генератор, привязанный к текущему потоку, без создания новых объектов при каждом вызове.

Это не только потокобезопасно, но и значительно быстрее в многопоточной среде (ускорение может достигать десятков раз).

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
Что выведет код?

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
Варианты ответа:
Anonymous Quiz
13%
1000
73%
Меньше 1000
7%
0
7%
Исключение
👍2
Что такое wildcard (<?>) в Generics? 🤓

Ответ:

Wildcards
(символы подстановки) в Generics — это знак ?, обозначающий неизвестный тип.

Используются для большей гибкости: ? 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марта
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>>:
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<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>>:
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