Перевод текста на русский язык:
Этот учебник представляет собой углублённый обзор потоков данных (Stream) в Java8. Когда я впервые увидел API Stream, я был очень озадачен, потому что он звучал похоже на InputStream и OutputStream в Java IO. Однако потоки данных в Java8 — это совсем другое дело. Поток данных является монадой и играет важную роль в функциональном программировании на Java8.
В функциональном программировании монада — это структура, представляющая вычисление, определённое как последовательность шагов. Структура монады определяет смысл вложенности функций с одинаковым типом или цепочечных операций.
Этот учебник научит вас использовать потоки данных Java8 и различные доступные операции с потоками данных. Вы узнаете об обработке порядка и о том, как порядок операций влияет на производительность во время выполнения. В этом учебнике также подробно рассматриваются более мощные операции с потоками, такие как reduce, collect и flatMap. Наконец, этот учебник глубоко исследует параллельные потоки.
Если вы ещё не знакомы с лямбда-выражениями, функциональными интерфейсами и методами ссылок в Java8, вам может потребоваться сначала прочитать мой учебник по Java8 (ch1.md), прежде чем начать эту главу.
Обновление: сейчас я пишу реализацию API потока данных Java8 для браузера на JavaScript. Если вам интересно, посетите Stream.js на Github. Я с нетерпением жду ваших отзывов.
Поток данных представляет собой последовательность элементов и поддерживает различные операции для выполнения вычислений над элементами:
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");
myList
.stream()
.filter(s -> s.startsWith("c"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
// C1
// C2
Операции с потоком данных либо являются цепочечными операциями, либо завершают операцию. Цепочечные операции возвращают поток данных, поэтому мы можем связывать несколько цепочечных операций без использования точки с запятой. Завершающие операции не имеют возвращаемого значения или возвращают результат, который не является потоком. В приведённом выше примере filter, map и sorted являются цепочечными операциями, а forEach — завершающей операцией. См. Javadoc для всех потоковых операций с данными (http://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html). Вид цепочки операций с потоками данных также называется операционным конвейером.
Большинство операций с потоками данных принимают некоторые параметры лямбда-выражений, которые используются для указания конкретного поведения операции. Большинство этих операций должны быть свободными от помех и без состояния. Что это значит?
Когда функция не изменяет базовый источник данных потока, она называется свободной от помех (http://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#NonInterference). Например, в приведённом выше примере ни одно из лямбда-выражений не добавляет и не удаляет элементы из списка myList.
Когда выполнение функции является детерминированным, оно называется без состояния (http://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#Statelessness). Например, в приведённом выше примере ни одно лямбда-выражение не зависит от какой-либо переменной или состояния, которое может быть изменено в области действия операции.
Потоковые данные могут быть созданы из различных источников данных, особенно коллекций. List и Set поддерживают новые методы stream() и parallelStream(), которые создают последовательные и параллельные потоки соответственно. Параллельные потоки могут выполнять операции на нескольких потоках, о которых будет рассказано в последующих главах. Сейчас мы рассмотрим последовательный поток:
Arrays.asList("a1", "a2", "a3")
.stream()
.findFirst()
.ifPresent(System.out::println); // a1
Вызов метода stream() для объекта List возвращает обычный объектный поток. Но нам не нужно создавать коллекцию для создания потока данных, например:
Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println); // a1
Используя Stream.of(), мы можем создать поток данных из серии объектов.
Помимо обычных объектных потоков данных, Java8 также предоставляет специальные типы потоков для обработки основных типов данных int, long и double. Возможно, вы уже догадались, что это IntStream, LongStream и DoubleStream.
IntStream можно использовать вместо обычного цикла for с помощью IntStream.range():
IntStream.range(1, 4)
.forEach(System.out::println);
// 1
// 2
// 3
Все эти основные потоки данных похожи на обычные потоки объектов, но есть некоторые отличия. Основные потоки данных используют специальные лямбда-выражения, такие как IntFunction вместо Function и IntPredicate вместо Predicate. Кроме того, основные потоки данных поддерживают дополнительные агрегатные завершающие операции sum() и average():
Arrays.stream(new int[] {1, 2, 3})
.map(n -> 2 * n + 1)
.average()
.ifPresent(System.out::println); // 5.0
Иногда необходимо преобразовать обычный поток объектов в основной поток данных или наоборот. Для этой цели объектные потоки данных поддерживают специальные операции сопоставления mapToInt(), mapToLong() и mapToDouble():
Stream.of("a1", "a2", "a3")
.map(s -> s.substring(1))
.mapToInt(Integer::parseInt)
.max()
.ifPresent(System.out::println); // 3
Основной поток данных можно преобразовать в поток объектов с помощью mapToObj():
IntStream.range(1, 4)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
// a1
// a2
// a3
Вот пример комбинирования: поток с плавающей точкой сначала отображается в целочисленный поток, а затем отображается в строковый поток объектов:
Stream.of(1.0, 2.0, 3.0)
.mapToInt(Double::intValue)
.mapToObj(i -> "a" + i)
.forEach(System.out::println);
// a1
// a2
// a3
Теперь, когда мы знаем, как создавать и использовать различные типы потоков данных, давайте углубимся в понимание того, как выполняются операции с потоками за кулисами.
Одной из важных особенностей цепочечных операций является их отложенное выполнение. Обратите внимание на следующий пример без завершающих операций:
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
});
При выполнении этого кода ничего не выводится на консоль. Это связано с тем, что цепочечные операции выполняются только при вызове завершающих операций.
Давайте расширим этот пример, добавив завершающую операцию forEach:
Stream.of("d2", "a2", "b1", "b3", "c")
.filter(s -> {
System.out.println("filter: " + s);
return true;
})
.forEach(s -> System.out::println("forEach: " + s));
Выполнение этого кода даст следующий вывод:
filter: d2
forEach: d2
filter: a2
forEach: a2
filter: b1
forEach: b1
filter: b3
forEach: b3
filter: c
forEach: c
``` <- + f.name))));
Теперь у нас есть список, содержащий три элемента foo
, каждый из которых содержит три элемента bar
.
flatMap
принимает функцию, возвращающую поток объектов. Поэтому для обработки каждого объекта bar
на элементе foo
нам нужно передать соответствующую функцию:
foos.stream()
.flatMap(f -> f.bars.stream())
.forEach(b -> System.out.println(b.name));
// Bar1 <- Foo1
// Bar2 <- Foo1
// Bar3 <- Foo1
// Bar1 <- Foo2
// Bar2 <- Foo2
// Bar3 <- Foo2
// Bar1 <- Foo3
// Bar2 <- Foo3
// Bar3 <- Foo3```
Как вы видите, мы успешно преобразовали поток, содержащий три объекта `foo`, в поток, содержащий девять объектов `bar`.
Наконец, пример кода выше можно упростить до одной потоковой операции:
```java
IntStream.range(1, 4)
.mapToObj(i -> new Foo("Foo" + i))
.peek(f -> IntStream.range(1, 4)
.mapToObj(i -> new Bar("Bar" + i + " <- " + f.name))
.forEach(f.bars::add))
.flatMap(f -> f.bars.stream())
.forEach(b -> System.out.println(b.name));```
Также `flatMap` может использоваться с классом `Optional`, представленным в Java 8. Операция `flatMap` для класса `Optional` возвращает объект типа `Optional` или другой тип. Таким образом, она может быть использована для избежания надоедливых проверок на `null`.
Рассмотрим более сложную иерархическую структуру:
```java
class Outer {
Nested nested;
}
class Nested {
Inner inner;
}
class Inner {
String foo;
}```
Для обработки строки `foo` во внешнем примере вам потребуется добавить несколько проверок на `null`, чтобы избежать потенциального исключения `NullPointerException`:
```java
Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
System.out.println(outer.nested.inner.foo);
}```
Вы можете использовать операцию `flatMap` класса `Optional`, чтобы выполнить то же самое:
```java
Optional.of(new Outer())
.flatMap(o -> Optional.ofNullable(o.nested))
.flatMap(n -> Optional.ofNullable(n.inner))
.flatMap(i -> Optional.ofNullable(i.foo))
.ifPresent(System.out::println);```
Если элемент существует, каждый вызов `flatMap` вернёт ожидаемый объект `Optional`, иначе — `null` в обёртке `Optional`.
### `reduce`
Операция `reduce` объединяет все элементы потока в один результат. В Java 8 есть три различных типа операций `reduce`. Первый тип сводит поток к одному элементу. Давайте посмотрим, как мы можем использовать этот метод для вычисления самого старого человека:
```java
persons
.stream()
.reduce((p1, p2) -> p1.age > p2.age ? p1 : p2)
.ifPresent(System.out::println); // Pamela```
Метод `reduce` принимает бинарную функцию накопления. Фактически это бинарная функция, которая принимает два аргумента одного типа. Функция в примере сравнивает возраст двух людей и возвращает человека с большим возрастом.
Второй тип операции `reduce` принимает начальное значение и бинарную функцию-накопитель. Этот метод можно использовать для создания нового объекта `Person` с агрегированным именем и возрастом из других объектов `Person`:
```java
Person result =
persons
.stream()
.reduce(new Person("", 0), (p1, p2) -> {
p1.age += p2.age;
p1.name += p2.name;
return p1;
});
System.out.format("name=%s; age=%s", result.name, result.age);
// name=MaxPeterPamelaDavid; age=76```
Третий тип операции `reduce` принимает три аргумента: начальное значение, бинарную функцию-накопитель и функцию-компоновщик бинарного типа. Поскольку тип начального значения не обязательно должен быть `Person`, мы можем использовать эту операцию сокращения для вычисления суммы возрастов всех людей:
```java
Integer ageSum = persons
.stream()
.reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2);
System.out.println(ageSum); // 76```
Вы видите результат 76. Но что происходит за кулисами? Давайте расширим приведённый выше код, добавив некоторые выходные данные отладки:
```java
Integer ageSum = persons
.stream()
.reduce(0,
(sum, p) -> {
System.out.format("accumulator: sum=%s; person=%s\n", sum, p);
return sum += p.age;
},
(sum1, sum2) -> {
System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2);
return sum1 + sum2;
});
// accumulator: sum=0; person=Max
// accumulator: sum=18; person=Peter
// accumulator: sum=41; person=Pamela
// accumлятор: sum=64; person=David```
Видно, что функция-аккумулятор выполняет всю работу. Она сначала использует начальное значение 0 и первого человека Max для вызова функции-аккумулятора. На следующих трёх шагах сумма будет продолжать увеличиваться до 76.
Подождите. Кажется, компоновщик никогда не вызывался? Раскрытие секрета произойдёт при параллельном выполнении того же потока:
```java
Integer ageSum = persons
.parallelStream()
.reduce(0,
(sum, p) -> {
System.out.format("accumulator: sum=%s; person=%s\n", sum, p);
return sum += p.age;
},
(sum1, sum2) -> {
System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2);
return sum1 + sum2;
});
// accumulator: sum=0; person=Pamela
// accumulator: sum=0; person=David
// accumulator: sum=0; person=Max
// accumulator: sum=0; person=Peter
// combiner: sum1=18; sum2=23
// combiner: sum1=23; sum2=12
// combiner: sum1=41; sum2=35```
Поведение параллельного выполнения этого потока будет совершенно другим. Теперь фактически вызывается компоновщик. Из-за параллельных вызовов функции-аккумулятора компоновщику необходимо вычислить общую сумму частичных накопленных значений.
В следующем разделе мы подробно рассмотрим параллельные потоки.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )