Включите исполнение JavaScript в браузере, чтобы запустить приложение.

Stream API в Java

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

Основные концепции Stream API

Stream API в Java разработан в соответствии со следующими концепциями:

  1. Ленивость вычислений. Промежуточные операции не выполняются сразу: они откладываются до тех пор, пока не будет вызвана терминальная. Это позволяет выполнять только необходимые вычисления и снижать общее количество операций, оптимизируя использование ресурсов.
  2. Параллельная обработка. Обычно операции выполняются друг за другом. Однако если нужно обработать большой объем данных, можно воспользоваться методом parallelStream() — так операция будет выполнена быстрее.
  3. Однократное использование стрима. После того как была вызвана терминальная операция, поток нельзя использовать повторно — для этого нужно создать новый стрим.
  4. Отсутствие хранения данных. Стримы не выступают хранилищем, они лишь используют (не изменяют) данные из указанных источников. 
  5. Декларативный стиль. Stream API не нужно предоставлять конкретную реализацию операций над данными: достаточно описать, что нужно с ними сделать. Это значительно сокращает код и делает его более читаемым. Рассмотрим пример, иллюстрирующий эту концепцию:
// Стандартная реализация цикла

public class StreamVsLoop {

  public static void main(String[] args) {

      List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

      List<Integer> result = new ArrayList<>();

      for (Integer number : numbers) {

          if (number % 2 == 0) { 

              result.add(number * 2); 

          }

      }

      for (Integer number : result) {

          System.out.println(number); 

      }

  }

}

// Эквивалентный пример с Stream API в Java

public class StreamVsLoop {

  public static void main(String[] args) {

      List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

      numbers.stream()

             .filter(n -> n % 2 == 0)   

             .map(n -> n * 2)       

             .forEach(System.out::println); 

  }

}
java

Создание Stream

Для создания стрима из коллекций используется метод stream():

List<Integer> list = Arrays.asList(1, 2, 3, 4);

Stream<Integer> stream = list.stream();
java

Для преобразования массива в поток предусмотрен метод stream() класса Arrays:

int[] numbers = {1, 2, 3, 4};

IntStream intStream = Arrays.stream(numbers);
java

Для примитивных типов данных также можно использовать метод boxed() — он преобразует элементы в объекты.

Метод Stream.of() позволяет создать поток из набора элементов:

Stream<String> stream = Stream.of("a", "b", "c");

stream.forEach(System.out::println);
java

Создать бесконечный поток можно с помощью метода generate(), однако лучше явно ограничивать поток с помощью limit():

Stream<Integer> infiniteStream = Stream.generate(() -> 1 );

infiniteStream.limit( 5 ).forEach(System.out::println);
java

Класс Stream.Builder позволяет построить стрим из добавленных элементов:

Stream<String> stream = Stream.<String>builder()

    .add("a")

    .add("b")

    .add("c")

    .build();
java

Для создания пустых стримов используется метод empty() — он позволяет не допустить выброса NullPointerException:

Stream<String> emptyStream = Stream.empty();
java

Промежуточные операции

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

Рассмотрим основные промежуточные методы Stream API в Java:

  • filter(Predicate) возвращает новый поток с элементами, соответствующими указанному условию:
// Останутся только четные элементы

Stream<Integer> filtered = Stream.of(1, 2, 3, 4, 5, 6, 7)

    .filter(n -> n % 2 == 0);

filtered.forEach(System.out::println); // 2, 4, 6
java
  • map(Function) выполняет преобразование каждого элемента потока в соответствии с указанным лямбда-выражением или ссылки на метод. Map() в Java Stream API также возвращает новый поток, в то время как исходный не меняется:
// Преобразование элементов в верхний регистр

Stream<String> mapped = Stream.of("Math", "Biology", "Chemistry")

    .map(String::toUpperCase);

mapped.forEach(System.out::println); // MATH, BIOLOGY, CHEMISTRY
java
  • flatMap() преобразует каждый элемент в поток, а затем разворачивает каждый из этих потоков в единый стрим. Метод используется для работы с вложенными структурами данных:
List<List<Integer>> nestedLists = Arrays.asList(

  Arrays.asList(1, 2, 3),

  Arrays.asList(4, 5),

  Arrays.asList(6, 7, 8, 9)

);

List<Integer> flatList = nestedLists.stream()

  .flatMap(List::stream) 

flatList.forEach(System.out::println); // 1, 2, 3, 4, 5, 6, 7, 8, 9
java
  • sorted() сортирует элементы в естественном порядке или с помощью предоставленного компаратора:
// В естественном порядке

Stream<Integer> sorted = Stream.of(3, 1, 4, 2)

    .sorted();

sorted.forEach(System.out::println); // 1, 2, 3, 4

// С компаратором

Stream<String> sortedDesc = Stream.of("Math", "Biology", "Chemistry")

    .sorted(Comparator.reverseOrder());

sortedDesc.forEach(System.out::println); // Biology, Chemistry, Math
java
  • distinct() предназначен для удаления дублирующихся элементов потока. Для проверки используется метод equals():
Stream<Integer> distinct = Stream.of(1, 2, 2, 3, 4, 4)

    .distinct();

distinct.forEach(System.out::println); // 1, 2, 3, 4
java
  • limit(n) возвращает стрим, содержащий ограниченное n число элементов. Если в потоке меньше элементов, чем n, то стрим вернется со всеми из них:
