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

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
Какие существуют виды ClassLoader'ов и как работает делегирование? 🤓

Ответ:

В Java три встроенных ClassLoader'а, работающих по принципу делегирования (parent-delegation model).

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 и прочим каналам. Буду крайне благодарен ❗️❗️❗️
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марта
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
[Совет по Java #012]

Тема: При итерации по Map для получения и ключа, и значения используйте entrySet(), а не keySet() с последующим get().

Проблема: Итерация по Map через keySet() с последующим вызовом get(key) для получения значения создает избыточную нагрузку.

Каждый вызов get() выполняет полноценный поиск по хэш-таблице: повторное вычисление хэш-кода, поиск в корзине (bucket) и сравнение ключей. При обходе всей мапы это означает двукратное выполнение работы — один раз для получения ключа из keySet(), и второй раз внутри get().

Для больших мап или частых итераций это приводит к неоправданному снижению производительности, особенно если хэш-функция сложная или коллизии часты.

Решение: Используйте entrySet(), который возвращает набор пар Map.Entry<K, V>. Каждый entry содержит и ключ, и значение, полученные за одну операцию доступа. Методы entry.getKey() и entry.getValue() работают с уже извлеченными данными и не выполняют повторного поиска по мапе. Это оптимальный способ итерации, когда нужны обе составляющие пары.

import java.util.*;

public class Advice050326 {

private static final Map<String, Integer> SCORES = new HashMap<>();

static {
SCORES.put("Alice", 95);
SCORES.put("Bob", 87);
SCORES.put("Charlie", 92);
SCORES.put("Diana", 98);
}

//Антипаттерн: итерация через keySet() + get()
public static void badIteration() {
System.out.println("Bad iteration (keySet + get):");
for (String key : SCORES.keySet()) {
// get() выполняет ПОЛНЫЙ поиск по мапе
Integer value = SCORES.get(key);
System.out.println(key + " = " + value);
}
}

//Правильно: итерация через entrySet()
public static void goodIteration() {
System.out.println("Good iteration (entrySet):");
for (Map.Entry<String, Integer> entry : SCORES.entrySet()) {
// Данные уже извлечены, повторного поиска нет
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
}

//Java 8+: forEach с лямбдой
public static void lambdaIteration() {
System.out.println("Lambda iteration (forEach):");
SCORES.forEach((key, value) ->
System.out.println(key + " = " + value)
);
}

//Модификация значений через entrySet
public static void modifyValues() {
Map<String, Integer> mutableMap = new HashMap<>(SCORES);

// Увеличить баллы всем на 5
for (Map.Entry<String, Integer> entry : mutableMap.entrySet()) {
entry.setValue(entry.getValue() + 5); // Работает!
}

// Через keySet так не сделать
System.out.println("Modified: " + mutableMap);
}

public static void main(String[] args) {
badIteration();
goodIteration();
lambdaIteration();
modifyValues();
}
}


Объяснение: keySet() возвращает представление ключей, не содержащее значений.

При вызове get(key) происходит повторный проход по структуре данных. Для HashMap это повторное вычисление хэша и поиск в корзине; для TreeMap — логарифмический поиск; для LinkedHashMap — повторный обход цепочки. entrySet() предоставляет прямой доступ к паре, которая была извлечена при обходе внутренней структуры.

Начиная с Java 8, метод forEach напрямую принимает би-функцию (ключ, значение), что делает код еще более лаконичным и читаемым.

Разработчик всегда выбирает entrySet() или forEach при работе с обоими компонентами записи и использует keySet() только когда нужны исключительно ключи.

#Java #советы
👍6
Что выведет код?

import java.util.*;

public class Task050326 {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);

Set<String> keys = map.keySet();
for (String key : keys) {
if (key.equals("b")) {
map.remove("c");
}
System.out.print(key + "=" + map.get(key) + " ");
}
}
}


#Tasks
👍2
👍2
Что такое Future и Callable? Чем Callable отличается от Runnable? 🤓

Ответ:

Оба интерфейса представляют задачи для выполнения в отдельном потоке.

Runnable существует с Java 1.0, его метод run() не возвращает результат и не может бросить проверяемое исключение.

Callable<V> появился в Java 5, его метод call() возвращает результат типа V и может бросать проверяемые исключения.

