Раздел 8. Stream API и функциональный стиль в Java
Глава 5: Контракты. Почему equals, hashCode и иммутабельность — закон
Иммутабельность данных в потоке — не прихоть, а необходимость
Stream API построен на функциональной парадигме, где данные текут через преобразования, сохраняя свою сущность неизменной. Этот принцип — иммутабельность (immutability) — не эстетическая прихоть, а архитектурный фундамент, обеспечивающий предсказуемость, безопасность параллелизма и корректность ленивых вычислений.
Когда мы говорим "поток обрабатывает данные, а не изменяет их", мы противопоставляем два ментальных модуса: трансформацию как порождение нового и мутацию как изменение существующего. Императивный код часто смешивает их: цикл читает элемент, модифицирует его поля, кладёт в результат. Потоковый код разделяет: исходные данные остаются неизменными, каждая операция порождает новое представление.
Контракт неизменности источника
Исходная коллекция, породившая поток, не должна модифицироваться во время обхода. Это правило кажется очевидным, но нарушается из-за недопонимания ленивости:
Поведение неопределённо. Если поток использует Iterator (коллекция не SIZED), модификация после создания потока, но до начала обхода выбросит ConcurrentModificationException при проверке modCount. Если поток использует Spliterator с характеристикой SIZED (например, ArrayList), и терминальная операция предвыделяет размер, добавление элемента приведёт к ArrayIndexOutOfBoundsException или пропуску элементов.
Ещё опаснее модификация внутри потоковой операции:
Удаление элементов из коллекции во время её обхода — классическая ошибка, приводящая к исключению или пропуску элементов из-за сдвига индексов.
Правильный подход — создание новой коллекции вместо модификации существующей:
Иммутабельность промежуточных результатов
Не только источник, но и объекты в потоке должны быть неизменяемыми для корректности.
Рассмотрим изменяемый объект в map:
forEach здесь используется не для агрегации, а для побочного эффекта — инкремента счётчика. Исходные объекты модифицированы, что делает код сложным для отладки и тестирования. Если позже тот же список обрабатывается другим потоком, результаты зависят от порядка выполнения.
Рефакторинг к иммутабельности:
Здесь "обновление" — порождение новой версии объекта, а не модификация существующего. Старые данные сохраняются, новые создаются, история не разрушается.
#Java #для_новичков #beginner #stream_api
Глава 5: Контракты. Почему equals, hashCode и иммутабельность — закон
Иммутабельность данных в потоке — не прихоть, а необходимость
Stream API построен на функциональной парадигме, где данные текут через преобразования, сохраняя свою сущность неизменной. Этот принцип — иммутабельность (immutability) — не эстетическая прихоть, а архитектурный фундамент, обеспечивающий предсказуемость, безопасность параллелизма и корректность ленивых вычислений.
Когда мы говорим "поток обрабатывает данные, а не изменяет их", мы противопоставляем два ментальных модуса: трансформацию как порождение нового и мутацию как изменение существующего. Императивный код часто смешивает их: цикл читает элемент, модифицирует его поля, кладёт в результат. Потоковый код разделяет: исходные данные остаются неизменными, каждая операция порождает новое представление.
Контракт неизменности источника
Исходная коллекция, породившая поток, не должна модифицироваться во время обхода. Это правило кажется очевидным, но нарушается из-за недопонимания ленивости:
List<Book> library = new ArrayList<>(fetchBooks());
Stream<Book> stream = library.stream(); // Поток создан, но не активирован
// Позже, до терминальной операции:
library.add(new Book("New", "Author", 2024)); // Модификация источника!
// Теперь терминальная операция:
List<Book> result = stream.collect(toList()); // ConcurrentModificationException?
Поведение неопределённо. Если поток использует Iterator (коллекция не SIZED), модификация после создания потока, но до начала обхода выбросит ConcurrentModificationException при проверке modCount. Если поток использует Spliterator с характеристикой SIZED (например, ArrayList), и терминальная операция предвыделяет размер, добавление элемента приведёт к ArrayIndexOutOfBoundsException или пропуску элементов.
Ещё опаснее модификация внутри потоковой операции:
// Катастрофа: модификация источника изнутри потока
library.stream()
.filter(b -> b.year() < 1950)
.forEach(library::remove); // ConcurrentModificationException гарантирован
Удаление элементов из коллекции во время её обхода — классическая ошибка, приводящая к исключению или пропуску элементов из-за сдвига индексов.
Правильный подход — создание новой коллекции вместо модификации существующей:
// Правильно: фильтрация через collect, не removeIf в потоке
List<Book> modernBooks = library.stream()
.filter(b -> b.year() >= 1950)
.collect(toList());
// Или без потока, если модификация исходной нужна:
library.removeIf(b -> b.year() < 1950); // Метод коллекции, не Stream API
Иммутабельность промежуточных результатов
Не только источник, но и объекты в потоке должны быть неизменяемыми для корректности.
Рассмотрим изменяемый объект в map:
public class MutableBook {
private String title;
private int readCount; // Мутабельное поле!
public void incrementRead() { readCount++; }
}
// Антипаттерн: мутация внутри потока
List<MutableBook> books = fetchBooks();
books.stream()
.filter(b -> b.getTitle().startsWith("A"))
.forEach(MutableBook::incrementRead); //Модифицируем исходные объекты!
// Теперь исходный список books изменён — побочный эффектforEach здесь используется не для агрегации, а для побочного эффекта — инкремента счётчика. Исходные объекты модифицированы, что делает код сложным для отладки и тестирования. Если позже тот же список обрабатывается другим потоком, результаты зависят от порядка выполнения.
Рефакторинг к иммутабельности:
public record Book(String title, int readCount) {
public Book withIncrementedRead() {
return new Book(title, readCount + 1); //Новый экземпляр
}
}
// Правильно: создание новых объектов
List<Book> updatedBooks = books.stream()
.filter(b -> b.title().startsWith("A"))
.map(Book::withIncrementedRead)
.collect(toList());
// Исходный список books неизменёнЗдесь "обновление" — порождение новой версии объекта, а не модификация существующего. Старые данные сохраняются, новые создаются, история не разрушается.
#Java #для_новичков #beginner #stream_api
👍6
Контролируемые побочные эффекты
Абсолютный запрет на побочные эффекты невозможен в реальных системах. Логирование, метрики, кэширование, внешние вызовы — всё это требует взаимодействия с внешним миром. Различие между допустимым и недопустимым — в контролируемости и изоляции.
Критерии контролируемого побочного эффекта:
Идемпотентность: повторный вызов с теми же входными данными даёт тот же результат (или эквивалентный с точки зрения системы).
Потокобезопасность: при параллельном выполнении эффект корректен без дополнительной синхронизации.
Изолированность: эффект не влияет на другие элементы потока или внешнее состояние, используемое в потоке.
Необходимость: эффект является целью операции, не побочным продуктом.
Пример: атомарный счётчик для отладки
AtomicInteger обеспечивает потокобезопасность инкремента. Эффект изолирован — счётчик не влияет на обработку элементов. Он идемпотентен с точки зрения бизнес-логики (конечный результат results не зависит от счётчика). Но это всё ещё антипаттерн: peek предназначен для отладки, а не для бизнес-логики, и его поведение может измениться в будущих версиях JDK.
Более чистый подход — отделение агрегации от обработки:
Или использование teeing для подсчёта параллельно с сбором результатов.
Пример: логирование
Логирование — необходимый побочный эффект. Он потокобезопасен, если auditLog потокобезопасен. Он изолирован — не влияет на обработку заказа. Но размещение в peek рискованно: при оптимизации конвейера peek может быть пропущен или вызван менее раз, чем ожидается.
Надёжнее:
Теперь логирование гарантировано для каждого прошедшего фильтр элемента, хотя map с побочным эффектом всё ещё нарушает чистоту функции.
Граница допустимого
Правило практики: побочные эффекты допустимы только в терминальных операциях, работающих с уже потокобезопасными внешними системами. forEach для отправки сообщений в Kafka, collect в ConcurrentHashMap, reduce с атомарными операциями — приемлемы. Побочные эффекты в промежуточных операциях (map, filter, flatMap) — почти всегда ошибка.
Исключение — кэширование вычислений внутри операции, но и здесь предпочтительны чистые функции с мемоизацией вне потока.
#Java #для_новичков #beginner #stream_api
Абсолютный запрет на побочные эффекты невозможен в реальных системах. Логирование, метрики, кэширование, внешние вызовы — всё это требует взаимодействия с внешним миром. Различие между допустимым и недопустимым — в контролируемости и изоляции.
Критерии контролируемого побочного эффекта:
Идемпотентность: повторный вызов с теми же входными данными даёт тот же результат (или эквивалентный с точки зрения системы).
Потокобезопасность: при параллельном выполнении эффект корректен без дополнительной синхронизации.
Изолированность: эффект не влияет на другие элементы потока или внешнее состояние, используемое в потоке.
Необходимость: эффект является целью операции, не побочным продуктом.
Пример: атомарный счётчик для отладки
AtomicInteger processedCount = new AtomicInteger(0);
List<Result> results = items.parallelStream()
.map(this::heavyProcessing)
.peek(r -> processedCount.incrementAndGet()) // Контролируемый эффект
.collect(toList());
System.out.println("Обработано: " + processedCount.get());
AtomicInteger обеспечивает потокобезопасность инкремента. Эффект изолирован — счётчик не влияет на обработку элементов. Он идемпотентен с точки зрения бизнес-логики (конечный результат results не зависит от счётчика). Но это всё ещё антипаттерн: peek предназначен для отладки, а не для бизнес-логики, и его поведение может измениться в будущих версиях JDK.
Более чистый подход — отделение агрегации от обработки:
// Лучше: явная агрегация результата
class ProcessingResult {
final List<Result> results;
final int count;
ProcessingResult(List<Result> results, int count) {
this.results = results;
this.count = count;
}
}
ProcessingResult finalResult = items.parallelStream()
.collect(() -> new ProcessingResult(new ArrayList<>(), 0),
(acc, item) -> {
acc.results.add(process(item));
acc.count++;
},
(left, right) -> {
left.results.addAll(right.results);
left.count += right.count;
});
Или использование teeing для подсчёта параллельно с сбором результатов.
Пример: логирование
orders.stream()
.filter(o -> o.amount().compareTo(THRESHOLD) > 0)
.peek(o -> auditLog.record("Крупный заказ", o.id())) // Побочный эффект
.map(this::processLargeOrder)
.collect(toList());
Логирование — необходимый побочный эффект. Он потокобезопасен, если auditLog потокобезопасен. Он изолирован — не влияет на обработку заказа. Но размещение в peek рискованно: при оптимизации конвейера peek может быть пропущен или вызван менее раз, чем ожидается.
Надёжнее:
List<ProcessedOrder> processed = orders.stream()
.filter(o -> o.amount().compareTo(THRESHOLD) > 0)
.map(o -> {
auditLog.record("Крупный заказ", o.id()); // Явный эффект в map
return processLargeOrder(o);
})
.collect(toList());
Теперь логирование гарантировано для каждого прошедшего фильтр элемента, хотя map с побочным эффектом всё ещё нарушает чистоту функции.
Граница допустимого
Правило практики: побочные эффекты допустимы только в терминальных операциях, работающих с уже потокобезопасными внешними системами. forEach для отправки сообщений в Kafka, collect в ConcurrentHashMap, reduce с атомарными операциями — приемлемы. Побочные эффекты в промежуточных операциях (map, filter, flatMap) — почти всегда ошибка.
Исключение — кэширование вычислений внутри операции, но и здесь предпочтительны чистые функции с мемоизацией вне потока.
#Java #для_новичков #beginner #stream_api
👍5
Что выведет код?
#Tasks
import java.util.*;
import java.util.stream.*;
public class Task120326 {
public static void main(String[] args) {
List<Person120326> people = new ArrayList<>(Arrays.asList(
new Person120326("Alice", 25),
new Person120326("Bob", 30),
new Person120326("Charlie", 20)
));
List<String> names = people.stream()
.map(p -> p.name)
.sorted((a, b) -> {
people.get(0).age = 999;
return a.compareTo(b);
})
.collect(Collectors.toList());
System.out.println(people);
System.out.println(names);
}
static class Person120326 {
String name;
int age;
Person120326(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + ":" + age;
}
}
}
#Tasks
👍2
Что такое Dependency Injection (DI) и Inversion of Control (IoC)? 🤓
Ответ:
Inversion of Control (IoC) — это общий принцип, при котором фреймворк управляет потоком программы и объектами, а не наоборот.
Dependency Injection (DI) — это конкретная реализация IoC, при которой объекты получают свои зависимости извне (через конструктор, сеттер или интерфейс), а не создают их сами.
Это уменьшает связанность кода, повышает тестируемость (легко подставить моки) и гибкость.
Главный контейнер DI в Java-мире — это Spring IoC-контейнер.
#собеседование
Ответ:
Dependency Injection (DI) — это конкретная реализация IoC, при которой объекты получают свои зависимости извне (через конструктор, сеттер или интерфейс), а не создают их сами.
Это уменьшает связанность кода, повышает тестируемость (легко подставить моки) и гибкость.
Главный контейнер DI в Java-мире — это Spring IoC-контейнер.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
История технологий сегодня — 13 марта
ℹ️ Кто родился в этот день
Джон Хазбрук Ван Флек (англ. John Hasbrouck van Vleck; 13 марта 1899, Мидлтаун — 27 октября 1980, Кембридж, Массачусетс) — американский физик, лауреат Нобелевской премии по физике 1977 года. Работы по квантовой теории атомной структуры, магнетизму, теории валентности, изучению атомных и молекулярных спектров, ферро- и ферримагнитному резонансу. Один из создателей современных представителей о магнетизме вещества. Разработал квантовомеханическую теорию диа- и парамагнетизма (1926—1928), получил парамагнитную добавку к диамагнитной восприимчивости несимметричных атомов и молекул, названную ванфлековским парамагнетизмом (1927). Развил теорию внутрикристаллического (лигандного) поля. Дал детальную трактовку антиферромагнетизма (1941), термодинамическую теорию молекулярного поля для антиферромагнетиков, развил гейзенберговскую модель локализованных спинов и предложил эффективный спиновый гамильтониан.
🌐 Знаковые события
1781 — Английский астроном Вильям Гершель с помощью собственноручно изготовленного телескопа открыл седьмую планету Солнечной системы — Уран. Правда, первоначально он принял её за комету. Когда выяснилось, что это неизвестная ранее планета, он получил медаль Королевского общества и должность придворного астронома. Сам Гершель назвал планету в честь своего высокого покровителя короля Георга III «Звездой Георга», короткое время она носила имя первооткрывателя, пока немецкий астроном Иоганн Боде не придумал для неё название Уран.
1930 — американский астроном Клайд Томбо объявил в докладе об открытии им 18 февраля девятой планеты Солнечной системы. Доклад был приурочен к 75-летию основателя и первого директора крупнейшей частной обсерватории в США, Персиваля Лоуэлла, который многие годы потратил на поиски предсказанной им девятой планеты. На следующий день 14 марта Венеция Бёрни, одиннадцатилетняя школьница из Оксфорда, предложит имя для новой планеты, и в мае будет опубликовано официальное имя — Плутон.
1989 — была изобретена Всемирная паутина (World Wide Web, WWW), более известная как Интернет. Изобретателем Интернета считается английский учёный Тим Бернерс-Ли и его коллеги, работавшие в Европейском совете по ядерным исследованиям (CERN), передали начальнику своего отдела документ, озаглавленный «Информационный менеджмент: некоторые предложения», в котором были заложены основные принципы WWW.
#Biography #Birth_Date #Events #13марта
Джон Хазбрук Ван Флек (англ. John Hasbrouck van Vleck; 13 марта 1899, Мидлтаун — 27 октября 1980, Кембридж, Массачусетс) — американский физик, лауреат Нобелевской премии по физике 1977 года. Работы по квантовой теории атомной структуры, магнетизму, теории валентности, изучению атомных и молекулярных спектров, ферро- и ферримагнитному резонансу. Один из создателей современных представителей о магнетизме вещества. Разработал квантовомеханическую теорию диа- и парамагнетизма (1926—1928), получил парамагнитную добавку к диамагнитной восприимчивости несимметричных атомов и молекул, названную ванфлековским парамагнетизмом (1927). Развил теорию внутрикристаллического (лигандного) поля. Дал детальную трактовку антиферромагнетизма (1941), термодинамическую теорию молекулярного поля для антиферромагнетиков, развил гейзенберговскую модель локализованных спинов и предложил эффективный спиновый гамильтониан.
1781 — Английский астроном Вильям Гершель с помощью собственноручно изготовленного телескопа открыл седьмую планету Солнечной системы — Уран. Правда, первоначально он принял её за комету. Когда выяснилось, что это неизвестная ранее планета, он получил медаль Королевского общества и должность придворного астронома. Сам Гершель назвал планету в честь своего высокого покровителя короля Георга III «Звездой Георга», короткое время она носила имя первооткрывателя, пока немецкий астроном Иоганн Боде не придумал для неё название Уран.
1930 — американский астроном Клайд Томбо объявил в докладе об открытии им 18 февраля девятой планеты Солнечной системы. Доклад был приурочен к 75-летию основателя и первого директора крупнейшей частной обсерватории в США, Персиваля Лоуэлла, который многие годы потратил на поиски предсказанной им девятой планеты. На следующий день 14 марта Венеция Бёрни, одиннадцатилетняя школьница из Оксфорда, предложит имя для новой планеты, и в мае будет опубликовано официальное имя — Плутон.
1989 — была изобретена Всемирная паутина (World Wide Web, WWW), более известная как Интернет. Изобретателем Интернета считается английский учёный Тим Бернерс-Ли и его коллеги, работавшие в Европейском совете по ядерным исследованиям (CERN), передали начальнику своего отдела документ, озаглавленный «Информационный менеджмент: некоторые предложения», в котором были заложены основные принципы WWW.
#Biography #Birth_Date #Events #13марта
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
[Совет по Java #015]
Тема: @Async по умолчанию не работает внутри того же класса. Вызов асинхронного метода из другого метода этого же класса не пройдет через Spring Proxy.
Проблема: Аннотация @Async работает благодаря Spring AOP, который создает прокси вокруг целевого бина.
При вызове метода через прокси (например, из другого бина) запускается асинхронное выполнение. Однако при прямом вызове из того же класса через this прокси не участвует — вызывается оригинальный метод, и асинхронность теряется.
Это частая ошибка: код выглядит правильно, но метод выполняется синхронно, что приводит к неожиданным задержкам и блокировкам.
Решение: Никогда не вызывайте @Async методы напрямую внутри того же класса.
Используйте self-injection (внедрение ссылки на самого себя через @Autowired/@jakarta.inject.Inject) или вынесите асинхронную логику в отдельный @Service-компонент.
Self-injection заставляет обращаться к прокси, а не к реальному объекту.
Объяснение: Spring создает прокси-объект, который перехватывает вызовы @Async методов и передает их в пул потоков.
Когда бин внедряет сам себя (selfProxy), он получает именно прокси, а не исходный объект. Вызов через selfProxy.asyncMethod() корректно обрабатывается перехватчиком. Self-injection работает благодаря тому, что Spring после создания бина внедряет его же прокси (если включен режим @EnableAsync и прокси-таргет класс). Для этого требуется либо использовать интерфейсы (JDK proxy), либо CGLIB (proxy-target-class=true).
Альтернативное и более чистое решение — вынести асинхронный метод в отдельный бин, что полностью исключает проблему внутренних вызовов.
Разработчик всегда проверяет, через какой объект вызывается @Async метод, и избегает вызовов через this.
#Java #советы
Тема: @Async по умолчанию не работает внутри того же класса. Вызов асинхронного метода из другого метода этого же класса не пройдет через Spring Proxy.
Проблема: Аннотация @Async работает благодаря Spring AOP, который создает прокси вокруг целевого бина.
При вызове метода через прокси (например, из другого бина) запускается асинхронное выполнение. Однако при прямом вызове из того же класса через this прокси не участвует — вызывается оригинальный метод, и асинхронность теряется.
Это частая ошибка: код выглядит правильно, но метод выполняется синхронно, что приводит к неожиданным задержкам и блокировкам.
Решение: Никогда не вызывайте @Async методы напрямую внутри того же класса.
Используйте self-injection (внедрение ссылки на самого себя через @Autowired/@jakarta.inject.Inject) или вынесите асинхронную логику в отдельный @Service-компонент.
Self-injection заставляет обращаться к прокси, а не к реальному объекту.
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.concurrent.CompletableFuture;
@Service
public class AsyncService {
@Autowired
private AsyncService selfProxy; // self-injection
@Async
public CompletableFuture<String> asyncMethod() {
System.out.println("Асинхронный вызов в потоке: " + Thread.currentThread().getName());
return CompletableFuture.completedFuture("Результат");
}
//Проблема: прямой вызов
public void badCall() {
System.out.println("Прямой вызов");
asyncMethod(); // this.asyncMethod() — асинхронности НЕТ!
}
//Решение: вызов через self-proxy
public void goodCall() {
System.out.println("Вызов через self-proxy");
selfProxy.asyncMethod(); // через прокси — асинхронно
}
}
// Альтернатива: отдельный компонент
@Service
class AsyncWorker {
@Async
public CompletableFuture<String> doWork() {
return CompletableFuture.completedFuture("Работа выполнена");
}
}
@Service
class CallerService {
@Autowired
private AsyncWorker worker;
public void call() {
worker.doWork(); // асинхронно через прокси
}
}
Объяснение: Spring создает прокси-объект, который перехватывает вызовы @Async методов и передает их в пул потоков.
Когда бин внедряет сам себя (selfProxy), он получает именно прокси, а не исходный объект. Вызов через selfProxy.asyncMethod() корректно обрабатывается перехватчиком. Self-injection работает благодаря тому, что Spring после создания бина внедряет его же прокси (если включен режим @EnableAsync и прокси-таргет класс). Для этого требуется либо использовать интерфейсы (JDK proxy), либо CGLIB (proxy-target-class=true).
Альтернативное и более чистое решение — вынести асинхронный метод в отдельный бин, что полностью исключает проблему внутренних вызовов.
Разработчик всегда проверяет, через какой объект вызывается @Async метод, и избегает вызовов через this.
#Java #советы
👍5
Что выведет код?
#Tasks
@SpringBootApplication
@EnableAsync
public class Task130326 implements CommandLineRunner {
@Autowired
private AsyncService130326 service;
public static void main(String[] args) {
SpringApplication.run(Task130326.class, args);
}
@Override
public void run(String... args) {
service.directCall();
service.proxyCall();
}
}
@Component
class AsyncService130326 {
@Lazy
@Autowired
private AsyncService130326 self;
@Async
public void asyncMethod() {
System.out.println("asyncMethod: " + Thread.currentThread().getName());
}
public void directCall() {
System.out.println("directCall: " + Thread.currentThread().getName());
asyncMethod();
}
public void proxyCall() {
System.out.println("proxyCall: " + Thread.currentThread().getName());
self.asyncMethod();
}
}
#Tasks
👍2
Что такое Java Memory Model (JMM)? 🤓
Ответ:
Java Memory Model (JMM) — это формальная модель, описывающая, как потоки взаимодействуют через память и какое поведение можно ожидать в многопоточной среде.
JMM определяет правила, когда изменения, сделанные одним потоком, становятся видимыми для других потоков. Она описывает отношение "happens-before", которое гарантирует видимость и упорядоченность действий. Без JMM компилятор и процессор могли бы переупорядочивать инструкции так, что многопоточный код работал бы непредсказуемо.
Ключевые слова synchronized, volatile, final обеспечивают определенные гарантии видимости в рамках JMM.
#собеседование
Ответ:
JMM определяет правила, когда изменения, сделанные одним потоком, становятся видимыми для других потоков. Она описывает отношение "happens-before", которое гарантирует видимость и упорядоченность действий. Без JMM компилятор и процессор могли бы переупорядочивать инструкции так, что многопоточный код работал бы непредсказуемо.
Ключевые слова synchronized, volatile, final обеспечивают определенные гарантии видимости в рамках JMM.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
История технологий сегодня — 14 марта
ℹ️ Кто родился в этот день
Станислав Алексеевич Кудж (род. 14 марта 1979, Москва, РСФСР, СССР) — российский учёный, специалист в области автоматизированных систем государственного управления, многопрофильных и многофункциональных информационных систем.
🌐 Знаковые события
1956 — американская компания Ampex продемонстрировала первый в мире видеомагнитофон VR-1000.
1994 — релиз Linux версии 1.0.0.
#Biography #Birth_Date #Events #14марта
Станислав Алексеевич Кудж (род. 14 марта 1979, Москва, РСФСР, СССР) — российский учёный, специалист в области автоматизированных систем государственного управления, многопрофильных и многофункциональных информационных систем.
1956 — американская компания Ampex продемонстрировала первый в мире видеомагнитофон VR-1000.
1994 — релиз Linux версии 1.0.0.
#Biography #Birth_Date #Events #14марта
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
С 07.03 по 13.03
Предыдущий пост(с 28.02 по 06.03)
Воскресный мотивационный пост:
Не было мотивации
Запись встреч/видео:
Видео в процессе подготовки...
Обучающие статьи:
Раздел 8. Stream API и функциональный стиль
Глава 5: Контракты. Почему equals, hashCode и иммутабельность — закон
equals/hashCode в операциях distinct, groupingBy, toSet
Иммутабельность данных в потоке — не прихоть, а необходимость
Советы по Java:
[Совет по Java #013]
Тема: Аннотация @Transactional не работает на private и protected методах
[Совет по Java #014]
Тема: Инъекция через конструктор предпочтительнее инъекции в поле
[Совет по Java #015]
Тема: @Async по умолчанию не работает внутри того же класса
Полезные статьи и видео:
Java без розовых очков: какие знания отделяют грейды
Как и всегда, задачи можно найти под тегом - #Tasks, вопросы с собеседований - #собеседование
Предыдущий пост(с 28.02 по 06.03)
Воскресный мотивационный пост:
Не было мотивации
Запись встреч/видео:
Видео в процессе подготовки...
Обучающие статьи:
Раздел 8. Stream API и функциональный стиль
Глава 5: Контракты. Почему equals, hashCode и иммутабельность — закон
equals/hashCode в операциях distinct, groupingBy, toSet
Иммутабельность данных в потоке — не прихоть, а необходимость
Советы по Java:
[Совет по Java #013]
Тема: Аннотация @Transactional не работает на private и protected методах
[Совет по Java #014]
Тема: Инъекция через конструктор предпочтительнее инъекции в поле
[Совет по Java #015]
Тема: @Async по умолчанию не работает внутри того же класса
Полезные статьи и видео:
Java без розовых очков: какие знания отделяют грейды
Как и всегда, задачи можно найти под тегом - #Tasks, вопросы с собеседований - #собеседование
👍2
История технологий сегодня — 15 марта
ℹ️ Кто родился в этот день
Жоре́с Ива́нович Алфёров (бел. Жарэс Іванавіч Алфёраў; 15 марта 1930, Витебск — 1 марта 2019, Санкт-Петербург) — советский и российский учёный-физик. Политический деятель. Лауреат Нобелевской премии по физике (2000 год, за разработку полупроводниковых гетероструктур и создание быстрых опто- и микроэлектронных компонентов).
🌐 Знаковые события
1892 — американский изобретатель Джесс Рено (англ. Jesse Reno) запатентовал первый эскалатор.
1985 — зарегистрирован первый домен в зоне .com
#Biography #Birth_Date #Events #15марта
Жоре́с Ива́нович Алфёров (бел. Жарэс Іванавіч Алфёраў; 15 марта 1930, Витебск — 1 марта 2019, Санкт-Петербург) — советский и российский учёный-физик. Политический деятель. Лауреат Нобелевской премии по физике (2000 год, за разработку полупроводниковых гетероструктур и создание быстрых опто- и микроэлектронных компонентов).
1892 — американский изобретатель Джесс Рено (англ. Jesse Reno) запатентовал первый эскалатор.
1985 — зарегистрирован первый домен в зоне .com
#Biography #Birth_Date #Events #15марта
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
Вы соскучились по встречам?
Anonymous Poll
56%
Да!
4%
Нет!
0%
Похер
36%
Встреча с кем?
4%
Что я тут делаю?
👍1
История технологий сегодня — 16 марта
ℹ️ Кто родился в этот день
Ричард Мэттью Столлман (англ. Richard Matthew Stallman; род. 16 марта 1953, Манхэттен, Нью-Йорк), также упоминаемый как rms, — основатель движения свободного программного обеспечения, проекта GNU, Фонда свободного программного обеспечения и Лиги за свободу программирования. Автор концепции «копилефта», призванной защищать идеалы движения; эту концепцию он с помощью юристов позже воплотил в лицензии GNU General Public License (GNU GPL) для ПО. Ранее также известный программист. Из авторских программ можно отметить GNU Emacs, Коллекция компиляторов GNU (GCC) и Отладчик GNU (GDB). С середины 1990-х годов Столлман стал программировать значительно меньше, посвятив себя распространению идей свободного ПО.
Э́ндрю Стюарт Таненба́ум (англ. Andrew Stuart Tanenbaum; род. 16 марта 1944, Нью-Йорк, Нью-Йорк), иногда упоминаемый под псевдонимом AST или Papá Tanenbaum — голландский учёный-компьютерщик американского происхождения, почётный профессор компьютерных наук в Амстердамском свободном университете в Нидерландах. Известен как автор MINIX (свободная Unix-подобная операционная система для учебных целей) и RFID-вируса. Автор книг по информатике, которые считаются стандартами в данной области. Сам он считает свою преподавательскую деятельность наиболее важной. Является главным разработчиком пакета «Amsterdam Compiler Kit».
Фре́дерик Ра́йнес (англ. Frederick Reines; 16 марта 1918, Патерсон, штат Нью-Джерси, США — 26 августа 1998, Ориндж, штат Калифорния, США) — американский физик, профессор, лауреат Нобелевской премии по физике (1995) за открытие нейтрино.
🌐 Знаковые события
1936 — с конвейера Горьковского автомобильного завода сошла первая советская легковая автомашина — лимузин марки «М-1».
#Biography #Birth_Date #Events #16марта
Ричард Мэттью Столлман (англ. Richard Matthew Stallman; род. 16 марта 1953, Манхэттен, Нью-Йорк), также упоминаемый как rms, — основатель движения свободного программного обеспечения, проекта GNU, Фонда свободного программного обеспечения и Лиги за свободу программирования. Автор концепции «копилефта», призванной защищать идеалы движения; эту концепцию он с помощью юристов позже воплотил в лицензии GNU General Public License (GNU GPL) для ПО. Ранее также известный программист. Из авторских программ можно отметить GNU Emacs, Коллекция компиляторов GNU (GCC) и Отладчик GNU (GDB). С середины 1990-х годов Столлман стал программировать значительно меньше, посвятив себя распространению идей свободного ПО.
Э́ндрю Стюарт Таненба́ум (англ. Andrew Stuart Tanenbaum; род. 16 марта 1944, Нью-Йорк, Нью-Йорк), иногда упоминаемый под псевдонимом AST или Papá Tanenbaum — голландский учёный-компьютерщик американского происхождения, почётный профессор компьютерных наук в Амстердамском свободном университете в Нидерландах. Известен как автор MINIX (свободная Unix-подобная операционная система для учебных целей) и RFID-вируса. Автор книг по информатике, которые считаются стандартами в данной области. Сам он считает свою преподавательскую деятельность наиболее важной. Является главным разработчиком пакета «Amsterdam Compiler Kit».
Фре́дерик Ра́йнес (англ. Frederick Reines; 16 марта 1918, Патерсон, штат Нью-Джерси, США — 26 августа 1998, Ориндж, штат Калифорния, США) — американский физик, профессор, лауреат Нобелевской премии по физике (1995) за открытие нейтрино.
1936 — с конвейера Горьковского автомобильного завода сошла первая советская легковая автомашина — лимузин марки «М-1».
#Biography #Birth_Date #Events #16марта
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
Раздел 8. Stream API и функциональный стиль в Java
Глава 5: Контракты. Почему equals, hashCode и иммутабельность — закон
(Практика): Саботаж и починка в «Библиотеке»
Создание «сломанного» класса Book
Создайте или обновите класс Book с минимальными полями:
Частичная реализация equals (только по title)
Добавьте equals, сравнивающий только название:
Намеренно не добавляйте hashCode!
Демонстрация проблемы в Stream API
Проблема 1: distinct() «слипает» разные книги
Создайте тестовый метод в Library или Main:
Ожидаемый вывод:
Что произошло: distinct() использует hashCode и equals. Поскольку hashCode не переопределён, наследуется от Object — разные адреса памяти → разные хэши. Но equals говорит, что книги равны по названию.
На самом деле поведение непредсказуемо: distinct() внутри использует HashSet, который сначала сравнивает hashCode, потом equals. Если хэши разные (а они будут разные), элементы попадают в разные бакеты и никогда не сравниваются через equals. Толстой и Достоевский останутся оба!
Проверьте: Запустите код. Если обе «Войны и мира» остались — hashCode разные спасает. Если одна пропала — вы попали в коллизию (редко, но возможно).
Проблема 2: toMap и потеря значений
Создайте Map книга → количество на складе:
Анализ:
toMap использует hashCode для выбора бакета, equals для проверки существования ключа
Без переопределённого hashCode — разные объекты, разные бакеты
Даже если equals сказал бы «равны», HashMap не найдёт элемент из-за разных хэшей
lookup и lookupWrongAuthor оба вернут null или случайное значение
Глава 5: Контракты. Почему equals, hashCode и иммутабельность — закон
(Практика): Саботаж и починка в «Библиотеке»
Создание «сломанного» класса Book
Создайте или обновите класс Book с минимальными полями:
public class Book {
private final String title;
private final String author;
public Book(String title, String author) {
this.title = Objects.requireNonNull(title, "title не может быть null");
this.author = Objects.requireNonNull(author, "author не может быть null");
}
public String getTitle() { return title; }
public String getAuthor() { return author; }
@Override
public String toString() {
return "Book[title=" + title + ", author=" + author + "]";
}
}Частичная реализация equals (только по title)
Добавьте equals, сравнивающий только название:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Book)) return false;
Book book = (Book) o;
return title.equals(book.title); // Только title! Автор игнорируется.
}
Намеренно не добавляйте hashCode!
Демонстрация проблемы в Stream API
Проблема 1: distinct() «слипает» разные книги
Создайте тестовый метод в Library или Main:
public void demonstrateDistinctProblem() {
List<Book> books = Arrays.asList(
new Book("Война и мир", "Толстой"),
new Book("Война и мир", "Достоевский"), // Другой автор!
new Book("1984", "Оруэлл")
);
List<Book> distinct = books.stream()
.distinct()
.collect(Collectors.toList());
System.out.println("Исходный список: " + books.size() + " книг");
System.out.println("После distinct: " + distinct.size() + " книг");
System.out.println("Уникальные книги: " + distinct);
}Ожидаемый вывод:
Исходный список: 3 книг
После distinct: 2 книг
Уникальные книги: [Book[title=Война и мир, author=Толстой], Book[title=1984, author=Оруэлл]]
Что произошло: distinct() использует hashCode и equals. Поскольку hashCode не переопределён, наследуется от Object — разные адреса памяти → разные хэши. Но equals говорит, что книги равны по названию.
На самом деле поведение непредсказуемо: distinct() внутри использует HashSet, который сначала сравнивает hashCode, потом equals. Если хэши разные (а они будут разные), элементы попадают в разные бакеты и никогда не сравниваются через equals. Толстой и Достоевский останутся оба!
Проверьте: Запустите код. Если обе «Войны и мира» остались — hashCode разные спасает. Если одна пропала — вы попали в коллизию (редко, но возможно).
Проблема 2: toMap и потеря значений
Создайте Map книга → количество на складе:
public void demonstrateToMapProblem() {
List<Book> inventory = Arrays.asList(
new Book("Война и мир", "Толстой"),
new Book("Война и мир", "Достоевский")
);
Map<Book, Integer> stock = inventory.stream()
.collect(Collectors.toMap(
Function.identity(),
b -> 10, // количество на складе
(a, b) -> a + b // слияние при коллизии
));
System.out.println("Map: " + stock);
// Попытка получить значение по «эквивалентной» книге
Book lookup = new Book("Война и мир", "Толстой");
System.out.println("Поиск " + lookup + ": " + stock.get(lookup));
Book lookupWrongAuthor = new Book("Война и мир", "Гоголь");
System.out.println("Поиск " + lookupWrongAuthor + ": " + stock.get(lookupWrongAuthor));
}Анализ:
toMap использует hashCode для выбора бакета, equals для проверки существования ключа
Без переопределённого hashCode — разные объекты, разные бакеты
Даже если equals сказал бы «равны», HashMap не найдёт элемент из-за разных хэшей
lookup и lookupWrongAuthor оба вернут null или случайное значение
👍5
Проблема 3: groupingBy ломается
Ожидаемо: 3 группы (все разные авторы) или 1 группа (если equals/hashCode считают их одинаковыми).
Фактически: Непредсказуемо из-за отсутствующего hashCode.
Исправление: корректные equals и hashCode
Правильная реализация по всем полям
Обновите класс Book:
Контракт equals и hashCode:
Рефлексивность: x.equals(x) всегда true
Симметричность: x.equals(y) ⇔ y.equals(x)
Транзитивность: x.equals(y) и y.equals(z) → x.equals(z)
Консистентность: повторные вызовы дают тот же результат
С null: x.equals(null) всегда false
Критически: Если x.equals(y), то x.hashCode() == y.hashCode()
Проверка исправления
Повторите все демонстрации
Ожидаемый результат: 3 уникальных книги, Map с 3 записями, поиск находит значение.
#Java #для_новичков #beginner #stream_api #практика
public void demonstrateGroupingProblem() {
List<Book> books = Arrays.asList(
new Book("Война и мир", "Толстой"),
new Book("Война и мир", "Достоевский"),
new Book("Война и мир", "Тургенев")
);
Map<Book, List<Book>> byBook = books.stream()
.collect(Collectors.groupingBy(Function.identity()));
System.out.println("Групп: " + byBook.size());
byBook.forEach((k, v) -> System.out.println(k + " -> " + v.size()));
}Ожидаемо: 3 группы (все разные авторы) или 1 группа (если equals/hashCode считают их одинаковыми).
Фактически: Непредсказуемо из-за отсутствующего hashCode.
Исправление: корректные equals и hashCode
Правильная реализация по всем полям
Обновите класс Book:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Book)) return false;
Book book = (Book) o;
return title.equals(book.title) &&
author.equals(book.author); // Теперь оба поля!
}
@Override
public int hashCode() {
return Objects.hash(title, author); // Консистентно с equals
}
Контракт equals и hashCode:
Рефлексивность: x.equals(x) всегда true
Симметричность: x.equals(y) ⇔ y.equals(x)
Транзитивность: x.equals(y) и y.equals(z) → x.equals(z)
Консистентность: повторные вызовы дают тот же результат
С null: x.equals(null) всегда false
Критически: Если x.equals(y), то x.hashCode() == y.hashCode()
Проверка исправления
Повторите все демонстрации
public void demonstrateFixed() {
List<Book> books = Arrays.asList(
new Book("Война и мир", "Толстой"),
new Book("Война и мир", "Достоевский"),
new Book("1984", "Оруэлл")
);
// distinct() — теперь 3 уникальные книги
System.out.println("distinct: " + books.stream().distinct().count());
// toMap — теперь 3 записи
Map<Book, Integer> stock = books.stream()
.collect(Collectors.toMap(
Function.identity(),
b -> 10,
Integer::sum
));
System.out.println("Map size: " + stock.size());
// Поиск работает
Book lookup = new Book("Война и мир", "Толстой");
System.out.println("Найдено: " + stock.get(lookup));
}Ожидаемый результат: 3 уникальных книги, Map с 3 записями, поиск находит значение.
#Java #для_новичков #beginner #stream_api #практика
👍4
Альтернативные стратегии equals/hashCode
Стратегия 1: Только неизменяемый идентификатор (ISBN)
Если Book имеет уникальный ISBN:
Плюсы: Эффективно, надёжно. Минусы: Требует уникального поля.
Стратегия 2: Все поля
Плюсы: Корректно для value objects. Минусы: Медленнее, чувствительно к изменениям.
Стратегия 3: Не переопределять (только identity)
Если Book — entity с уникальным ID в базе данных, и вы никогда не сравниваете разные объекты с одинаковым ID:
Плюсы: Просто. Минусы: distinct(), toMap, groupingBy работают по ссылке, не по содержимому.
Практические задания
Задача 1: воспроизвести все три проблемы
В проекте «Библиотека» создайте класс BrokenBook с equals только по title и без hashCode.
Продемонстрируйте:
distinct() «проглатывает» книги или нет (зависит от хэш-коллизий)
toMap создаёт больше записей, чем ожидается
groupingBy разбивает на неожиданное количество групп
Зафиксируйте наблюдения в комментариях.
Задача 2: исправить и сравнить
Создайте FixedBook с корректной парой equals/hashCode. Повторите те же операции.
Убедитесь, что:
Толстой, Достоевский, Тургенев — три разные книги с одинаковым названием
distinct() оставляет все три
toMap создаёт три записи
Поиск по ключу находит правильное значение
Задача 3: специфический equals для бизнес-логики
Добавьте в Library метод findByTitle(String title), который находит все книги с данным названием (независимо от автора).
Реализуйте через filter с кастомным предикатом, не через equals:
Объясните, почему это безопаснее, чем менять equals для всего класса.
Задача 4: кеширование hashCode (звёздочка)
Если поля title и author неизменяемы, закешируйте hashCode:
Сравните производительность при частом использовании в HashMap для миллиона операций.
#Java #для_новичков #beginner #stream_api #практика
Стратегия 1: Только неизменяемый идентификатор (ISBN)
Если Book имеет уникальный ISBN:
private final String isbn; // уникальный, неизменяемый
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Book)) return false;
return isbn.equals(((Book) o).isbn);
}
@Override
public int hashCode() {
return isbn.hashCode();
}
Плюсы: Эффективно, надёжно. Минусы: Требует уникального поля.
Стратегия 2: Все поля
Плюсы: Корректно для value objects. Минусы: Медленнее, чувствительно к изменениям.
Стратегия 3: Не переопределять (только identity)
Если Book — entity с уникальным ID в базе данных, и вы никогда не сравниваете разные объекты с одинаковым ID:
// Нет equals/hashCode — используем Object
Плюсы: Просто. Минусы: distinct(), toMap, groupingBy работают по ссылке, не по содержимому.
Практические задания
Задача 1: воспроизвести все три проблемы
В проекте «Библиотека» создайте класс BrokenBook с equals только по title и без hashCode.
Продемонстрируйте:
distinct() «проглатывает» книги или нет (зависит от хэш-коллизий)
toMap создаёт больше записей, чем ожидается
groupingBy разбивает на неожиданное количество групп
Зафиксируйте наблюдения в комментариях.
Задача 2: исправить и сравнить
Создайте FixedBook с корректной парой equals/hashCode. Повторите те же операции.
Убедитесь, что:
Толстой, Достоевский, Тургенев — три разные книги с одинаковым названием
distinct() оставляет все три
toMap создаёт три записи
Поиск по ключу находит правильное значение
Задача 3: специфический equals для бизнес-логики
Добавьте в Library метод findByTitle(String title), который находит все книги с данным названием (независимо от автора).
Реализуйте через filter с кастомным предикатом, не через equals:
public List<Book> findByTitle(String title) {
return books.stream()
.filter(b -> b.getTitle().equals(title))
.collect(Collectors.toList());
}Объясните, почему это безопаснее, чем менять equals для всего класса.
Задача 4: кеширование hashCode (звёздочка)
Если поля title и author неизменяемы, закешируйте hashCode:
private int hashCode; // 0 = не вычислено
@Override
public int hashCode() {
int result = hashCode;
if (result == 0) {
result = Objects.hash(title, author);
hashCode = result;
}
return result;
}
Сравните производительность при частом использовании в HashMap для миллиона операций.
#Java #для_новичков #beginner #stream_api #практика
👍5