1. Обзор
В этой статье мы кратко рассмотрим одну из основных новых функций, добавленных в Java 8, — потоки.
Мы объясним, что такое потоки, и продемонстрируем создание и основные операции с потоками на простых примерах.
2. Потоковое API
Одной из основных новых функций Java 8 является введение потоковой функциональности — java.util.stream
— которая содержит классы для обработки последовательностей элементов.
Центральным классом API является Stream<T> .
В следующем разделе показано, как можно создавать потоки с использованием существующих источников поставщиков данных.
2.1. Создание потока
Потоки могут быть созданы из различных источников элементов, например коллекции или массива, с помощью методов 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();
2.2. Многопоточность с потоками
Stream API также упрощает многопоточность, предоставляя метод parallelStream()
, который выполняет операции над элементами потока в параллельном режиме.
Код ниже позволяет запускать метод doWork()
параллельно для каждого элемента потока:
list.parallelStream().forEach(element -> doWork(element));
В следующем разделе мы представим некоторые основные операции Stream API.
3. Потоковые операции
Есть много полезных операций, которые можно выполнять над потоком.
Они делятся на промежуточные операции (возвращают Stream<T>
) и терминальные операции (возвращают результат определенного типа). Промежуточные операции позволяют создавать цепочки.
Также стоит отметить, что операции над потоками не изменяют источник.
Вот краткий пример:
long count = list.stream().distinct().count();
Итак, метод different()
представляет собой промежуточную операцию, которая создает новый поток уникальных элементов предыдущего потока. А метод count()
— это терминальная операция ,
которая возвращает размер потока.
3.1. Итерация
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"));
3.2. Фильтрация
Метод 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"));
3.3. Отображение
Чтобы преобразовать элементы Stream
, применив к ним специальную функцию, и собрать эти новые элементы в Stream
, мы можем использовать метод 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>
, применяя определенное лямбда-выражение к каждому элементу исходного Stream
.
Если у вас есть поток, в котором каждый элемент содержит свою собственную последовательность элементов, и вы хотите создать поток этих внутренних элементов, вы должны использовать метод 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>
будет утерян .
3.4. Соответствие
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
Опять же, это разумно, так как мы не можем найти элемент, удовлетворяющий этому условию.
3.5. Снижение
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);
3.6. Сбор
Сокращение также может быть обеспечено методом collect()
типа Stream.
Эта операция очень удобна в случае преобразования потока в Коллекцию
или Карту
и представления потока в виде одной строки .
Существует служебный класс Collectors
, который обеспечивает решение почти для всех типичных операций по сбору. Для некоторых нетривиальных задач можно создать собственный Коллектор
.
List<String> resultList
= list.stream().map(element -> element.toUpperCase()).collect(Collectors.toList());
Этот код использует операцию терминала collect()
для сокращения Stream<String>
до List<String>.
4. Выводы
В этой статье мы кратко коснулись потоков Java — определенно одной из самых интересных функций Java 8.
Есть много более продвинутых примеров использования Streams; цель этой статьи состояла только в том, чтобы предоставить быстрое и практическое введение в то, что вы можете начать делать с функциональностью, и в качестве отправной точки для изучения и дальнейшего изучения.
Исходный код, сопровождающий статью, доступен на GitHub .