Stream<Integer> limited = Stream.of(1, 2, 3, 4, 5)

    .limit(3);

limited.forEach(System.out::println); // 1, 2, 3
java
  • skip(n) возвращает новый поток, пропуская первые n элементов. Если в потоке меньше элементов, чем n, то вернется пустой стрим:
Stream<Integer> skipped = Stream.of(1, 2, 3, 4, 5)

    .skip(3);

limited.forEach(System.out::println); // 4, 5
java
  • peek() предназначен для добавления промежуточных операций для каждого элемента потока, при этом не изменяя исходный стрим. Обычно этот метод в Stream API Java используется для отладки и логирования:
Stream<Integer> peeked = Stream.of(1, 2, 3)

    .peek(n -> System.out.println("Step: " + n))

    .map(n -> n * 2);

peeked.forEach(System.out::println);

/* 

Step: 1

2

Step: 2

4

Step: 3

6

*\
java

Из промежуточных операций можно собирать цепочки, например:

import java.util.*;

import java.util.stream.*;

public class FlatMapExample {

    public static void main(String[] args) {

        // Список списков предметов

        List<List<String>> studentCourses = Arrays.asList(

            Arrays.asList("Mathematics", "Physics", "Chemistry"),

            Arrays.asList("Biology", "Chemistry"),

            Arrays.asList("History", "Geography", "Philosophy")

        );

        // flatMap() для получения плоского списка предметов

        List<String> allCourses = studentCourses.stream()

            .flatMap(List::stream) // Преобразование предметов в стрим и «развертывание» 

            .distinct()            // Удаление дубликатов

            .sorted()              // Сортировка по алфавиту

            .collect(Collectors.toList()); // Сбор в список

        System.out.println(allCourses);

    }

}

// [Biology, Chemistry, Geography, History, Mathematics, Philosophy, Physics]
java

Терминальные операции

Терминальные операции нужны для запуска выполнения всей цепочки операций над потоком. Они завершают поток — после вызова терминальной операции работать со стримом больше нельзя.

Рассмотрим главные промежуточные методы:

  • forEach(Consumer) применяет операцию к каждому элементу потока. Этот метод ничего не возвращает, но производит побочный эффект:
List<String> names = Arrays.asList("Mary", "Ann", "Nick");

names.stream()

    .forEach(System.out::println);

/* 

Mary

Ann

Nick

*\
java
  • collect(Collector) собирает элементы в структуру данных: строку, множество, список и т. д. Конкретная структура определяется методом из класса Collector, например, toSet() для преобразования во множество, toList — в список, joining() — в строку:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);

List<Integer> evenNumbers = numbers.stream()

    .filter(n -> n % 2 == 0)

    .collect(Collectors.toList());

System.out.println(evenNumbers); // [2, 4]
java
  • count() возвращает количество элементов в потоке:
long count = Stream.of(1, 2, 3, 4, 5)

    .filter(n -> n % 2 == 0)

    .count();

System.out.println(count); // 2
java
  • allMatch(Predicate), anyMatch(Predicate), noneMatch(Predicate) проверяют соответствие элементов определенному условию. allMatch(Predicate) возвращает true, если все элементы соответствуют предикату, anyMatch(Predicate) — если хотя бы один, noneMatch(Predicate) — если все элементы не удовлетворяют условию:
boolean allEven = Stream.of(2, 4, 6).allMatch(n -> n % 2 == 0);

System.out.println(allEven); // true

boolean hasOdd = Stream.of(2, 4, 6, 7).anyMatch(n -> n % 2 != 0);

System.out.println(hasOdd); // true

boolean noGreaterThanTen = Stream.of(2, 4, 6).noneMatch(n -> n > 10);

System.out.println(noGreaterThanTen); // true
java
  • reduce() формирует один результат из всех элементов потока на основе указанной операции. Обычно используется для сложения, умножения и других операций агрегирования:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);

int sum = numbers.stream()

    .reduce(0, Integer::sum);

System.out.println(sum); // 10
java
  • findFirst() возвращает первый элемент потока, а findAny() — любой. findAny() используется для параллельных потоков, так как позволяет быстро получить результат:
Optional<Integer> first = Stream.of(1, 2, 3, 4).findFirst();

first.ifPresent(System.out::println); // 1
java

Параллельные потоки

Параллельная обработка данных позволяет ускорить выполнение операций на многоядерных процессорах. Stream API поддерживает такую возможность через методы parallelStream() (для коллекций) или parallel():

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

int sum = numbers.parallelStream()

    .reduce(0, Integer::sum);

System.out.println("Sum: " + sum); // 10
java

Эти методы не гарантируют порядок обработки элементов, однако есть метод forEachOrdered(), который сохраняет последовательность, но снижает производительность. Параллельная обработка в Stream API работает за счет разделения потока на подзадачи и их последующего независимого выполнения на разных ядрах. После завершения обработки каждого параллельного потока результаты выполнения объединяются.

Шпаргалка по Stream API в Java

  • Механизм позволяет писать более читаемый код в декларативном стиле.
  • Стрим может быть создан из разных источников: коллекций, массивов, строк и других.
  • Промежуточные методы позволяют формировать цепочки операций, они не выполняются, пока не будет вызвана терминальная операция.
  • Поддерживается параллельный режим обработки с помощью parallelStream() или parallel().