Stream API
Генерация бесконечных стримов:
1 | Stream<Double> randomStream = Stream.generate(Math::random); |
Аналог for цикла с использованием итератора:
1 | Stream<Integer> loop = Stream.generate(1, i -> i<100, i -> i+1); |
Фабрики и методы стримов:
1 | Stream.empty(); |
findAny vs findFirst
findAny возвращает первый дошедший до терминальной операции элемент стрима. В отличие от него, findFirst выполняет терминальную операцию для всех элементов, и только после этого завершает выполнение стрима. Это может критически сказаться на скорость выполнения параллельных стримов! При этом, если важна сортировка (и влияние ее на результат), то findFirst предпочтительнее, т.к. findAny может выдать некорректный результат.
reduce
Терминальная операция, которая объединяет все объекты в один (конечно, исключительно те, которые до нее дошли).
1 | IntStream.iterate(0, i -> i<10, i -> i+1).reduce(5, (a, b) -> a+b); // 5+0+1+2+...+9=50 |
Или если требуется преобразование:
1 | Stream.of("1", "2", "3", "4", "5", "6", "7", "8", "9") |
reduce используется для сведения значений стрима в 1 результирующее значение. Самая расширенная сигнатура позволяет задавать первичные значение и производить сведение (комбинацию) нескольких значений в одно финальное (например, если промежуточными являются объекты, коллекции, массивы и т.д.). Первичные значение определяет начальное значение, которое получает поток, обрабатывающий стрим. Это может критически сказаться на результате, если стрим параллельный. Например:
1 | int sum = Stream.iterate(10, i -> i<20, i -> i+1) |
Результатом будет: 5+10=15 > 15+11=26 > 26+12=38 > 38+13=51 > 51+14=65 > 65+15=80 > 80+16=96 > 96+17=113 > 113+18=131 > 131+19=150 > 150+5=155 > 155
Но стоит убрать параллельность, результат поменяется: 5+10=15 > 15+11=26 > 26+12=38 > 38+13=51 > 51+14=65 > 65+15=80 > 80+16=96 > 96+17=113 > 113+18=131 > 131+19=150 > 150
collect
Метод позволяет выполнять сложные преобразования объединения. Например:
1 | Stream.of("0", "1", "2", "3", "4", "5", "6", "7", "8", "9") |
В первом аргументе указывается Supplier, который создает результирующий объект (тот, который будет впоследствии передаваться в аргументы для сведения). Во втором аргументе выполняется некая операция над каждым элементом стрима. Первые два аргумента можно представить как:
1 | AtomicInteger res = new AtomicInteger(); |
Третий аргумент используется только(!) в случае если стрим параллельный. Он объединяет результаты всех параллельных выполнений из второго аргумента и собирает их в один итоговый результат.
Для того чтобы это реализовать, второму аргументу передается каждый раз новый объект (созданный с использованием Supplier из первого аргумента), а в третьем аргументе уже создается новый отдельный итоговый объект:
1 | AtomicInteger result = new AtomicInteger(); |
Параллельное выполнение операции reduce довольно опасна, т.к. может сильно забить пул потоков, а управление им доступно только JVM. В связи с этим, collect (и reduce) рекомендуется использовать только для коротких, быстрых операций не обладающих сложной логикой (и, в особенности, не использующих внешние ресурсы!).
flatMap
Преобразует несколько стримов в 1 (сливает их). В случае параллельного использования, очередность не гарантируется (для последовательной операции, она будет выполняться согласно стандартной итерации, как, например, в коллекции).
Примитивы и стримы
Примитивные числовые стримы содержат в составе терминальные операции:
average()- получение среднего;boxed()- преобразует элементы в обертки типаInteger,Double,Longи т.д;max()/min()- максимальные и минимальные значения;range()- генерация стрима в диапазоне;rangeClosed()- генерация стрима в диапазоне, включительно;sum()- сумма по стриму;summaryStatistics()- сводная статистика по стриму. Выполняется в одну операцию, что полезно если нужно несколько статистических показателей. Так,SummaryStatisticsвключаетgetMin(),getMax(),getAverage(),getSum(),getCount(). Является терминальной операцией для стрима.
Шаблон collect
Могут содержать вложенные коллекторы, тем самым создавая цепочку:
1 | Stream.iterate(0, i -> i<10, i -> i+1).collect(Collectors.groupBy(i -> i%2, TreeMap::new, Collectors.toList())); |
Первый атрибут - признак, по которому будет выполняться группировка. Де-факто станет ключом для Map;
Второй атрибут - Supplier, который представляет реализацию Map;
Третий атрибут - коллектор, который собирает сгруппированные элементы.
В примере выше ключом будет остаток от деления на 2 (т.е. может быть либо 1, либо 0); реализация Map - TreeMap; значениями будут все элементы, которые попадут в соответствующую группу (у которых %2 будет равен 0 или 1), будут объединены в List. Итого:
1 | {0=[0,2,4,6,8], 1=[1,3,5,7,9]} |
Стандартные коллекторы из фабрики Collectors
summingDouble/Int/Long
Суммирование сгруппированных стримов (числовых).
averagingDouble/Int/Long
Выявление среднего для сгрупированных числовых стримов (числовых).
counting
Итогом будет количество в сгруппированной карте зачений с одинаковым ключом.
summarizingDouble/Int/Long
Подводка статистики по сгруппированным элементам карты. Вернет Map<K, ? extends SummaryStatistics>.
partitioningBy
Группировка аналогичная groupingBy с той лишь разницей что ключом будет только boolean, т.е. при реализации первого атрибута он должен вернуть либо true, либо false.
toMap
Метод, фактически, такой же как и groupingBy, но с упрощенным синтаксисом. Возвращает Map.
1 | Stream.of("1", "2", "3", "2", "1", "4") |
Метод может иметь только два атрибута - на формирование ключа и на формирование значения. Третий атрибут используется для того чтобы не возникала ошибка дубликата ключей - он объединяет текущее значение Map по ключу с новым значением. Так в примере выше будет:
1 | {1=11, 2=22, 3=3, 4=4} |
Еще 1 параметр может быть указан для определения Supplier, который готовит реализацию Map (по умолчанию HashMap).
mapping
Еще 1 звено в цепочке коллекторов, который может создавать вложенные Map с нужными ключами и изменением значений.
1 | IntStream.iterate(0, i -> i<10, i -> i+1) |
Результат будет:
1 | {false={0=0, 4=4, 8=8, 12=12, 16=16}, true={2=2, 6=6, 10=10, 14=14, 18=18}} |
Function.identity
Метод используется как замена i->i. Лямбда i->i может вызывать ошибку при понижении типа. Как обход данной потенциальной проблемы можно привести все к одному типу, либо сделать type-cast принудительно. Function.identity в этом смысле безопасен и рекомендован к использованию.
Optional
map- возвращаетOptional, внутри которого результат выполнения метода, переданного в качестве аргумента;flatMap- возвращает результат метода, который в свою очередь, должен возвращатьOptional;get- возвращает содержимое. Может выкинуть исключение NoSuchElementException;orElse- возвращает аргумент в случае еслиOptionalпуст. Выполняется сразу и всегда, так что следует использовать только если операция, создающая альтернативу, легковесная;orElseGet- выполняет метод и возвращает результат только еслиOptionalпустой. Запуск метода отложенный - только если объекта вOptionalнет;or- аналогorElseGet, но возвращает результат метода, который должен быть “обернут” вOptional(для получения фактического объекта потребуется продолжить цепочку, например, с помощью все того же or*);orElseThrow- выкидывает исключение в случае если значения вOptionalнет;ifPresent- выполняет метод, еслиOptionalне(!) пустой;ifPresentOrElse- выполняет метод если Optional не(!) пустой, или вызовет второй метод (если Optional пустой).
Совмещение нескольких Optional
1 | if(optional1.isPresent() && optional2.isPresent() { |
Данную запись можно сократить до:
1 | return optional1.flatMap(o1 -> optional2.map(o2 -> Optional.ofNullable(this::doSomething))); |
Существуют Optional обертки для примитивов: OptionalInt, OptionalLong и т.д. Но у них нет методов map, flatMap и filter, которые очень удобны в использовании - это делает типизированные обертки малоэффективными.
Эффективная итерация коллекций
1 | public Set<String> doGet(List<Double> list) { |
andThen
Метод выполняет действие в цепочке последовательности выполнения:
1 | Consumer<String> consumer = source -> System.out.println("1." + source); |
Выводом будет:
1 | 1.test 2.test 3.test |
compose
Метод вставляет операцию в цепочку выполнения до всех(!) ранее определенных операций:
1 | Function<String, Integer> func = /*0*/; |
Последовательность будет следующей:
1 | Compose 2 |
Важно! Метод compose всегда на входе (и на выходе) получает тот тип, который является входным для функционального интерфейса:
1 | Function<Integer, String> func = ...; |
negate, and, or
Данные методы помогают в организации логических цепочек для Predicate:
1 | Predicate<String> check = String::isEmpty; |
ifPresent
Позволяет выполнить Consumer только если Optional содержит значение:
1 | Optional.ofNullable("test").ifPresent(str -> System.out.println(str)); |
ifPresentOrElse
Аналогичен ifPresent, но в случае если значение в Optional нет, будет запущен второй аргумент, Runnable.
1 | Optional.ofNullable(null).ifPresentOrElse(System.out::println, new Runnable() { |
Примечательно, что поток-исполнитель будет тот же.
orElse
Метод возвращает текущее значение Optional, если оно есть, или результат выполнения Consumer из атрибута.
map
Используется для преобразования значения Optional из первого типа в другой, определяемый результатом метода функционального интерфейса.
filter
Возвращает новый Optional, если условие Predicate для старого Optional выполнено.
Реализации стримов
Optional<Double>, Optional<Integer> и прочие иногда может быть уместно заменять на OptionalDouble, OptionalInteger и т.д. В первом случае внутри Optional используется обертка примитива, а во втором случаем непосредственно примитив. Так же, ко второй реализации добавляется несколько утилитарных методов, которые строго типизированы на примитив и могут быть полезны в ряде случаев.
То же самое касается и stream’ов. Иногда предпочтительнее использование IntegerStream и LongStream вместо Stream<Integer> и Stream<Long>.
Stream Spliterator
Метод spliterator в объекте Stream создает объект Spliterator. Методом trySplit можно разбить поток на 2 равных потока, а именно в случае:
1 | var stream = Stream.of("1", "2", "3", "4", "5", "6"); |
В объекте Spliterator доступен метод forEachRemaining, который для каждого элемента разбитого потока выполняется некое действие. Важно отметить что разбиение производится с начала имеющихся данных, т.е. в примере выше содержимое каждого Spliterator’а будет следующим:
1 | origin.forEachRemaining(System.out::println); |
Важно отметить что в случае если для оригинального Stream’а уже был выполнен терминальный метод, то попытка выполнить spliterator на нем выдаст ошибку выполнения:
1 | var stream = Stream.of("1", "2", "3", "4", "5", "6"); |
Метод tryAdvance выполняет действие для “следующего” возможного элемента из Spliterator’а. “Следующий” поскольку очередность выдачи и составление Spliterator’а зависит от параметров самого Spliterator’a.
Создание множества коллекций из Stream
Может быть полезным создание нескольких коллекций из одного оригинального Stream’а. Для этого можно воспользоваться коллектором teeing:
1 | var stream = Stream.of("1", "2", "3", "4", "5", "6"); |
В результате получится строка со всеми элементами Stream: “1,2,3,4,5,6” и отдельный объект List [“1”,”2”,”3”,”4”,”5”,”6”]. И оба эти объекта будут завернуты в объект Pair. Коллекторы, могут быть выбраны любые, в т.ч. пользовательские.