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
выполнено.