Java Basics: Stream

Stream API

Генерация бесконечных стримов:

1
2
Stream<Double> randomStream = Stream.generate(Math::random);
Stream<Integer> intStream = Stream.iterate(1, i -> i+1);

Аналог for цикла с использованием итератора:

1
Stream<Integer> loop = Stream.generate(1, i -> i<100, i -> i+1);

Фабрики и методы стримов:

1
2
3
4
5
Stream.empty();
Stream.of("a", "b", "c");
Stream.generate(Supplier<T>);
Stream.iterate(int, UnaryOperation<Integer>);
Stream.iterate(int, Predicate, UnaryOperation<Integer>);

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
2
Stream.of("1", "2", "3", "4", "5", "6", "7", "8", "9")
.reduce(5, (current, next) -> current + Integer.parseInt(next), Integer::sum);

reduce используется для сведения значений стрима в 1 результирующее значение. Самая расширенная сигнатура позволяет задавать первичные значение и производить сведение (комбинацию) нескольких значений в одно финальное (например, если промежуточными являются объекты, коллекции, массивы и т.д.). Первичные значение определяет начальное значение, которое получает поток, обрабатывающий стрим. Это может критически сказаться на результате, если стрим параллельный. Например:

1
2
3
4
5
6
int sum = Stream.iterate(10, i -> i<20, i -> i+1)
.parallel()
.reduce(5, (total, next) -> {
out.print(total + "+" + next + "=" + (total + next) + " > ");
return total + next;
});

Результатом будет: 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
2
Stream.of("0", "1", "2", "3", "4", "5", "6", "7", "8", "9")
.collect(AtomicInteger::new, (current, next) -> current.addAndGet(Integer.parseInt(next)), (current, result) -> result.addAndGet(current.get()));

В первом аргументе указывается Supplier, который создает результирующий объект (тот, который будет впоследствии передаваться в аргументы для сведения). Во втором аргументе выполняется некая операция над каждым элементом стрима. Первые два аргумента можно представить как:

1
2
3
4
AtomicInteger res = new AtomicInteger();
for(String value : streamElements) {
res.addAndGet(Integer.parseInt(value));
}

Третий аргумент используется только(!) в случае если стрим параллельный. Он объединяет результаты всех параллельных выполнений из второго аргумента и собирает их в один итоговый результат.
Для того чтобы это реализовать, второму аргументу передается каждый раз новый объект (созданный с использованием Supplier из первого аргумента), а в третьем аргументе уже создается новый отдельный итоговый объект:

1
2
3
4
5
6
AtomicInteger result = new AtomicInteger();
int maxThreads = ...;
for(int i=0; i<maxThreads; i++) {
Callable<AtomicInteger> resultCompiler = ...;
result.addAndGet(resultCompiler.call().get());
}

Параллельное выполнение операции reduce довольно опасна, т.к. может сильно забить пул потоков, а управление им доступно только JVM. В связи с этим, collectreduce) рекомендуется использовать только для коротких, быстрых операций не обладающих сложной логикой (и, в особенности, не использующих внешние ресурсы!).

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
2
Stream.of("1", "2", "3", "2", "1", "4")
.collect(key -> key, val -> val, (val1, val2) -> val1.concat(val2));

Метод может иметь только два атрибута - на формирование ключа и на формирование значения. Третий атрибут используется для того чтобы не возникала ошибка дубликата ключей - он объединяет текущее значение Map по ключу с новым значением. Так в примере выше будет:

1
{1=11, 2=22, 3=3, 4=4}

Еще 1 параметр может быть указан для определения Supplier, который готовит реализацию Map (по умолчанию HashMap).

mapping

Еще 1 звено в цепочке коллекторов, который может создавать вложенные Map с нужными ключами и изменением значений.

1
2
3
IntStream.iterate(0, i -> i<10, i  -> i+1)
.boxed()
.collect(Collectors.partitioningBy(i -> i%2 > 0, Collectors.mapping(i -> i*2, Collectors.toMap(i -> i, i -> i))));

Результат будет:

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
2
3
if(optional1.isPresent() && optional2.isPresent() {
return Optional.ofNullable(this::doSomething);
}

Данную запись можно сократить до:

1
return optional1.flatMap(o1 -> optional2.map(o2 -> Optional.ofNullable(this::doSomething)));

Существуют Optional обертки для примитивов: OptionalInt, OptionalLong и т.д. Но у них нет методов map, flatMap и filter, которые очень удобны в использовании - это делает типизированные обертки малоэффективными.

Эффективная итерация коллекций

1
2
3
4
5
6
7
8
public Set<String> doGet(List<Double> list) {
return list.stream()
.map(d -> d+10)
.filter(d -> d > 15)
.map(Optional::ofNullable)
.flatMap(Optional::stream)
.collect(Collectors::toSet);
}

andThen

Метод выполняет действие в цепочке последовательности выполнения:

1
2
3
4
5
Consumer<String> consumer = source -> System.out.println("1." + source);
consumer = consumer
.andThen(source -> System.out.println("2." + source))
.andThen(source -> System.out.println("3." + source));
consumer.apply("test");

Выводом будет:

1
1.test 2.test 3.test

compose

Метод вставляет операцию в цепочку выполнения до всех(!) ранее определенных операций:

1
2
Function<String, Integer> func = /*0*/;
func = func.applyThen(/*1*/).applyThen(/*2*/).compose(/*1*/).applyThen(/*3*/).compose(/*2*/).applyThen(/*4*/);

Последовательность будет следующей:

1
2
3
4
5
6
7
Compose 2
Compose 1
Function 0
applyThen 1
applyThen 2
applyThen 3
applyThen 4

Важно! Метод compose всегда на входе (и на выходе) получает тот тип, который является входным для функционального интерфейса:

1
2
3
4
5
Function<Integer, String> func = ...;
func = func
.compose((String input) - > 100) // compile error
.compose((Integer input) -> "100") // compile error
.compose((Integer input) -> 100);

negate, and, or

Данные методы помогают в организации логических цепочек для Predicate:

1
2
Predicate<String> check = String::isEmpty;
сheck = check.negate().and(str -> str.startsWith("Hello")).or(str -> str.endsWith("Bye"));

ifPresent

Позволяет выполнить Consumer только если Optional содержит значение:

1
Optional.ofNullable("test").ifPresent(str -> System.out.println(str));

ifPresentOrElse

Аналогичен ifPresent, но в случае если значение в Optional нет, будет запущен второй аргумент, Runnable.

1
2
3
4
5
6
Optional.ofNullable(null).ifPresentOrElse(System.out::println, new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread());
}
});

Примечательно, что поток-исполнитель будет тот же.

orElse

Метод возвращает текущее значение Optional, если оно есть, или результат выполнения Consumer из атрибута.

map

Используется для преобразования значения Optional из первого типа в другой, определяемый результатом метода функционального интерфейса.

filter

Возвращает новый Optional, если условие Predicate для старого Optional выполнено.

 Comments
Comment plugin failed to load
Loading comment plugin