С 28.02 по 06.03
Предыдущий пост(с 21.02 по 27.02)
Воскресный мотивационный пост:
Не было мотивации
Запись встреч/видео:
8. Логирование и ELK-стек: как расследуют инциденты в production
Обучающие статьи:
Раздел 8. Stream API и функциональный стиль
Глава 4: Искусство агрегации. Коллекторы и группировки
Коллектор — это рецепт агрегации
groupingBy и многоуровневая агрегация
(Практика): Анализ библиотеки в один проход
Советы по Java:
[Совет по Java #011]
Тема: Random — потоконебезопасный и медленный для многопоточки.
[Совет по Java #012]
Тема: При итерации по Map для получения и ключа, и значения используйте entrySet(), а не keySet() с последующим get().
Полезные статьи и видео:
Модель памяти Java процесса
Как и всегда, задачи можно найти под тегом - #Tasks, вопросы с собеседований - #собеседование
Предыдущий пост(с 21.02 по 27.02)
Воскресный мотивационный пост:
Не было мотивации
Запись встреч/видео:
8. Логирование и ELK-стек: как расследуют инциденты в production
Обучающие статьи:
Раздел 8. Stream API и функциональный стиль
Глава 4: Искусство агрегации. Коллекторы и группировки
Коллектор — это рецепт агрегации
groupingBy и многоуровневая агрегация
(Практика): Анализ библиотеки в один проход
Советы по Java:
[Совет по Java #011]
Тема: Random — потоконебезопасный и медленный для многопоточки.
[Совет по Java #012]
Тема: При итерации по Map для получения и ключа, и значения используйте entrySet(), а не keySet() с последующим get().
Полезные статьи и видео:
Модель памяти Java процесса
Как и всегда, задачи можно найти под тегом - #Tasks, вопросы с собеседований - #собеседование
👍3🔥1
Java for Beginner
Как вам первая фаза нашего OrderHub? Нравится?
Можно выбрать много вариантов.
Можно выбрать много вариантов.
Как всегда все те же голосуют))
Обнял - приподнял❤️
Обнял - приподнял
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
История технологий сегодня — 08 марта
💐 Международный женский день — праздник, который отмечается ежегодно 8 марта в ряде государств и стран мира.
Появился как день солидарности женщин в борьбе за женские права и эмансипацию. В 1945 году устав Организации Объединённых Наций стал первым международным документом, утвердившим принцип равенства мужчин и женщин. С 1977 года Международный женский день отмечается государствами-членами ООН.
Всех присутствующих на канале женщин поздравляю с праздником, желаю всегда оставаться обворожительными и незаменимыми🌹
ℹ️ Кто родился в этот день
Отто Ган (нем. Otto Hahn — Отто Хан; 8 марта 1879, Франкфурт-на-Майне — 28 июля 1968, Гёттинген) — немецкий физик, учёный-новатор в области радиохимии, открывший ядерную изомерию (Уран Z) и расщепление урана. Получил Нобелевскую премию по химии за 1944 год. Гленн Сиборг назвал его «отцом ядерной химии».
Ральф Генри Бер (англ. Ralph Henry Baer; 8 марта 1922, Родальбен, Юго-западный Пфальц — 6 декабря 2014, Манчестер, Нью-Гэмпшир) — американский изобретатель, разработчик видеоигр, инженер, предприниматель, получивший прозвище «Отец видеоигр» за свой огромный вклад в развитие этой индустрии, особенно в первые годы существования компьютерных игр как таковых. В 1956 году основал собственную компанию. В ней работали до 500 инженеров, которые занимались преимущественно военными заказами. Именно эта работа подала Беру идею, которая в итоге привела к созданию первой в мире игровой приставки.
Майкл Стерн Харт (англ. Michael Stern Hart; 8 марта 1947 — 6 сентября 2011) — американский писатель, изобретатель электронных книг и основатель проекта «Гутенберг», который сделал электронные книги свободно доступными через Интернет. Большинство первых материалов он напечатал и разместил лично. В 1971 году Майкл Харт получил неограниченный доступ ко времени крупного компьютера Xerox Sigma V от операторов в университете штата Иллинойс. Пытаясь достойно применить этот ресурс, он создал первую электронную книгу Декларация независимости США, когда впечатал её текст в компьютер. Так путём создания электронных копий большего количества книг получил начало Проект «Гутенберг». Избегал расходов на врачей, предпочитая домашние средства. Собрал много компьютеров и стереосистем, другой аппаратуры, часто из вышедшего из употребления оборудования, жертвуя собственной роскошью ради борьбы с безграмотностью и сохранения доступа общественности к информационным ресурсам. Вёл жизнь, близкую к бедности, «питаясь, в основном, бобовыми консервами».
🌐 Знаковые события
1950 — СССР объявил о наличии атомной бомбы.
1979 — компания Philips представляет компакт-диск.
1979 — изображения, сделанные Вояджером-I, доказали существование вулканизма на Ио, спутнике Юпитера.
#Biography #Birth_Date #Events #08марта
Появился как день солидарности женщин в борьбе за женские права и эмансипацию. В 1945 году устав Организации Объединённых Наций стал первым международным документом, утвердившим принцип равенства мужчин и женщин. С 1977 года Международный женский день отмечается государствами-членами ООН.
Всех присутствующих на канале женщин поздравляю с праздником, желаю всегда оставаться обворожительными и незаменимыми
Отто Ган (нем. Otto Hahn — Отто Хан; 8 марта 1879, Франкфурт-на-Майне — 28 июля 1968, Гёттинген) — немецкий физик, учёный-новатор в области радиохимии, открывший ядерную изомерию (Уран Z) и расщепление урана. Получил Нобелевскую премию по химии за 1944 год. Гленн Сиборг назвал его «отцом ядерной химии».
Ральф Генри Бер (англ. Ralph Henry Baer; 8 марта 1922, Родальбен, Юго-западный Пфальц — 6 декабря 2014, Манчестер, Нью-Гэмпшир) — американский изобретатель, разработчик видеоигр, инженер, предприниматель, получивший прозвище «Отец видеоигр» за свой огромный вклад в развитие этой индустрии, особенно в первые годы существования компьютерных игр как таковых. В 1956 году основал собственную компанию. В ней работали до 500 инженеров, которые занимались преимущественно военными заказами. Именно эта работа подала Беру идею, которая в итоге привела к созданию первой в мире игровой приставки.
Майкл Стерн Харт (англ. Michael Stern Hart; 8 марта 1947 — 6 сентября 2011) — американский писатель, изобретатель электронных книг и основатель проекта «Гутенберг», который сделал электронные книги свободно доступными через Интернет. Большинство первых материалов он напечатал и разместил лично. В 1971 году Майкл Харт получил неограниченный доступ ко времени крупного компьютера Xerox Sigma V от операторов в университете штата Иллинойс. Пытаясь достойно применить этот ресурс, он создал первую электронную книгу Декларация независимости США, когда впечатал её текст в компьютер. Так путём создания электронных копий большего количества книг получил начало Проект «Гутенберг». Избегал расходов на врачей, предпочитая домашние средства. Собрал много компьютеров и стереосистем, другой аппаратуры, часто из вышедшего из употребления оборудования, жертвуя собственной роскошью ради борьбы с безграмотностью и сохранения доступа общественности к информационным ресурсам. Вёл жизнь, близкую к бедности, «питаясь, в основном, бобовыми консервами».
1950 — СССР объявил о наличии атомной бомбы.
1979 — компания Philips представляет компакт-диск.
1979 — изображения, сделанные Вояджером-I, доказали существование вулканизма на Ио, спутнике Юпитера.
#Biography #Birth_Date #Events #08марта
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
История технологий сегодня — 09 марта
ℹ️ Кто родился в этот день
Ю́рий Алексе́евич Гага́рин (9 марта 1934, Клушино, Гжатский (ныне Гагаринский) район, Западная область (ныне — Смоленская область) — 27 марта 1968, возле села Новосёлово, Киржачский район, Владимирская область) — советский космонавт и военный лётчик, первый человек, совершивший космический полёт. Герой Советского Союза, кавалер высших знаков отличия ряда государств, почётный гражданин многих российских и зарубежных городов.
Го́вард Ха́тауэй Э́йкен (англ. Howard Hathaway Aiken; 9 марта 1900, Хобокен, штат Нью-Джерси, США — 14 марта 1973, Сент-Луис, штат Миссури, США) — американский пионер компьютеростроения. В должности инженера IBM руководил работами по созданию первого американского компьютера «Марк I».
Эндрю Джеймс Витерби (англ. Andrew James Viterbi, имя при рождении Андреа Джакомо Витерби (итал. Andrea Giacomo Viterbi); род. 9 марта 1935 года, Бергамо, Королевство Италия) — американский инженер и бизнесмен, сооснователь Qualcomm и разработчик алгоритма Витерби.
🌐 Знаковые события
1937 — Московский телецентр на Шаболовке осуществил первую в СССР опытную передачу электронного телевидения в эфир.
1948 — на циклотроне в Калифорнии удаётся получить мезоны.
#Biography #Birth_Date #Events #09марта
Ю́рий Алексе́евич Гага́рин (9 марта 1934, Клушино, Гжатский (ныне Гагаринский) район, Западная область (ныне — Смоленская область) — 27 марта 1968, возле села Новосёлово, Киржачский район, Владимирская область) — советский космонавт и военный лётчик, первый человек, совершивший космический полёт. Герой Советского Союза, кавалер высших знаков отличия ряда государств, почётный гражданин многих российских и зарубежных городов.
Го́вард Ха́тауэй Э́йкен (англ. Howard Hathaway Aiken; 9 марта 1900, Хобокен, штат Нью-Джерси, США — 14 марта 1973, Сент-Луис, штат Миссури, США) — американский пионер компьютеростроения. В должности инженера IBM руководил работами по созданию первого американского компьютера «Марк I».
Эндрю Джеймс Витерби (англ. Andrew James Viterbi, имя при рождении Андреа Джакомо Витерби (итал. Andrea Giacomo Viterbi); род. 9 марта 1935 года, Бергамо, Королевство Италия) — американский инженер и бизнесмен, сооснователь Qualcomm и разработчик алгоритма Витерби.
1937 — Московский телецентр на Шаболовке осуществил первую в СССР опытную передачу электронного телевидения в эфир.
1948 — на циклотроне в Калифорнии удаётся получить мезоны.
#Biography #Birth_Date #Events #09марта
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
[Совет по Java #013]
Тема: Аннотация @Transactional не работает на private и protected методах. Spring использует AOP-прокси, которые не могут переопределить не-public методы.
Проблема: Многие разработчики ошибочно полагают, что достаточно поставить @Transactional над любым методом, и Spring магическим образом обеспечит управление транзакциями.
Однако механизм работы Spring Transactions основан на AOP-прокси (динамических прокси JDK или CGLIB-прокси). Прокси — это подкласс или реализация интерфейса целевого класса, который переопределяет публичные методы для добавления сквозной функциональности (открытие/закрытие транзакции).
Если метод помечен как private или protected, прокси не может его переопределить, и аннотация игнорируется. Метод выполняется без транзакционного контекста, что приводит к трудноотлавливаемым багам: данные сохраняются неполностью, отсутствует откат при исключениях, соединения не закрываются.
Решение: Всегда используйте @Transactional только на public методах.
Транзакционность должна применяться на уровне сервиса (фасада), а не на внутренних вспомогательных методах. Если вам нужно вызвать транзакционный метод из того же класса, используйте self-injection (внедрение ссылки на самого себя через @Autowired/@jakarta.inject.Inject) или выделите транзакционную логику в отдельный @Service-компонент. В Spring 4.3+ можно использовать @Autowired для внедрения прокси самого себя.
Объяснение: Spring создает прокси вокруг целевого бина.
При вызове метода через прокси сначала выполняется транзакционный перехватчик, который открывает транзакцию, затем вызывается оригинальный метод, после чего транзакция коммитится или откатывается. При вызове private метода прокси не участвует — вызов идет напрямую к целевому объекту (this). Self-injection решает эту проблему, заставляя обращаться к самому себе через прокси.
Важно понимать, что для работы @Transactional нужен публичный метод, управление транзакциями на уровне контейнера и правильная конфигурация прокси (по умолчанию — JDK dynamic proxy, требующий интерфейса, или CGLIB, работающий с классами).
#Java #советы
Тема: Аннотация @Transactional не работает на private и protected методах. Spring использует AOP-прокси, которые не могут переопределить не-public методы.
Проблема: Многие разработчики ошибочно полагают, что достаточно поставить @Transactional над любым методом, и Spring магическим образом обеспечит управление транзакциями.
Однако механизм работы Spring Transactions основан на AOP-прокси (динамических прокси JDK или CGLIB-прокси). Прокси — это подкласс или реализация интерфейса целевого класса, который переопределяет публичные методы для добавления сквозной функциональности (открытие/закрытие транзакции).
Если метод помечен как private или protected, прокси не может его переопределить, и аннотация игнорируется. Метод выполняется без транзакционного контекста, что приводит к трудноотлавливаемым багам: данные сохраняются неполностью, отсутствует откат при исключениях, соединения не закрываются.
Решение: Всегда используйте @Transactional только на public методах.
Транзакционность должна применяться на уровне сервиса (фасада), а не на внутренних вспомогательных методах. Если вам нужно вызвать транзакционный метод из того же класса, используйте self-injection (внедрение ссылки на самого себя через @Autowired/@jakarta.inject.Inject) или выделите транзакционную логику в отдельный @Service-компонент. В Spring 4.3+ можно использовать @Autowired для внедрения прокси самого себя.
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.support.TransactionSynchronizationManager;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private UserService selfProxy; // Self-injection для вызова из того же класса
//Антипаттерн: @Transactional на private методе
@Transactional
private void badPrivateMethod(User user) {
// Транзакции НЕ БУДЕТ! Метод не переопределяется прокси
userRepository.save(user);
if (user.getEmail() == null) {
throw new RuntimeException("Ошибка — отката не будет!");
}
}
//Правильно: public метод
@Transactional
public void goodPublicMethod(User user) {
userRepository.save(user);
if (user.getEmail() == null) {
throw new RuntimeException("Ошибка — транзакция откатится");
}
}
// Проблема внутреннего вызова
public void callInternal() {
User user = new User("John");
//Транзакции не будет — вызов напрямую, не через прокси
goodPublicMethod(user); // ФАКТИЧЕСКИ: this.goodPublicMethod()
}
//Решение: вызов через self-proxy
public void callCorrectly() {
User user = new User("John");
selfProxy.goodPublicMethod(user); // Вызов через прокси — транзакция работает
}
}
// Альтернативное решение: вынос логики в отдельный компонент
@Service
class InternalUserService {
@Transactional
public void doInTransaction(User user) {
// транзакционная логика
}
}
@Service
class MainUserService {
@Autowired
private InternalUserService internalService;
public void call() {
internalService.doInTransaction(new User("John")); // Транзакция работает
}
}
Объяснение: Spring создает прокси вокруг целевого бина.
При вызове метода через прокси сначала выполняется транзакционный перехватчик, который открывает транзакцию, затем вызывается оригинальный метод, после чего транзакция коммитится или откатывается. При вызове private метода прокси не участвует — вызов идет напрямую к целевому объекту (this). Self-injection решает эту проблему, заставляя обращаться к самому себе через прокси.
Важно понимать, что для работы @Transactional нужен публичный метод, управление транзакциями на уровне контейнера и правильная конфигурация прокси (по умолчанию — JDK dynamic proxy, требующий интерфейса, или CGLIB, работающий с классами).
#Java #советы
👍5
Что выведет код?
#Tasks
@SpringBootApplication
public class TransactionalTricky implements CommandLineRunner {
@Autowired
private UserService userService;
public static void main(String[] args) {
SpringApplication.run(TransactionalTricky.class, args);
}
@Override
public void run(String... args) {
userService.saveUser();
System.out.println(userService.getCounter());
}
@Service
static class UserService {
private int counter = 0;
@Transactional
public void saveUser() {
privateIncrement();
}
@Transactional
private void privateIncrement() {
counter++;
}
public int getCounter() {
return counter;
}
}
}
#Tasks
👍3
👍4
Что такое ConcurrentHashMap и чем он лучше Hashtable? 🤓
Ответ:
ConcurrentHashMap из пакета java.util.concurrent — это потокобезопасная реализация Map, оптимизированная для многопоточной среды.
В отличие от Hashtable, которая синхронизирует все методы (блокируя всю таблицу), ConcurrentHashMap использует более тонкую блокировку.
В Java 7 — сегментирование, в Java 8+ блокируются отдельные элементы корзины (или используется Compare-And-Swap). Это позволяет параллельно выполнять чтение и запись в разные сегменты/корзины. Он также не выбрасывает ConcurrentModificationException при итерации.
#собеседование
Ответ:
В отличие от Hashtable, которая синхронизирует все методы (блокируя всю таблицу), ConcurrentHashMap использует более тонкую блокировку.
В Java 7 — сегментирование, в Java 8+ блокируются отдельные элементы корзины (или используется Compare-And-Swap). Это позволяет параллельно выполнять чтение и запись в разные сегменты/корзины. Он также не выбрасывает ConcurrentModificationException при итерации.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
История технологий сегодня — 10 марта
ℹ️ Кто родился в этот день
Евге́ний Ла́заревич Роша́л (род. 10 марта 1972, Челябинск) — российский программист, автор файлового менеджера FAR Manager, формата сжатия RAR, архиваторов RAR и WinRAR.
🌐 Знаковые события
1977 — несколько групп астрономов США, Австралии, Индии, ЮАР открыли кольца Урана.
1982 — редкий парад планет — все девять планет (Меркурий, Венера, Земля, Марс, Юпитер, Сатурн, Уран, Нептун и Плутон) собрались по одну сторону от Солнца в секторе с углом 95 градусов (то есть, максимальная разность гелиоцентрических эклиптических долгот планет составила 95 градусов).
2006 — автоматическая межпланетная станция Mars Reconnaissance Orbiter достигла Марса.
#Biography #Birth_Date #Events #10марта
Евге́ний Ла́заревич Роша́л (род. 10 марта 1972, Челябинск) — российский программист, автор файлового менеджера FAR Manager, формата сжатия RAR, архиваторов RAR и WinRAR.
1977 — несколько групп астрономов США, Австралии, Индии, ЮАР открыли кольца Урана.
1982 — редкий парад планет — все девять планет (Меркурий, Венера, Земля, Марс, Юпитер, Сатурн, Уран, Нептун и Плутон) собрались по одну сторону от Солнца в секторе с углом 95 градусов (то есть, максимальная разность гелиоцентрических эклиптических долгот планет составила 95 градусов).
2006 — автоматическая межпланетная станция Mars Reconnaissance Orbiter достигла Марса.
#Biography #Birth_Date #Events #10марта
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Раздел 8. Stream API и функциональный стиль в Java
Глава 5: Контракты. Почему equals, hashCode и иммутабельность — закон
equals/hashCode в операциях distinct, groupingBy, toSet
Stream API не существует в вакууме. Он опирается на фундаментальные контракты Java-объектов, установленные классом Object: equals для логического сравнения, hashCode для хеширования, compareTo для упорядочивания.
Нарушение этих контрактов не всегда приводит к немедленным ошибкам компиляции или очевидным исключениям. Вместо этого порождаются "мистические" баги: данные исчезают, дублируются, группируются неправильно — и всё это проявляется недетерминированно, часто только под нагрузкой или при смене окружения.
Рассмотрим класс Book, разработанный без должного внимания к контрактам:
По умолчанию equals и hashCode наследуются от Object, реализуя идентичность по ссылке: два объекта равны только если это один и тот же экземпляр в памяти. Для доменной модели, где логическая эквивалентность определяется содержимым, это поведение некорректно.
groupingBy и распределение по бакетам
Коллектор groupingBy использует HashMap для организации групп. При добавлении элемента вычисляется хеш-код ключа, определяющий бакет (сегмент внутреннего массива HashMap). Если в бакете уже есть элементы, выполняется проверка equals для разрешения коллизий.
Проблема возникает при использовании ключа без корректного hashCode:
Без переопределённого hashCode book1 и book2 имеют разные хеш-коды (по умолчанию System.identityHashCode), вычисляемые из адресов памяти. Они попадают в разные бакеты HashMap. Внутри каждого бакета equals возвращает false, потому что сравнение по ссылке даёт отрицательный результат. Результат: две записи в Map вместо одной, счётчики показывают 2 для каждой "уникальной" книги.
Баг проявляется недетерминированно. При небольшом числе элементов HashMap может поместить оба объекта в один бакет случайно (из-за пересечения хешей по модулю размера таблицы), и equals обнаружит их различность, но коллизия разрешится корректно — повезло. При росте данных таблица расширяется, бакеты становятся уже, вероятность коллизии падает, баг становится стабильным. При смене JVM (разная версия, разная реализация Object.hashCode) хеш-коды меняются, и поведение меняется без изменения кода.
distinct: фильтрация дубликатов без гарантий
Операция distinct() использует LinkedHashSet для отслеживания уникальных элементов. Она полагается на тот же контракт equals/hashCode:
Без корректных методов distinct не удаляет логические дубликаты, только ссылочные. Если book1 и book2 — разные объекты с одинаковым содержимым, оба попадут в результат. Память растёт, кэширование не работает.
Особенно опасно это в распределённых системах: объект десериализуется в разных узлах, создавая разные экземпляры в памяти. Без equals/hashCode по значению они воспринимаются как разные сущности.
toSet и потеря элементов
Collectors.toSet() возвращает HashSet, основанный на хеш-таблице. Некорректный hashCode ведёт к дублированию:
HashSet добавляет элемент, если в соответствующем бакете нет равного (по equals). Разные хеш-коды → разные бакеты → оба элемента добавлены. Множество перестаёт быть множеством.
#Java #для_новичков #beginner #stream_api #equals_hashCode
Глава 5: Контракты. Почему equals, hashCode и иммутабельность — закон
equals/hashCode в операциях distinct, groupingBy, toSet
Stream API не существует в вакууме. Он опирается на фундаментальные контракты Java-объектов, установленные классом Object: equals для логического сравнения, hashCode для хеширования, compareTo для упорядочивания.
Нарушение этих контрактов не всегда приводит к немедленным ошибкам компиляции или очевидным исключениям. Вместо этого порождаются "мистические" баги: данные исчезают, дублируются, группируются неправильно — и всё это проявляется недетерминированно, часто только под нагрузкой или при смене окружения.
Рассмотрим класс Book, разработанный без должного внимания к контрактам:
public class Book {
private final String title;
private final String author;
private final int year;
// Конструктор, геттеры
// equals и hashCode НЕ переопределены!
}По умолчанию equals и hashCode наследуются от Object, реализуя идентичность по ссылке: два объекта равны только если это один и тот же экземпляр в памяти. Для доменной модели, где логическая эквивалентность определяется содержимым, это поведение некорректно.
groupingBy и распределение по бакетам
Коллектор groupingBy использует HashMap для организации групп. При добавлении элемента вычисляется хеш-код ключа, определяющий бакет (сегмент внутреннего массива HashMap). Если в бакете уже есть элементы, выполняется проверка equals для разрешения коллизий.
Проблема возникает при использовании ключа без корректного hashCode:
// Две логически идентичные книги
Book book1 = new Book("1984", "George Orwell", 1949);
Book book2 = new Book("1984", "George Orwell", 1949);
// Создаём поток с дублированием
List<Book> library = Arrays.asList(book1, book1, book2, book2);
// Группировка по книге как ключу
Map<Book, Long> countByBook = library.stream()
.collect(groupingBy(Function.identity(), counting()));
System.out.println(countByBook.size()); // Ожидаем 1, получаем...?
Без переопределённого hashCode book1 и book2 имеют разные хеш-коды (по умолчанию System.identityHashCode), вычисляемые из адресов памяти. Они попадают в разные бакеты HashMap. Внутри каждого бакета equals возвращает false, потому что сравнение по ссылке даёт отрицательный результат. Результат: две записи в Map вместо одной, счётчики показывают 2 для каждой "уникальной" книги.
Баг проявляется недетерминированно. При небольшом числе элементов HashMap может поместить оба объекта в один бакет случайно (из-за пересечения хешей по модулю размера таблицы), и equals обнаружит их различность, но коллизия разрешится корректно — повезло. При росте данных таблица расширяется, бакеты становятся уже, вероятность коллизии падает, баг становится стабильным. При смене JVM (разная версия, разная реализация Object.hashCode) хеш-коды меняются, и поведение меняется без изменения кода.
distinct: фильтрация дубликатов без гарантий
Операция distinct() использует LinkedHashSet для отслеживания уникальных элементов. Она полагается на тот же контракт equals/hashCode:
List<Book> uniqueBooks = library.stream()
.distinct()
.collect(toList());
Без корректных методов distinct не удаляет логические дубликаты, только ссылочные. Если book1 и book2 — разные объекты с одинаковым содержимым, оба попадут в результат. Память растёт, кэширование не работает.
Особенно опасно это в распределённых системах: объект десериализуется в разных узлах, создавая разные экземпляры в памяти. Без equals/hashCode по значению они воспринимаются как разные сущности.
toSet и потеря элементов
Collectors.toSet() возвращает HashSet, основанный на хеш-таблице. Некорректный hashCode ведёт к дублированию:
Set<Book> bookSet = library.stream()
.collect(toSet());
System.out.println(bookSet.size()); // Ожидаем 1, получаем 2
HashSet добавляет элемент, если в соответствующем бакете нет равного (по equals). Разные хеш-коды → разные бакеты → оба элемента добавлены. Множество перестаёт быть множеством.
#Java #для_новичков #beginner #stream_api #equals_hashCode
👍5
Иммутабельность ключей: бомба замедленного действия
Даже при корректных equals/hashCode опасность подстерегает в изменяемых ключах. Рассмотрим сценарий с составным ключом-автором:
HashMap хранит ключ в бакете, соответствующем хеш-коду на момент вставки. При изменении полей name или country hashCode объекта меняется, но позиция в таблице — нет. get(key) вычисляет новый хеш, ищет в другом бакете, не находит совпадения, возвращает null. Данные не потеряны физически (объект Author и список Book существуют в памяти), но недоступны через Map.
Это "утечка" памяти логического характера: структура данных разрастается, но поиск перестаёт работать. Баг проявляется далеко от места мутации, диагностика требует глубокого понимания работы хеш-таблиц.
Правильная реализация контрактов
Для Book корректные equals и hashCode должны учитывать все значимые поля:
Ключевые требования к контракту:
Рефлексивность: x.equals(x) всегда true
Симметричность: x.equals(y) тогда и только тогда, когда y.equals(x)
Транзитивность: если x.equals(y) и y.equals(z), то x.equals(z)
Консистентность: многократные вызовы дают один результат (требует иммутабельности)
Согласованность с hashCode: равные объекты имеют равные хеш-коды
Последнее требование критично для HashMap/HashSet. Нарушение (equals true, но hashCode разный) делает коллекции неработоспособными.
Рекорды как решение
Java Records (14+, стабильно в 16+) автоматически генерируют корректные equals, hashCode, toString по всем компонентам, гарантируя иммутабельность:
Использование records для ключей groupingBy, элементов distinct, элементов toSet устраняет класс ошибок, связанных с контрактами. Единственное ограничение: компоненты records также должны быть иммутабельными и с корректными контрактами. Если Author в Book — изменяемый класс, проблема сохраняется.
Диагностика в production
При подозрении на нарушение контрактов:
Логирование хеш-кодов: вывести System.identityHashCode(obj) и obj.hashCode() для "одинаковых" объектов. Различие указывает на проблему.
Проверка equals: убедиться, что obj1.equals(obj2) возвращает true для логически эквивалентных объектов.
Анализ мутаций: установить точки останова на сеттеры ключей, используемых в Map.
Использование LinkedHashMap/LinkedHashSet: сохраняют порядок вставки, облегчая отладку.
#Java #для_новичков #beginner #stream_api #equals_hashCode
Даже при корректных equals/hashCode опасность подстерегает в изменяемых ключах. Рассмотрим сценарий с составным ключом-автором:
public class Author {
private String name; // Не final! Сеттер есть!
private String country;
// equals и hashCode по всем полям
@Override public int hashCode() {
return Objects.hash(name, country);
}
}
// Группировка книг по автору
Map<Author, List<Book>> byAuthor = library.stream()
.collect(groupingBy(Book::author));
// Позже в коде...
Author key = byAuthor.keySet().iterator().next();
key.setName("Modified Name"); // Мутация ключа!
// Попытка доступа
List<Book> books = byAuthor.get(key); // null?!HashMap хранит ключ в бакете, соответствующем хеш-коду на момент вставки. При изменении полей name или country hashCode объекта меняется, но позиция в таблице — нет. get(key) вычисляет новый хеш, ищет в другом бакете, не находит совпадения, возвращает null. Данные не потеряны физически (объект Author и список Book существуют в памяти), но недоступны через Map.
Это "утечка" памяти логического характера: структура данных разрастается, но поиск перестаёт работать. Баг проявляется далеко от места мутации, диагностика требует глубокого понимания работы хеш-таблиц.
Правильная реализация контрактов
Для Book корректные equals и hashCode должны учитывать все значимые поля:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return year == book.year &&
Objects.equals(title, book.title) &&
Objects.equals(author, book.author);
}
@Override
public int hashCode() {
return Objects.hash(title, author, year);
}
Ключевые требования к контракту:
Рефлексивность: x.equals(x) всегда true
Симметричность: x.equals(y) тогда и только тогда, когда y.equals(x)
Транзитивность: если x.equals(y) и y.equals(z), то x.equals(z)
Консистентность: многократные вызовы дают один результат (требует иммутабельности)
Согласованность с hashCode: равные объекты имеют равные хеш-коды
Последнее требование критично для HashMap/HashSet. Нарушение (equals true, но hashCode разный) делает коллекции неработоспособными.
Рекорды как решение
Java Records (14+, стабильно в 16+) автоматически генерируют корректные equals, hashCode, toString по всем компонентам, гарантируя иммутабельность:
public record Book(String title, String author, int year) {}
// Автоматически: final поля, конструктор, геттеры, equals, hashCode, toStringИспользование records для ключей groupingBy, элементов distinct, элементов toSet устраняет класс ошибок, связанных с контрактами. Единственное ограничение: компоненты records также должны быть иммутабельными и с корректными контрактами. Если Author в Book — изменяемый класс, проблема сохраняется.
Диагностика в production
При подозрении на нарушение контрактов:
Логирование хеш-кодов: вывести System.identityHashCode(obj) и obj.hashCode() для "одинаковых" объектов. Различие указывает на проблему.
Проверка equals: убедиться, что obj1.equals(obj2) возвращает true для логически эквивалентных объектов.
Анализ мутаций: установить точки останова на сеттеры ключей, используемых в Map.
Использование LinkedHashMap/LinkedHashSet: сохраняют порядок вставки, облегчая отладку.
#Java #для_новичков #beginner #stream_api #equals_hashCode
👍7
Что выведет код?
#Tasks
import java.util.*;
import java.util.stream.Collectors;
public class Task100326 {
public static void main(String[] args) {
List<Item100326> items = Arrays.asList(
new Item100326(1, "A"),
new Item100326(1, "B"),
new Item100326(2, "C"),
new Item100326(2, "D")
);
Set<Item100326> set = new LinkedHashSet<>(items);
Map<Integer, List<Item100326>> grouped = items.stream()
.collect(Collectors.groupingBy(Item100326::getId));
System.out.print(set.size() + " ");
System.out.print(grouped.get(1).size() + " ");
System.out.print(grouped.get(2).get(0).getName());
}
static class Item100326 {
private int id;
private String name;
Item100326(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() { return id; }
public String getName() { return name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Item100326 item = (Item100326) o;
return id == item.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
}
#Tasks
👍3
👍3
Что такое аннотации (Annotations) и как создать свою? 🤓
Ответ:
Аннотации — это форма метаданных, добавляемая в код (классы, методы, поля) для передачи информации компилятору, инструментам сборки или фреймворкам во время выполнения.
Они начинаются с @.
Стандартные аннотации: @Override , @Deprecated , @SuppressWarnings .
Чтобы создать свою аннотацию, используют ключевое слово @interface . Можно задать мета-аннотации: @Retention (когда доступна: SOURCE, CLASS, RUNTIME), @Target (к чему применять), @Inherited и др.
Аннотации могут содержать элементы (методы без параметров и тела).
#собеседование
Ответ:
Они начинаются с @.
Стандартные аннотации:
Чтобы создать свою аннотацию, используют ключевое слово
Аннотации могут содержать элементы (методы без параметров и тела).
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
История технологий сегодня — 11 марта
ℹ️ Кто родился в этот день
Джозеф Карл Робнетт Ликлайдер (англ. Joseph Carl Robnett Licklider; 11 марта 1915, Сент-Луис, штат Миссури, США — 26 июня 1990, Арлингтон, штат Массачусетс, США) — известный в научной и ИТ-среде как J.C.R. или «Лик» («Lick») — американский учёный. Ранние работы были посвящены психоакустике, последующие работы — сфере информационных технологий. Один из создателей сети ARPANET, прототипа Интернета.
В 1962—1964 годы работал в ARPA, заложил основы ARPANET. Высказал идею необходимости создания объединения компьютеров в сеть со свободным доступом любого человека из любого места мира к её ресурсам. Ликлайдера называют духовным отцом всемирной сети, человеком, посеявшим семена Интернета.
Вклад Ликлайдера в возникновение Интернета огромен, он состоит из идей и принципов, а не из изобретений и технологий. Ликлайдер предвидел необходимость объединения в сеть компьютеров, имеющих простые пользовательские интерфейсы. Его идеи предвосхитили компьютерную графику, интерфейсы, работающие по принципу указания и выбора (point-and-click), цифровые библиотеки, электронную коммерцию (e-commerce), дистанционное банковское обслуживание (online banking), а также программное обеспечение, размещаемое в сети. В США его считают «Джонни Эпплсидом программирования».
Вэни́вар[a] Буш (англ. Vannevar Bush /væˈniːvɑr/, 11 марта 1890, Эверетт, Массачусетс — 28 июня 1974, Белмонт, Массачусетс) — американский ученый, инженер, разработчик аналоговых компьютеров, методолог и организатор научных исследований и научного сообщества. Советник по науке при президенте Рузвельте. Автор статьи «Как мы можем мыслить», в которой предложил прообраз гипертекстового устройства Memex (интересное дальновидение - мое прим.).
Урбе́н Жан Жозе́ф Леверье́ (фр. Urbain Jean Joseph Le Verrier; 11 марта 1811, Сен-Ло — 23 сентября 1877, Париж) — французский математик, занимавшийся небесной механикой, бо́льшую часть своей жизни проработавший в Парижской обсерватории. Его наиболее известным достижением является предсказание существования планеты Нептун, сделанное с помощью математического анализа астрономических наблюдений. По предложению Франсуа Араго он выполнил вычисления для объяснения несоответствий между наблюдаемой орбитой Урана и той, которая должна быть согласно законам Кеплера и Ньютона.
🌐 Знаковые события
1878 — Французской Академии продемонстрирован фонограф, но изобретение было объявлено шарлатанством.
#Biography #Birth_Date #Events #11марта
Джозеф Карл Робнетт Ликлайдер (англ. Joseph Carl Robnett Licklider; 11 марта 1915, Сент-Луис, штат Миссури, США — 26 июня 1990, Арлингтон, штат Массачусетс, США) — известный в научной и ИТ-среде как J.C.R. или «Лик» («Lick») — американский учёный. Ранние работы были посвящены психоакустике, последующие работы — сфере информационных технологий. Один из создателей сети ARPANET, прототипа Интернета.
В 1962—1964 годы работал в ARPA, заложил основы ARPANET. Высказал идею необходимости создания объединения компьютеров в сеть со свободным доступом любого человека из любого места мира к её ресурсам. Ликлайдера называют духовным отцом всемирной сети, человеком, посеявшим семена Интернета.
Вклад Ликлайдера в возникновение Интернета огромен, он состоит из идей и принципов, а не из изобретений и технологий. Ликлайдер предвидел необходимость объединения в сеть компьютеров, имеющих простые пользовательские интерфейсы. Его идеи предвосхитили компьютерную графику, интерфейсы, работающие по принципу указания и выбора (point-and-click), цифровые библиотеки, электронную коммерцию (e-commerce), дистанционное банковское обслуживание (online banking), а также программное обеспечение, размещаемое в сети. В США его считают «Джонни Эпплсидом программирования».
Вэни́вар[a] Буш (англ. Vannevar Bush /væˈniːvɑr/, 11 марта 1890, Эверетт, Массачусетс — 28 июня 1974, Белмонт, Массачусетс) — американский ученый, инженер, разработчик аналоговых компьютеров, методолог и организатор научных исследований и научного сообщества. Советник по науке при президенте Рузвельте. Автор статьи «Как мы можем мыслить», в которой предложил прообраз гипертекстового устройства Memex (интересное дальновидение - мое прим.).
Урбе́н Жан Жозе́ф Леверье́ (фр. Urbain Jean Joseph Le Verrier; 11 марта 1811, Сен-Ло — 23 сентября 1877, Париж) — французский математик, занимавшийся небесной механикой, бо́льшую часть своей жизни проработавший в Парижской обсерватории. Его наиболее известным достижением является предсказание существования планеты Нептун, сделанное с помощью математического анализа астрономических наблюдений. По предложению Франсуа Араго он выполнил вычисления для объяснения несоответствий между наблюдаемой орбитой Урана и той, которая должна быть согласно законам Кеплера и Ньютона.
1878 — Французской Академии продемонстрирован фонограф, но изобретение было объявлено шарлатанством.
#Biography #Birth_Date #Events #11марта
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
[Совет по Java #014]
Тема: Инъекция через конструктор предпочтительнее инъекции в поле. Она делает зависимости явными, поля immutable и упрощает тестирование. В новых версиях Spring @Autowired можно не ставить, если конструктор один.
Проблема: Инъекция зависимостей через поле (@Autowired над полем) — самый быстрый способ написать код, но он создает скрытые проблемы.
Поля не могут быть final, что нарушает иммутабельность и позволяет создавать объекты в частично инициализированном состоянии.
При тестировании невозможно подставить mock через конструктор — приходится использовать рефлексию (PowerMock, ReflectionTestUtils) или поднимать контекст Spring.
Также это скрывает обязательные зависимости: глядя на класс, непонятно, без каких компонентов он не может работать.
Решение: Используйте конструктор для инъекции зависимостей.
Все поля помечайте как private final — они инициализируются один раз при создании бина и гарантированно не равны null после этого.
Объяснение: Constructor injection гарантирует, что все необходимые зависимости будут предоставлены в момент создания объекта.
Пометка полей как final защищает от случайного изменения и делает класс потокобезопасным (если сами зависимости потокобезопасны).
Проверка Objects.requireNonNull() в конструкторе обеспечивает раннее обнаружение ошибок конфигурации.
При использовании одного конструктора Spring автоматически использует его для инъекции — аннотация @Autowired становится опциональной, что соответствует принципу "convention over configuration".
Для циклических зависимостей используется @Lazy.
Разработчик всегда предпочитает constructor injection, оставляя field injection только для очень специфичных случаев (например, некоторые тестовые фреймворки).
#Java #советы
Тема: Инъекция через конструктор предпочтительнее инъекции в поле. Она делает зависимости явными, поля immutable и упрощает тестирование. В новых версиях Spring @Autowired можно не ставить, если конструктор один.
Проблема: Инъекция зависимостей через поле (@Autowired над полем) — самый быстрый способ написать код, но он создает скрытые проблемы.
Поля не могут быть final, что нарушает иммутабельность и позволяет создавать объекты в частично инициализированном состоянии.
При тестировании невозможно подставить mock через конструктор — приходится использовать рефлексию (PowerMock, ReflectionTestUtils) или поднимать контекст Spring.
Также это скрывает обязательные зависимости: глядя на класс, непонятно, без каких компонентов он не может работать.
Решение: Используйте конструктор для инъекции зависимостей.
Все поля помечайте как private final — они инициализируются один раз при создании бина и гарантированно не равны null после этого.
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import java.util.Objects;
//Антипаттерн: field injection
@Service
public class BadService {
@Autowired
private UserRepository userRepository; // не final, можно забыть проинициализировать
@Autowired
private EmailService emailService; // скрытая зависимость
public void registerUser(User user) {
// может быть NullPointerException, если забыли протестировать
userRepository.save(user);
emailService.sendWelcome(user);
}
// невозможно создать объект в тесте без Spring
}
//Правильно: constructor injection
@Service
public class GoodService {
private final UserRepository userRepository;
private final EmailService emailService;
private final AuditService auditService;
// С одним конструктором @Autowired не нужен (Spring 4.3+)
public GoodService(UserRepository userRepository,
EmailService emailService,
@Lazy AuditService auditService) {
this.userRepository = Objects.requireNonNull(userRepository,
"UserRepository must not be null");
this.emailService = Objects.requireNonNull(emailService);
this.auditService = auditService; // @Lazy — прокси будет создан при первом вызове
}
public void registerUser(User user) {
userRepository.save(user);
emailService.sendWelcome(user);
auditService.log("User registered: " + user.getId());
}
}
// Для циклических зависимостей — @Lazy
@Service
class A {
private final B b;
public A(@Lazy B b) { // B создастся лениво
this.b = b;
}
}
@Service
class B {
private final A a;
public B(A a) {
this.a = a;
}
}
Объяснение: Constructor injection гарантирует, что все необходимые зависимости будут предоставлены в момент создания объекта.
Пометка полей как final защищает от случайного изменения и делает класс потокобезопасным (если сами зависимости потокобезопасны).
Проверка Objects.requireNonNull() в конструкторе обеспечивает раннее обнаружение ошибок конфигурации.
При использовании одного конструктора Spring автоматически использует его для инъекции — аннотация @Autowired становится опциональной, что соответствует принципу "convention over configuration".
Для циклических зависимостей используется @Lazy.
Разработчик всегда предпочитает constructor injection, оставляя field injection только для очень специфичных случаев (например, некоторые тестовые фреймворки).
#Java #советы
👍4
Что выведет код?
#Tasks
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class Task110326 {
public static void main(String[] args) {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Task110326.class)) {
UserService110326 service = context.getBean(UserService110326.class);
System.out.println(service.getUser());
}
}
}
@Component
class UserService110326 {
private UserRepository110326 userRepository;
@Autowired
public UserService110326(UserRepository110326 userRepository) {
this.userRepository = userRepository;
}
public String getUser() {
return userRepository.findUser();
}
}
@Component
class UserRepository110326 {
private DatabaseService110326 databaseService;
@Autowired
public UserRepository110326(DatabaseService110326 databaseService) {
this.databaseService = databaseService;
}
public String findUser() {
return "User from " + databaseService.getDataSource();
}
}
@Component
class DatabaseService110326 {
public String getDataSource() {
return "MainDB";
}
}
#Tasks
👍2
Варианты ответа:
Anonymous Quiz
56%
User from MainDB
11%
User from null
11%
Исключение NoSuchBeanDefinitionException
22%
Исключение NullPointerException
👍1
Что такое SOLID принципы? 🤓
Ответ:
SOLID — это пять основных принципов объектно-ориентированного проектирования.
S — Single Responsibility Principle (Принцип единственной ответственности): у класса должна быть только одна причина для изменения.
O — Open/Closed Principle (Открытости/закрытости): классы открыты для расширения, но закрыты для изменения.
L — Liskov Substitution Principle (Подстановки Лисков): наследники не должны нарушать поведение базового класса.
I — Interface Segregation Principle (Разделения интерфейса): много специализированных интерфейсов лучше, чем один общий.
D — Dependency Inversion Principle (Инверсии зависимостей): зависимости должны строиться на абстракциях, а не на конкретных классах.
#собеседование
Ответ:
S — Single Responsibility Principle (Принцип единственной ответственности): у класса должна быть только одна причина для изменения.
O — Open/Closed Principle (Открытости/закрытости): классы открыты для расширения, но закрыты для изменения.
L — Liskov Substitution Principle (Подстановки Лисков): наследники не должны нарушать поведение базового класса.
I — Interface Segregation Principle (Разделения интерфейса): много специализированных интерфейсов лучше, чем один общий.
D — Dependency Inversion Principle (Инверсии зависимостей): зависимости должны строиться на абстракциях, а не на конкретных классах.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
История технологий сегодня — 12 марта
ℹ️ Кто родился в этот день
Пётр Алекса́ндрович Фре́зе (29 февраля [12 марта] 1844, Санкт-Петербург, Российская империя — 24 апреля 1918, Граново, Вышневолоцкий уезд, Тверская губерния, РСФСР) — российский изобретатель, один из конструкторов первого российского автомобиля.
🌐 Знаковые события
1889 — Элмон Строуджер из США запатентовал автоматическую телефонную станцию.
1974 — станция «Марс-6» села на Марсе, впервые передав на Землю данные об атмосфере и почве этой планеты.
#Biography #Birth_Date #Events #12марта
Пётр Алекса́ндрович Фре́зе (29 февраля [12 марта] 1844, Санкт-Петербург, Российская империя — 24 апреля 1918, Граново, Вышневолоцкий уезд, Тверская губерния, РСФСР) — российский изобретатель, один из конструкторов первого российского автомобиля.
1889 — Элмон Строуджер из США запатентовал автоматическую телефонную станцию.
1974 — станция «Марс-6» села на Марсе, впервые передав на Землю данные об атмосфере и почве этой планеты.
#Biography #Birth_Date #Events #12марта
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🔥1
Раздел 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