Перейти к содержанию

Введение в Stream API Java 8

В этой статье кратко рассмотрим одну из основных функций, добавленных в Java 8, – Stream (поток), а также выясним, что такое потоки, и продемонстрируем создание и основные операции с потоками на простых примерах.

Stream API

Одной из основных новых функций Java 8 является введение потоковой функциональности – java.util.stream – которая содержит классы для обработки последовательностей элементов.

Центральным классом API является Stream<T>. В следующем разделе показано, как можно создавать потоки с использованием существующих источников данных.

Создание потока

Потоки могут быть созданы из различных источников элементов, например, коллекции или массива с помощью методов stream() и of():

String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
stream = Stream.of("a", "b", "c");

Метод по умолчанию stream() добавлен в интерфейс Collection и позволяет создать Stream<T>, используя любую коллекцию в качестве источника элемента:

Stream<String> stream = list.stream();

Многопоточность

Stream API также упрощает многопоточность, предоставляя метод parallelStream(), который выполняет операции над элементами потока в параллельном режиме.

Код ниже позволяет запускать метод doWork() параллельно для каждого элемента потока:

list.parallelStream().forEach(element -> doWork(element));

В следующем разделе представим некоторые основные операции Stream API.

Потоковые операции

Есть много полезных операций, которые можно выполнять над потоком.

Они делятся на промежуточные (возвращают Stream<T>) и терминальные операции (возвращают результат определенного типа). Промежуточные операции позволяют создавать цепочки.

Также стоит отметить, что операции над потоками не изменяют источник.

Вот краткий пример:

long count = list.stream().distinct().count();

Метод distinct() представляет собой промежуточную операцию, которая создает новый поток уникальных элементов предыдущего потока. А метод count() – это терминальная операция, которая возвращает размер потока.

Итерация

Stream API помогает заменить циклы for, for-each и while. Это позволяет сконцентрироваться на логике работы, а не на переборе последовательности элементов. Например:

for (String string : list) {
    if (string.contains("a")) {
        return true;
    }
}

Этот код можно изменить всего одной строкой кода Java 8:

boolean isExist = list.stream().anyMatch(element -> element.contains("a"));

Фильтрация

Метод filter() позволяет выбрать поток элементов, удовлетворяющих предикату.

Например, рассмотрим следующий список:

ArrayList<String> list = new ArrayList<>();
list.add("One");
list.add("OneAndOnly");
list.add("Derek");
list.add("Change");
list.add("factory");
list.add("justBefore");
list.add("Italy");
list.add("Italy");
list.add("Thursday");
list.add("");
list.add("");

Следующий код создает Stream<String> из List<String>, находит все элементы этого потока, которые содержат char «d», и создает новый поток, содержащий только отфильтрованные элементы:

Stream<String> stream = list.stream().filter(element -> element.contains("d"));

Маппинг

Чтобы преобразовать элементы потока, применив к ним специальную функцию, и собрать эти новые элементы в поток, можем использовать метод map():

List<String> uris = new ArrayList<>();
uris.add("C:\\My.txt");
Stream<Path> stream = uris.stream().map(uri -> Paths.get(uri));

Приведенный выше код преобразует Stream<String> в Stream<Path>, применяя определенное лямбда-выражение к каждому элементу исходного потока.

Если есть поток, в котором каждый элемент содержит собственную последовательность элементов, и вы хотите создать поток этих внутренних элементов, то следует использовать метод flatMap():

List<Detail> details = new ArrayList<>();
details.add(new Detail());
Stream<String> stream
  = details.stream().flatMap(detail -> detail.getParts().stream());

В этом примере есть список элементов типа Detail. Класс Detail содержит поле PARTS, представляющее собой List<String>. С помощью метода flatMap() каждый элемент из поля PARTS будет извлечен и добавлен в новый результирующий поток. После этого исходный Stream<Detail> будет потерян.

Соответствие

Stream API предоставляет удобный набор инструментов для проверки элементов последовательности в соответствии с некоторым предикатом. Для этого можно использовать один из следующих методов: anyMatch(), allMatch(), noneMatch(). Их имена говорят сами за себя. Это терминальные операции, которые возвращают логическое значение:

boolean isValid = list.stream().anyMatch(element -> element.contains("h")); // true
boolean isValidOne = list.stream().allMatch(element -> element.contains("h")); // false
boolean isValidTwo = list.stream().noneMatch(element -> element.contains("h")); // false

Для пустых потоков метод allMatch() с любым заданным предикатом вернет true:

Stream.empty().allMatch(Objects::nonNull); // true

Это разумное значение по умолчанию, поскольку мы не можем найти ни одного элемента, не удовлетворяющего предикату. Точно так же метод anyMatch() всегда возвращает false для пустых потоков:

Stream.empty().anyMatch(Objects::nonNull); // false

Опять же, это разумно, так как мы не можем найти элемент, удовлетворяющий этому условию.

Сведение

Stream API позволяет свести последовательность элементов к некоторому значению по заданной функции с помощью метода reduce() типа Stream. Этот метод принимает два параметра: первый – начальное значение, второй – функция-аккумулятор.

Представьте, что есть List<Integer> и нужно получить сумму всех этих элементов и некоторое начальное целое число (в этом примере 23). Для этого можно запустить следующий код, и результатом будет 26 (23 + 1 + 1 + 1).

List<Integer> integers = Arrays.asList(1, 1, 1);
Integer reduced = integers.stream().reduce(23, (a, b) -> a + b);

Сбор

Сведение также может быть обеспечено методом collect() типа Stream. Эта операция очень удобна в случае преобразования потока в Collection или Map и представления потока в виде одной строки. Существует служебный класс Collectors, который обеспечивает решение почти для всех типичных операций по сбору. Для некоторых нетривиальных задач можно создать собственный коллектор.

List<String> resultList 
  = list.stream().map(element -> element.toUpperCase()).collect(Collectors.toList());

Этот код использует терминальную операцию collect() для сведения Stream<String> в List<String>.

Заключение

В этой статье кратко затронули Stream API – одну из самых интересных функций Java 8.

Существует много более продвинутых примеров использования потоков; цель этой статьи состояла только в кратком введении в то, что можно делать с функциональностью, а также в качестве отправной точки для дальнейшего изучения.

Исходный код, сопровождающий статью, доступен на GitHub.

Оригинал