Callable удобно использовать с ExecutorService, который возвращает объект Future, позволяющий отслеживать состояние задачи, ожидать ее завершения и получать результат методом get().


#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
История технологий сегодня — 06 марта

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

Адам Осборн (англ. Adam Osborne; 6 марта 1939, Бангкок, Таиланд — 18 марта 2003, Кодаиканал[англ.], Индия) — американский предприниматель британского происхождения, издатель книг и создатель программного обеспечения, компьютерный дизайнер, основавший несколько компаний в США и других странах. Создатель первого коммерчески успешного портативного компьютера Osborne 1.


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

1869 — Дмитрий Менделеев представил периодическую систему химических элементов Русскому химическому обществу.

1983 — стартовали продажи Motorola DynaTAC 8000X, первого коммерчески успешного сотового телефона.

1992 — в день рождения великого художника и скульптора Микеланджело впервые активизировался компьютерный вирус Michelangelo, от которого только в Германии пострадали 1500 компьютеров.


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

Глава 4: Искусство агрегации. Коллекторы и группировки

(Практика): Анализ библиотеки в один проход

Подготовка: обновление модели Book

Для этой практики расширьте класс Book:
public class Book {
private final String title;
private final String author;
private final int year; // год издания
private final int pages; // количество страниц
private final double price; // цена
private final List<String> genres;

// конструктор, геттеры...

public boolean hasGenre(String genre) {
return genres.contains(genre);
}
}



Задача: три агрегации над библиотекой

Требуемые результаты:
Map<String, Book> — автор → его самая старая книга (минимальный год)
int — общее количество страниц во всех книгах
OptionalDouble — средняя цена книг жанра «Фантастика»


Способ 1: Три отдельных Stream (три прохода)
Простой, читаемый, но неэффективный для больших данных.

Агрегация 1: автор → самая старая книга
public Map<String, Book> getOldestBookByAuthor() {
return books.stream()
.collect(Collectors.toMap(
Book::getAuthor, // ключ — автор
Function.identity(), // значение — книга
(existing, replacement) -> // разрешение коллизии
existing.getYear() < replacement.getYear()
? existing : replacement
));
}
Что происходит: при встрече второй книги того же автора сравниваем годы, оставляем старую.


Агрегация 2: общее количество страниц
public int getTotalPages() {
return books.stream()
.mapToInt(Book::getPages)
.sum();
}
Что происходит: mapToInt создаёт примитивный поток, sum — терминальная редукция.


Агрегация 3: средняя цена фантастики
public OptionalDouble getAverageFantasyPrice() {
return books.stream()
.filter(Book::hasGenre, "Фантастика")
.mapToDouble(Book::getPrice)
.average();
}
Что происходит: фильтр оставляет только фантастику, average возвращает OptionalDouble для пустого результата.



Анализ трёх проходов

Плюсы:
Каждый метод простой и понятный
Легко тестировать изолированно
Легко добавить или убрать агрегацию

Минусы:
Три полных обхода списка — O(3n) время
Три раза загружаются данные в кэш процессора
Для миллиона книг — заметная разница с одним проходом


#Java #для_новичков #beginner #stream_api #практика
👍4
Способ 2: Один Stream с комбинированным коллектором

Используем Collectors.teeing (Java 12+) или кастомный коллектор для одного прохода.
Подход через teeing (Java 12+)

teeing направляет элементы в два коллектора параллельно, затем объединяет результаты. Для трёх агрегаций применяем вложенно.
public LibraryStatistics analyzeInOnePass() {
return books.stream()
.collect(Collectors.teeing(
// Первый коллектор: автор → старая книга
Collectors.toMap(
Book::getAuthor,
Function.identity(),
(a, b) -> a.getYear() < b.getYear() ? a : b
),
// Второй коллектор: teeing для страниц и фантастики
Collectors.teeing(
Collectors.summingInt(Book::getPages),
Collectors.filtering(
b -> b.hasGenre("Фантастика"),
Collectors.averagingDouble(Book::getPrice)
),
(totalPages, avgFantasyPrice) ->
new IntermediateStats(totalPages, avgFantasyPrice)
),
// Объединение результатов
(oldestByAuthor, intermediate) -> new LibraryStatistics(
oldestByAuthor,
intermediate.totalPages,
intermediate.avgFantasyPrice
)
));
}

