В этой статье кратко рассмотрим одну из основных функций, добавленных в 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.