Fairness. Честная многопоточность.
Одна из проблем многопоточного программирования называется starvation. Её суть в том, что поток не может получить доступ к общим ресурсам и продолжать работу. Так получается, если:
- у потока низкий приоритет,
- другие потоки захватывают доступ к критической секции быстрее,
- поток вызвал wait() у объекта, но notify() достаётся другим потокам.
Ситуация неприятная: ресурсы потока заняты, а задача не выполняется. Чтобы избежать проблем выше можно использовать средства синхронизации с флажком fairness = true. Например,
Lock lock = new ReentrantLock(true);
Что при этом происходит? И почему этот параметр по умолчанию false?
При fairness=true преимущество получает самый долго ожидающий поток. При этом вероятность starvation ощутимо снижается. Конкретная очередность не гарантируется, так как параметр не влияет на планировщик потоков в ОС. Пропускная способность при этом ухудшается в разы
Интересный факт:
tryLock() не обращает внимания на параметр fairness и постарается захватить блокировку, даже если в очереди стоят другие потоки.
tryLock(0, TimeUnit.SECONDS) учитывает fairness и при наличии других ожидающих потоков встанет в общую очередь.
Что же делать? Использовать флажок fairness или нет?
Если описанные выше проблемы возможны и критичны, надёжнее использовать tryLock с увеличивающимся временем ожидания и не полагаться на fairness.
#core
Одна из проблем многопоточного программирования называется starvation. Её суть в том, что поток не может получить доступ к общим ресурсам и продолжать работу. Так получается, если:
- у потока низкий приоритет,
- другие потоки захватывают доступ к критической секции быстрее,
- поток вызвал wait() у объекта, но notify() достаётся другим потокам.
Ситуация неприятная: ресурсы потока заняты, а задача не выполняется. Чтобы избежать проблем выше можно использовать средства синхронизации с флажком fairness = true. Например,
Lock lock = new ReentrantLock(true);
Что при этом происходит? И почему этот параметр по умолчанию false?
При fairness=true преимущество получает самый долго ожидающий поток. При этом вероятность starvation ощутимо снижается. Конкретная очередность не гарантируется, так как параметр не влияет на планировщик потоков в ОС. Пропускная способность при этом ухудшается в разы
Интересный факт:
tryLock() не обращает внимания на параметр fairness и постарается захватить блокировку, даже если в очереди стоят другие потоки.
tryLock(0, TimeUnit.SECONDS) учитывает fairness и при наличии других ожидающих потоков встанет в общую очередь.
Что же делать? Использовать флажок fairness или нет?
Если описанные выше проблемы возможны и критичны, надёжнее использовать tryLock с увеличивающимся временем ожидания и не полагаться на fairness.
#core
👍13❤2🔥2
Бесполезная калибровка Thread.sleep
или как JDK создаёт иллюзию выбора.
Все знают метод sleep(long millis) у класса Thread. Он останавливает выполнение потока на заданное количество миллисекунд.
Также в классе есть похожий метод sleep(long millis, int nanos). Здравый смысл подсказывает, что это тот же sleep, только время приостановки задаётся точнее.
Документация к методу подтверждает нашу догадку:
...sleep for the specified number of milliseconds plus the specified number of nanoseconds.
Что тут ещё обсуждать? Расходимся!
Но всё же посмотрим в код. После проверок на валидность, видим следующие строки:
Или выставить паузу в 1 миллисекунду, если микросекунд 0, а наносекунд не 0.
У JVM доступен один нативный метод, который работает только с миллисекундами. Поэтому результат не очень удивляет. Но зачем тогда нужен второй метод?
#core
или как JDK создаёт иллюзию выбора.
Все знают метод sleep(long millis) у класса Thread. Он останавливает выполнение потока на заданное количество миллисекунд.
Также в классе есть похожий метод sleep(long millis, int nanos). Здравый смысл подсказывает, что это тот же sleep, только время приостановки задаётся точнее.
Документация к методу подтверждает нашу догадку:
...sleep for the specified number of milliseconds plus the specified number of nanoseconds.
Что тут ещё обсуждать? Расходимся!
Но всё же посмотрим в код. После проверок на валидность, видим следующие строки:
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
То есть прибавить 1 миллисекунду, если наносекунд больше 500к.Или выставить паузу в 1 миллисекунду, если микросекунд 0, а наносекунд не 0.
У JVM доступен один нативный метод, который работает только с миллисекундами. Поэтому результат не очень удивляет. Но зачем тогда нужен второй метод?
#core
👍8❤1🔥1
Паттерн Barrier, ч.1
Если для выполнения задачи нужно дождаться завершения других параллельных задач, то используется паттерн Barrier или барьерная синхронизация.
В нём есть 2 типа участников:
1️⃣ Исполнители — выполняют параллельные задачи и сигнализируют об окончании.
2️⃣ Наблюдатели — ждут завершения указанного количества задач.
В пакете java.util.concurrent этот паттерн реализуют 3 класса:
✅ CountDownLatch
✅ CyclicBarrier
✅ Phaser
В основе их реализации лежит счётчик. Каждый поток, который завершил задачу, уменьшает его значение. Барьер переходит в состояние "преодолён", когда счётчик достигает 0.
Посмотрим, как в них распределены роли:
✔️ CountDownLatch — чёткое разделение на исполнителей и наблюдателей. Потоки исполнителей уменьшают счётчик, затем могут продолжить работу или завершиться. Наблюдатели ждут преодоления барьера, затем выполняют свою задачу.
✔️ CyclicBarrier — роли исполнителей и наблюдателей совмещены. Поток завершает задачу, уменьшает счётчик и блокируется в ожидании остальных. Можно добавить действие, который выполнит последний поток перед тем, как барьер будет считаться преодолённым.
✔️ Phaser комбинирует функционал CountDownLatch и CyclicBarrier и допускает разные варианты использования. Исполнитель может ждать преодоления барьера, а может просто уменьшить счётчик и пойти дальше.
Это основная концептуальная разница между инструментами, которая определяет когда какой использовать. Больше технических аспектов рассмотрим в части 2.
#core
Если для выполнения задачи нужно дождаться завершения других параллельных задач, то используется паттерн Barrier или барьерная синхронизация.
В нём есть 2 типа участников:
1️⃣ Исполнители — выполняют параллельные задачи и сигнализируют об окончании.
2️⃣ Наблюдатели — ждут завершения указанного количества задач.
В пакете java.util.concurrent этот паттерн реализуют 3 класса:
✅ CountDownLatch
✅ CyclicBarrier
✅ Phaser
В основе их реализации лежит счётчик. Каждый поток, который завершил задачу, уменьшает его значение. Барьер переходит в состояние "преодолён", когда счётчик достигает 0.
Посмотрим, как в них распределены роли:
✔️ CountDownLatch — чёткое разделение на исполнителей и наблюдателей. Потоки исполнителей уменьшают счётчик, затем могут продолжить работу или завершиться. Наблюдатели ждут преодоления барьера, затем выполняют свою задачу.
✔️ CyclicBarrier — роли исполнителей и наблюдателей совмещены. Поток завершает задачу, уменьшает счётчик и блокируется в ожидании остальных. Можно добавить действие, который выполнит последний поток перед тем, как барьер будет считаться преодолённым.
✔️ Phaser комбинирует функционал CountDownLatch и CyclicBarrier и допускает разные варианты использования. Исполнитель может ждать преодоления барьера, а может просто уменьшить счётчик и пойти дальше.
Это основная концептуальная разница между инструментами, которая определяет когда какой использовать. Больше технических аспектов рассмотрим в части 2.
#core
❤3👍3🔥1
Паттерн Barrier, часть 2
В предыдущей части мы рассмотрели разные реализации паттерна Barrier в java.util.concurrent.
Посмотрим на отличия в использовании. Они касаются того
▪️когда ставится начальное значение счётчика,
▪️что происходит после преодоления барьера.
1️⃣ CountDownLatch
✔️ Счётчик задаётся при создании объекта.
✔️ Поток, который встал на await(), блокируется без возможности прерывания.
✔️ Когда значение счётчика достигает 0, наблюдатель продолжает работу, а сам объект CountDownLatch больше использовать нельзя, он одноразовый.
2️⃣ CyclicBarrier
✔️ Начальное значение счётчика тоже задаётся в конструкторе.
✔️ Поток, который дошёл до барьера, блокируется.
✔️ При преодолении барьера счётчик возвращается в исходное значение и готов снова собирать потоки. Количество циклов бесконечно.
3️⃣ Phaser. Каждый цикл называется фазой.
✔️ Начальное значение счётчика может меняться в каждой фазе.
✔️ Количество фаз по умолчанию бесконечно, но можно выставить ограничение с помощью функции onAdvance.
✔️ Ожидание наблюдателей может быть блокирующим или прерываемым.
Phaser включает в себя весь функционал CountDownLatch и CyclicBarrier. Он гибкий и помогает реализовывать сложные сценарии.
При выборе класса для реализации барьера, помните простую мудрость:
✴️ Если задача решается просто, нужно решать её просто.
#core
В предыдущей части мы рассмотрели разные реализации паттерна Barrier в java.util.concurrent.
Посмотрим на отличия в использовании. Они касаются того
▪️когда ставится начальное значение счётчика,
▪️что происходит после преодоления барьера.
1️⃣ CountDownLatch
✔️ Счётчик задаётся при создании объекта.
✔️ Поток, который встал на await(), блокируется без возможности прерывания.
✔️ Когда значение счётчика достигает 0, наблюдатель продолжает работу, а сам объект CountDownLatch больше использовать нельзя, он одноразовый.
2️⃣ CyclicBarrier
✔️ Начальное значение счётчика тоже задаётся в конструкторе.
✔️ Поток, который дошёл до барьера, блокируется.
✔️ При преодолении барьера счётчик возвращается в исходное значение и готов снова собирать потоки. Количество циклов бесконечно.
3️⃣ Phaser. Каждый цикл называется фазой.
✔️ Начальное значение счётчика может меняться в каждой фазе.
✔️ Количество фаз по умолчанию бесконечно, но можно выставить ограничение с помощью функции onAdvance.
✔️ Ожидание наблюдателей может быть блокирующим или прерываемым.
Phaser включает в себя весь функционал CountDownLatch и CyclicBarrier. Он гибкий и помогает реализовывать сложные сценарии.
При выборе класса для реализации барьера, помните простую мудрость:
✴️ Если задача решается просто, нужно решать её просто.
#core
❤2👍1
Java 14 features
Меньше двух недель осталось до выхода новой версии java. 17 марта анонсирован релиз JDK 14. В нём будет 16 фич, давайте посмотрим на некоторые из них.
1️⃣ Текстовые блоки — многострочные строки, ограниченные тремя двойными кавычками.
Удобно для чтения и написания частей HTML, XML, SQL, JSON.
2️⃣ Изменения в switch.
✅ появился способ не писать break. Если использовать вместо двоеточия стрелочку
то выполнится только код этого кейса.
❌
Теперь при возникновении исключения будет доступна не только строка, где это произошло, но и описание ситуации.
Меньше двух недель осталось до выхода новой версии java. 17 марта анонсирован релиз JDK 14. В нём будет 16 фич, давайте посмотрим на некоторые из них.
1️⃣ Текстовые блоки — многострочные строки, ограниченные тремя двойными кавычками.
String html = """
select *
from users
""";
То, ради чего многие перешли на котлин, теперь есть и в java.Удобно для чтения и написания частей HTML, XML, SQL, JSON.
2️⃣ Изменения в switch.
✅ появился способ не писать break. Если использовать вместо двоеточия стрелочку
то выполнится только код этого кейса.
case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
Напомню, что если в обычной форме не написать в конце блока break, то поиск соответствий после выхода из блока продолжится. В примере напечатаются обе строки:case 1: System.out.println("1");
default: System.out.println("0");
✅ switch теперь возвращает значение. Если строк много, то используется слово yield.int j = switch (day) {
case MONDAY -> 0;
case TUESDAY -> 1;
default -> {
int res = f(k);
yield res;
}
};
3️⃣ Сокращение размера кода при сужении типа. Каждому, кто переопределял метод equals, знаком этот паттерн:if(o instanceof String) {
String s = (String) o;
// use s
}
Теперь проверка и приведение типа объединены:if(o instanceof String str) {
// can use str here
}
Если в if несколько условий, то область видимости переменной зависит от типа их связи. В условиях, соединённых через И, переменную можно использовать:if(o instanceof String s && s.length() > 5)
В условиях, соединённых через ИЛИ, переменная не будет видна:❌
if(o instanceof String s || s.length() > 5)
4️⃣ Больше информации в NullPointerException.Теперь при возникновении исключения будет доступна не только строка, где это произошло, но и описание ситуации.
Exception in thread "main" java.lang.NullPointerException:
Cannot read field "j" because "b" is null
at Pr.main(Pr.java:5)
По умолчанию эта фича не работает, для её включения нужно установить флаг:-XX:+ShowCodeDetailsInExceptionMessages
#core👍7❤2🔥2
Что такое инкапсуляция?
Разберём популярный вопрос на собеседовании джуниор разработчика.
В 2013 году я отвечала, что инкапсуляция - это сокрытие деталей реализации. Обращаемся к объекту через методы и получаем ожидаемый результат, не погружаясь в лишние детали. Что там творится внутри метода - неважно.
Это верный ответ, но не полный.
Выделить часть кода в отдельный метод - это ещё не инкапсуляция. Такое можно провернуть в любом языке, это не делает его объекто-ориентированным.
❓Что же такое инкапсуляция?
У каждого объекта есть состояние - внутренние поля.
Иногда с ограничениями:
▪️Возраст - целое число меньше 120
▪️Имя - хотя бы одна буква
Иногда поля связаны между собой:
▪️Статус пользователя зависит от количества заказов
▪️Коэффициент ОСАГО зависит от города
Инкапсуляция - это когда состояние объекта нельзя поменять напрямую, только через методы класса. При этом важно защитить внутреннее состояние от нежелательных изменений. Это ответ на первый вопрос перед постом.
На практике решение обычно простое: сделать поля private и добавить методы get и set. Но иногда этого недостаточно.
❓Что не так с инкапсуляцией в классе Account?
Метод getOrders отдаёт список заказов List<Order>. Подразумевается, что клиент добавит ещё один заказ или обработает список.
По факту возможностей гораздо больше:
❌Удалить элементы
❌Отредактировать текущие заказы
В сложных системах не поможет комментарий "этот список только для добавления". Надёжный способ избежать ошибок - это понятный и ограниченный API.
Правильный ответ на вопрос 2:
Инкапсуляция так себе: внутреннее состояние (список заказов) не защищено.
Возможность создать два аккаунта с одним ID - вопрос дизайна и сценариев работы. С точки зрения инкапсуляции всё ок - ID нельзя поменять.
❓Как исправить ситуацию и защитить список заказов?
1️⃣ Хранить заказы отдельно
2️⃣ Поменять класс:
▫️Добавить orders модификатор final
▫️Добавить метод addOrder
▫️Метод getOrders пусть возвращает неизменяемый список
(возможны другие варианты, зависит от контекста)
#core
Разберём популярный вопрос на собеседовании джуниор разработчика.
В 2013 году я отвечала, что инкапсуляция - это сокрытие деталей реализации. Обращаемся к объекту через методы и получаем ожидаемый результат, не погружаясь в лишние детали. Что там творится внутри метода - неважно.
Это верный ответ, но не полный.
Выделить часть кода в отдельный метод - это ещё не инкапсуляция. Такое можно провернуть в любом языке, это не делает его объекто-ориентированным.
❓Что же такое инкапсуляция?
У каждого объекта есть состояние - внутренние поля.
Иногда с ограничениями:
▪️Возраст - целое число меньше 120
▪️Имя - хотя бы одна буква
Иногда поля связаны между собой:
▪️Статус пользователя зависит от количества заказов
▪️Коэффициент ОСАГО зависит от города
Инкапсуляция - это когда состояние объекта нельзя поменять напрямую, только через методы класса. При этом важно защитить внутреннее состояние от нежелательных изменений. Это ответ на первый вопрос перед постом.
На практике решение обычно простое: сделать поля private и добавить методы get и set. Но иногда этого недостаточно.
❓Что не так с инкапсуляцией в классе Account?
Метод getOrders отдаёт список заказов List<Order>. Подразумевается, что клиент добавит ещё один заказ или обработает список.
По факту возможностей гораздо больше:
❌Удалить элементы
❌Отредактировать текущие заказы
В сложных системах не поможет комментарий "этот список только для добавления". Надёжный способ избежать ошибок - это понятный и ограниченный API.
Правильный ответ на вопрос 2:
Инкапсуляция так себе: внутреннее состояние (список заказов) не защищено.
Возможность создать два аккаунта с одним ID - вопрос дизайна и сценариев работы. С точки зрения инкапсуляции всё ок - ID нельзя поменять.
❓Как исправить ситуацию и защитить список заказов?
1️⃣ Хранить заказы отдельно
2️⃣ Поменять класс:
▫️Добавить orders модификатор final
▫️Добавить метод addOrder
▫️Метод getOrders пусть возвращает неизменяемый список
(возможны другие варианты, зависит от контекста)
#core
❤1
Default методы: неудачный кейс
В чём ценность опытного разработчика? Способность видеть возможные проблемы. Интуиция, чуйка, "что-то мне здесь не нравится, давайте разберёмся".
Чтобы развить этот навык, нужно совершить много ошибок самому и изучать чужие ошибки. Прямо или косвенно, это всё идёт на пользу, и сегодня хочу поделиться одним неудачным случаем.
Default методы появились в java 8, чтобы упростить добавление методов в интерфейс. Подробный обзор можно прочитать в этом посте.
✅ Легко добавить новый метод
✅ Нет ошибок компиляции
✅ Методы при желании переопределяются
C java 8 в интерфейсе Collection появились методы по умолчанию spliterator(), stream(), parallelStream(), removeIf(…).
Какие могут быть проблемы?
❌ Реализация по умолчанию не подходит
❌ Разработчики не узнают, что добавился новый метод, который нужно переопределить. Ошибок компиляции нет, предупреждений тоже
❌ Нет тестов нового метода и интеграционных тестов
От этих проблем пострадали пользователи SynchronizedCollection из библиотеки Apache Commons.
Что произошло?
В SynchronizedCollection каждый метод синхронизирован по объекту lock:
Что делает дефолтный метод removeIf? Берёт итератор, проверяет каждый элемент на соответствие условию и удаляет, если нужно.
Переложим на методы SynchronizedCollection. Синхронизация по lock берётся, отпускается, берётся, отпускается, и так несколько раз. При большой нагрузке управление перехватит другой поток, и произойдёт коллизия. Дефолтный метод не выполнит гарантий, заданных классом.
Ошибку легко исправить - переопределить метод removeIf:
5 лет! Может новым методом никто не пользовался. Может поток данных через коллекцию был небольшим. Может никаких последствий не было. А может были, неизвестно.
Даже такая безобидная фича как "методы по умолчанию" привела к ошибке. Мы можем вынести из неё пару best practices:
▫️Если интерфейс используется только внутри системы, достаточно написать тесты для всех реализаций.
▫️Для общедоступных библиотек по возможности избегать методов по умолчанию.
#core
В чём ценность опытного разработчика? Способность видеть возможные проблемы. Интуиция, чуйка, "что-то мне здесь не нравится, давайте разберёмся".
Чтобы развить этот навык, нужно совершить много ошибок самому и изучать чужие ошибки. Прямо или косвенно, это всё идёт на пользу, и сегодня хочу поделиться одним неудачным случаем.
Default методы появились в java 8, чтобы упростить добавление методов в интерфейс. Подробный обзор можно прочитать в этом посте.
✅ Легко добавить новый метод
✅ Нет ошибок компиляции
✅ Методы при желании переопределяются
C java 8 в интерфейсе Collection появились методы по умолчанию spliterator(), stream(), parallelStream(), removeIf(…).
Какие могут быть проблемы?
❌ Реализация по умолчанию не подходит
❌ Разработчики не узнают, что добавился новый метод, который нужно переопределить. Ошибок компиляции нет, предупреждений тоже
❌ Нет тестов нового метода и интеграционных тестов
От этих проблем пострадали пользователи SynchronizedCollection из библиотеки Apache Commons.
Что произошло?
В SynchronizedCollection каждый метод синхронизирован по объекту lock:
synchronized (lock) {
return coll.remove(object);
}
Изменения последовательны, данные в безопасности и всегда актуальны. Что делает дефолтный метод removeIf? Берёт итератор, проверяет каждый элемент на соответствие условию и удаляет, если нужно.
Переложим на методы SynchronizedCollection. Синхронизация по lock берётся, отпускается, берётся, отпускается, и так несколько раз. При большой нагрузке управление перехватит другой поток, и произойдёт коллизия. Дефолтный метод не выполнит гарантий, заданных классом.
Ошибку легко исправить - переопределить метод removeIf:
synchronized (lock) {
return coll.removeIf(filter);
}
Проблема в том, что такие ошибки сложно обнаружить. Default метод появился в марте 2014, а класс обновили в июле 2019. 5 лет пользователи SynchronizedCollection пользовались ненадёжным методом.5 лет! Может новым методом никто не пользовался. Может поток данных через коллекцию был небольшим. Может никаких последствий не было. А может были, неизвестно.
Даже такая безобидная фича как "методы по умолчанию" привела к ошибке. Мы можем вынести из неё пару best practices:
▫️Если интерфейс используется только внутри системы, достаточно написать тесты для всех реализаций.
▫️Для общедоступных библиотек по возможности избегать методов по умолчанию.
#core
❤2
Коллекторы Stream API, часть 1: простые методы
В первой части повторим основы - из чего состоит стрим и что такое коллектор. Каждый код со Stream API состоит из 3х частей:
1️⃣ Получение стрима
2️⃣ Преобразования
3️⃣ Терминальная операция
1️⃣
Коллекторы - статические методы класса Collectors, которые возвращают аргумент для метода collect. Я буду опускать основной класс и вместо Collectors.counting() будут писать counting(). Чтобы было короче.
Чаще всего элементы стрима собирают в обычную коллекцию:
▪️toCollection, toList, toSet
▪️toUnmodifiableSet, toUnmodifiableList
▫️counting
▫️averagingToInt / Long / Double
▫️joining
▫️maxBy, minBy
▫️reducing
▫️summingInt / Long / Double
▫️summarizingInt / Long / Double
Интересны здесь только два метода:
🔸 joining
Соединяет элементы в одну строку:
Возвращает объект IntSummaryStatistics, который содержит минимум, максимум, среднее, количество элементов и их сумму.
Остальные методы сами по себе бесполезны, так как есть простые аналоги:
Они нужны в коллекторах groupingBy и partitioningBy, про них подробно поговорим завтра.
#core
В первой части повторим основы - из чего состоит стрим и что такое коллектор. Каждый код со Stream API состоит из 3х частей:
1️⃣ Получение стрима
2️⃣ Преобразования
3️⃣ Терминальная операция
1️⃣
list.stream()
2️⃣ .filter(e -> e != 3)
3️⃣ .count();
Терминальная операция collect собирает элементы стрима в другую структуру данных. Все подробности передаются через аргумент:collect(Collector collector) Коллекторы - статические методы класса Collectors, которые возвращают аргумент для метода collect. Я буду опускать основной класс и вместо Collectors.counting() будут писать counting(). Чтобы было короче.
Чаще всего элементы стрима собирают в обычную коллекцию:
▪️toCollection, toList, toSet
▪️toUnmodifiableSet, toUnmodifiableList
Set res=students.stream()Ещё одна группа - коллекторы, которые возвращают одно значение:
.filter(…).collect(toSet())
▫️counting
▫️averagingToInt / Long / Double
▫️joining
▫️maxBy, minBy
▫️reducing
▫️summingInt / Long / Double
▫️summarizingInt / Long / Double
Интересны здесь только два метода:
🔸 joining
Соединяет элементы в одну строку:
chars.stream().collect(joining("-"));
// ['a','b','c'] → a-b-c
🔸summarizingIntВозвращает объект IntSummaryStatistics, который содержит минимум, максимум, среднее, количество элементов и их сумму.
Остальные методы сами по себе бесполезны, так как есть простые аналоги:
list.stream().collect(counting())Коллекторы mapping, flatMapping и filtering применяют функцию к элементам перед отправкой в другой коллектор.
// аналог
list.stream().count()
Set<Long> ids = …Использовать их напрямую тоже смысла нет. Проще применить к элементам map, flatmap или filter, а потом собрать результаты:
collect(mapping(Student::id, toSet())
map(Student::id).collect(toSet())❓Зачем нужны эти методы?
Они нужны в коллекторах groupingBy и partitioningBy, про них подробно поговорим завтра.
#core
👍5❤1
Коллекторы Stream API, часть 2: сложные коллекторы
В этой части разберёмся в коллекторах toMap, groupingBy и partitioningBy. Уверена, года через 3 их будут спрашивать на занудных собеседованиях.
Будем тренироваться на классе Student из вопроса перед постом. У него есть id, имя, город и список оценок.
🔸toMap, toConcurrentMap
Цель понятна: уложить элементы стрима в map. Указываем функцию для ключа и для значения:
Если ключи повторяются, то вылетит IllegalStateException. Чтобы этого избежать, укажите третьим параметром, как объединять значения:
Похоже на группировку по городу, но это не она. В результате получается одно значение, а не группа.
🔸groupingBy
Настоящую группировку делает коллектор groupingBy:
▫️Города и количество студентов:
▫️ID студента и средняя оценка за экзамены:
...
Для этой задачи группировка не подходит. У одного студента только одна средняя оценка, количество итоговых элементов = количеству исходных. Группировать нечего, поэтому используем toMap:
🔸partitioningBy
Метод делит элементы на две группы по заданному условию. Результат - map с двумя ключами - true и false.
▫️Делим студентов на москвичей и жителей других городов:
Вы поручили джуниору посчитать среди студентов тех, кто не сдал ни одного экзамена. Вариант 3 предлагает применить к набору студентов метод Integer::longValue, что невозможно. Остальные варианты переводят каждого студента в число 1 и суммируют все значения. Ошибочный ответ - 3.
#core
В этой части разберёмся в коллекторах toMap, groupingBy и partitioningBy. Уверена, года через 3 их будут спрашивать на занудных собеседованиях.
Будем тренироваться на классе Student из вопроса перед постом. У него есть id, имя, город и список оценок.
🔸toMap, toConcurrentMap
Цель понятна: уложить элементы стрима в map. Указываем функцию для ключа и для значения:
collect(toMap(Student::id, Student::name)) Если ключи повторяются, то вылетит IllegalStateException. Чтобы этого избежать, укажите третьим параметром, как объединять значения:
collect(toMap(Student::city,["Москва":"Антон Аня Эдуард"]
Student::name,
(a,b) -> a+" "+b))
Похоже на группировку по городу, но это не она. В результате получается одно значение, а не группа.
🔸groupingBy
Настоящую группировку делает коллектор groupingBy:
Map<String, List<Student>>=…По умолчанию сгруппированные элементы объединяются в список. Чтобы получить что-нибудь другое, передайте в метод другой коллектор:
collect(groupingBy (Student::city))
groupingBy(Student::city, toSet())Здесь нам наконец пригодятся коллекторы из предыдущего поста, которые подставим во второй аргумент.
▫️Города и количество студентов:
groupingBy(Student::city, counting())▫️Города и имена студентов:
groupingBy(Student::city,Передаём в groupingBy функцию для ключей и коллектор mapping для значений. Он же принимает другой коллектор toSet, чтобы было понятно, куда складывать результат.
mapping(Student::name, toSet()))
▫️ID студента и средняя оценка за экзамены:
...
Для этой задачи группировка не подходит. У одного студента только одна средняя оценка, количество итоговых элементов = количеству исходных. Группировать нечего, поэтому используем toMap:
toMap(Student::id,Вместо расчёт() - километр кода. Подсчёт среднего недоступен в интерфейсе Stream, а удобных методов перевода List<Integer> в IntStream нет.
s -> s.marks().stream().расчёт())
🔸partitioningBy
Метод делит элементы на две группы по заданному условию. Результат - map с двумя ключами - true и false.
▫️Делим студентов на москвичей и жителей других городов:
Map<Boolean,Set<Student>>…Вместо toSet можно подставить другой коллектор. Посчитаем количество студентов:
partitioningBy(s-> s.city().equals("Moscow"), toSet())
partitioningBy(s-> s.city().equals("Moscow"), counting())
Не очень понятно, зачем нужен partitioningBy. groupingBy даёт такой же результат, но на 2 символа короче:groupingBy(s-> s.city().equals("Moscow"), counting())
❓Ответ на вопрос перед постомВы поручили джуниору посчитать среди студентов тех, кто не сдал ни одного экзамена. Вариант 3 предлагает применить к набору студентов метод Integer::longValue, что невозможно. Остальные варианты переводят каждого студента в число 1 и суммируют все значения. Ошибочный ответ - 3.
#core
👍3
Коллекторы Stream API, часть 3: дизайн
Вчера мы разбирали groupingBy и partitioningBy. У многих возникла мысль, что лучше держаться от группировок подальше. Коллекторы выглядят неважно относительно других методов Stream API:
▪️Многословные: collect(Collectors.toSet())
▪️Вложенные коллекторы
▪️Дублированные методы map, flatMap, max, min - 18 штук
Итог: плохая читаемость и желание написать всё через цикл for.
Почему так получилось? Разберёмся в этом посте.
Выделим три вопроса:
1️⃣ Почему вместо toSet() такой сложный collect(Collectors.toSet())?
2️⃣ Почему группировка - терминальная операция?
3️⃣ Зачем в классе 18 неполноценных методов и вложенные коллекторы?
Разберём по порядку.
1️⃣ Метод collect и класс Collectors.
Цель Stream API - удобная работа с данными. За интерфейсом Stream может быть любой источник данных: список, строка, коллекция или файл. Сторонние библиотеки могут реализовать свой источник данных и работать с ним стандартными средствами Stream API.
В обратную сторону это тоже работает. Пользователь может преобразовать стрим в свою структуру данных, для этого в интерфейсе Stream метод collect(Collector).
А ещё источник данных и конечная структура данных - разные сущности, поэтому в интерфейсе Stream нет методов toSet(), toList(). Single Responsibility.
2️⃣ Почему группировка - терминальная операция?
С точки зрения пользователя группировка - просто преобразование. После группировки элементы можно фильтровать, менять и так далее.
Дизайн Stream API для этого не подходит. Источник данных делится на части, элементы обрабатываются независимо, иногда в разных потоках.
Терминальная операция - единственное место, где потоки объединяются и вычисляется общий результат. Посчитать количество, найти элемент, объединить в коллекцию - это конечные точки в обработке.
Группировка работает со всеми элементами, и в терминах Stream API это терминальная операция, дальше работать со стримом нельзя.
3️⃣ Вложенные коллекторы и 18 методов для groupingBy
Кстати, разработчики JDK понимают, что группировка - это преобразование данных, а вовсе не финальная точка.
Что может понадобиться программисту?
🔸Указать итоговый тип данных.
🔸Преобразовать итоговые данные. Это сложно сделать напрямую, потому что универсального интерфейса для filter, map и average нет.
Для решения этих задач groupingBy получил аргумент-коллектор, а Collectors пополнился 18 методами-обёртками над map, flatMap, filter и average .
На мой взгляд получилось сложновато, но это спорный вопрос. А какие вам нравятся библиотеки?
💅 - Удобные методы для целевых кейсов, но с ограничениями
💪 - Максимум возможностей и кастомизации
#core
Вчера мы разбирали groupingBy и partitioningBy. У многих возникла мысль, что лучше держаться от группировок подальше. Коллекторы выглядят неважно относительно других методов Stream API:
▪️Многословные: collect(Collectors.toSet())
▪️Вложенные коллекторы
▪️Дублированные методы map, flatMap, max, min - 18 штук
Итог: плохая читаемость и желание написать всё через цикл for.
Почему так получилось? Разберёмся в этом посте.
Выделим три вопроса:
1️⃣ Почему вместо toSet() такой сложный collect(Collectors.toSet())?
2️⃣ Почему группировка - терминальная операция?
3️⃣ Зачем в классе 18 неполноценных методов и вложенные коллекторы?
Разберём по порядку.
1️⃣ Метод collect и класс Collectors.
Цель Stream API - удобная работа с данными. За интерфейсом Stream может быть любой источник данных: список, строка, коллекция или файл. Сторонние библиотеки могут реализовать свой источник данных и работать с ним стандартными средствами Stream API.
В обратную сторону это тоже работает. Пользователь может преобразовать стрим в свою структуру данных, для этого в интерфейсе Stream метод collect(Collector).
А ещё источник данных и конечная структура данных - разные сущности, поэтому в интерфейсе Stream нет методов toSet(), toList(). Single Responsibility.
2️⃣ Почему группировка - терминальная операция?
С точки зрения пользователя группировка - просто преобразование. После группировки элементы можно фильтровать, менять и так далее.
Дизайн Stream API для этого не подходит. Источник данных делится на части, элементы обрабатываются независимо, иногда в разных потоках.
Терминальная операция - единственное место, где потоки объединяются и вычисляется общий результат. Посчитать количество, найти элемент, объединить в коллекцию - это конечные точки в обработке.
Группировка работает со всеми элементами, и в терминах Stream API это терминальная операция, дальше работать со стримом нельзя.
3️⃣ Вложенные коллекторы и 18 методов для groupingBy
Кстати, разработчики JDK понимают, что группировка - это преобразование данных, а вовсе не финальная точка.
Что может понадобиться программисту?
🔸Указать итоговый тип данных.
🔸Преобразовать итоговые данные. Это сложно сделать напрямую, потому что универсального интерфейса для filter, map и average нет.
Для решения этих задач groupingBy получил аргумент-коллектор, а Collectors пополнился 18 методами-обёртками над map, flatMap, filter и average .
На мой взгляд получилось сложновато, но это спорный вопрос. А какие вам нравятся библиотеки?
💅 - Удобные методы для целевых кейсов, но с ограничениями
💪 - Максимум возможностей и кастомизации
#core
👍2❤1