// Вспомогательные классы-контейнеры
private static class IntermediateStats {
final int totalPages;
final Double avgFantasyPrice; // null если нет фантастики

IntermediateStats(int totalPages, Double avgFantasyPrice) {
this.totalPages = totalPages;
this.avgFantasyPrice = avgFantasyPrice;
}
}

public static class LibraryStatistics {
public final Map<String, Book> oldestByAuthor;
public final int totalPages;
public final OptionalDouble avgFantasyPrice;

// конструктор, преобразование Double → OptionalDouble...
}


Что происходит:
Каждая книга обрабатывается один раз
teeing разветвляет поток: одна копия идёт в Map авторов, другая — в сумматор страниц и усреднитель фантастики
filtering (Java 9+) — коллектор-обёртка, пропускающий элементы по условию

#Java #для_новичков #beginner #stream_api #практика
👍5
Подход через кастомный коллектор (Java 8+)

Если teeing недоступен, реализуем Collector вручную:

public LibraryStatistics analyzeWithCustomCollector() {
return books.stream()
.collect(new LibraryStatsCollector());
}

private static class LibraryStatsCollector
implements Collector<Book, StatsAccumulator, LibraryStatistics> {

@Override
public Supplier<StatsAccumulator> supplier() {
return StatsAccumulator::new;
}

@Override
public BiConsumer<StatsAccumulator, Book> accumulator() {
return (acc, book) -> {
// Обновление Map авторов
acc.oldestByAuthor.merge(
book.getAuthor(),
book,
(a, b) -> a.getYear() < b.getYear() ? a : b
);

// Накопление страниц
acc.totalPages += book.getPages();

// Накопление для фантастики
if (book.hasGenre("Фантастика")) {
acc.fantasyCount++;
acc.fantasyPriceSum += book.getPrice();
}
};
}

@Override
public BinaryOperator<StatsAccumulator> combiner() {
return (left, right) -> {
// Слияние для параллельных потоков
left.oldestByAuthor.putAll(right.oldestByAuthor); // с разрешением
left.totalPages += right.totalPages;
left.fantasyCount += right.fantasyCount;
left.fantasyPriceSum += right.fantasyPriceSum;
return left;
};
}

@Override
public Function<StatsAccumulator, LibraryStatistics> finisher() {
return acc -> new LibraryStatistics(
acc.oldestByAuthor,
acc.totalPages,
acc.fantasyCount == 0
? OptionalDouble.empty()
: OptionalDouble.of(acc.fantasyPriceSum / acc.fantasyCount)
);
}

@Override
public Set<Characteristics> characteristics() {
return Collections.emptySet(); // не CONCURRENT из-за HashMap
}
}

private static class StatsAccumulator {
Map<String, Book> oldestByAuthor = new HashMap<>();
int totalPages = 0;
int fantasyCount = 0;
double fantasyPriceSum = 0.0;
}


Практические задания

Задача 1: реализовать оба способа
В проекте «Библиотека» создайте класс LibraryAnalyzer с методами:
analyzeThreePasses() — три отдельных Stream
analyzeOnePassTeeing() — через teeing (если Java 12+)
analyzeOnePassCustom() — через кастомный Collector
Протестируйте на 100, 10 000 и 100 000 книг. Измерьте время System.nanoTime().

Задача 2: добавить четвёртую агрегацию
Добавьте: Map<String, Long> — жанр → количество книг.
Обновите оба подхода. В кастомном коллекторе используйте Map.merge() для подсчёта.

Задача 3: параллельная версия
Модифицируйте кастомный коллектор: добавьте CONCURRENT в характеристики, замените HashMap на ConcurrentHashMap или используйте toConcurrentMap.
Сравните stream() vs parallelStream() для 1 000 000 книг на многоядерной машине.

Задача 4: ленивая статистика (звёздочка)
Реализуйте LazyLibraryStatistics — класс, который не вычисляет агрегации при создании, а при первом обращении к любому полю выполняет один проход и кэширует все результаты.
Используйте Supplier + AtomicReference для thread-safe ленивой инициализации.


#Java #для_новичков #beginner #stream_api #практика
👍5