1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/wizardforcel-modern-java-zh

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
ch2.md 19 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
gitlife-traslator Отправлено 29.11.2024 19:52 2387d6d

Перевод текста на русский язык:

Java 8: учебник по потокам данных

Этот учебник представляет собой углублённый обзор потоков данных (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 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/wizardforcel-modern-java-zh.git
git@api.gitlife.ru:oschina-mirror/wizardforcel-modern-java-zh.git
oschina-mirror
wizardforcel-modern-java-zh
wizardforcel-modern-java-zh
master