Перейти к основному содержимому

Введение в потоки Java 8

· 5 мин. чтения

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 .