1. Список методов типа Stream

Класс Stream был создан для того, чтобы можно было легко конструировать цепочки потоков данных. Для этого у объекта типа Stream<T> есть методы, которые возвращают новые объекты типа Stream.

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

Вот какие методы есть у класса Stream (только самые основные):

Методы Описание
Stream<T> of()
Создает поток из набора объектов
Stream<T> generate()
Генерирует поток по заданному правилу
Stream<T> concat()
Объединяет вместе несколько потоков
Stream<T> filter()
Фильтрует данные: пропускает только данные, которые соответствуют заданному правилу
Stream<T> distinct()
Удаляет дубликаты: не пропускает данные, которые уже были
Stream<T> sorted()
Сортирует данные
Stream<T> peek()
Выполняет действие над каждым данным
Stream<T> limit(n)
Обрезает данные после достижения лимита
Stream<T> skip(n)
Пропускает первые n данных
Stream<R> map()
Преобразовывает данные из одного типа в другой
Stream<R> flatMap()
Преобразовывает данные из одного типа в другой
boolean anyMatch()
Проверяет, что среди данных потока есть хоть одно, которое соответствует заданному правилу
boolean allMatch()
Проверяет, что все данные в потоке соответствуют заданному правилу
boolean noneMatch()
Проверяет, что никакие данные в потоке не соответствуют заданному правилу
Optional<T> findFirst()
Возвращает первый найденный элемент, который соответствует правилу
Optional<T> findAny()
Возвращает любой элемент из потока, который соответствует правилу
Optional<T> min()
Ищет минимальный элемент в потоке данных
Optional<T> max()
Возвращает максимальный элемент в потоке данных
long count()
Возвращает количество элементов в потоке данных
R collect()
Вычитывает все данные из потока и возвращает их в виде коллекции

2. Intermediate и terminal методы Stream

Как вы заметили, не все методы, представленные в таблице выше, возвращают Stream. Это связано с тем, что методы класса Stream можно разделить на промежуточные (intermediate, non‑terminal) и конечные (terminal).

Промежуточные методы

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

Конечные методы

Конечные методы возвращают значение, тип которого отличен от типа Stream.

Цепочка вызовов методов

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

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

То есть, без вызова конечного метода, данные в потоке данных никак не обрабатываются. И только после вызова терминального метода, данные начинают обрабатываться по правилам, заданным цепочкой вызовов методов.

stream()
  .intemediateOperation1()
  .intemediateOperation2()
  ...
  .intemediateOperationN()
  .terminalOperation();
Общий вид цепочки вызовов

Сравнение промежуточных и конечных методов:

промежуточные конечные
Тип возвращаемого значения Stream не Stream
Возможность объединения нескольких методов данного типа в цепочку вызовов да нет
Количество методов в одной цепочке вызовов любое не более одного
Производит конечный результат нет да
Запускает обработку данных в потоке нет да

Давайте рассмотрим пример.

Есть клуб любителей животных. Завтра у них день «рыжего кота». В клубе есть владельцы животных, у каждого из которых есть список питомцев. Это могут быть не только коты.

Задача: нужно выбрать имена всех рыжих котов, чтобы на завтра распечатать для них именные поздравительные открытки с «Профессиональным праздником». Открытки должны быть отсортированы по возрасту кота: от более старого до более молодого.

Вначале приведем вспомогательные классы для решения этой задачи:

public enum Color {
   WHITE,
   BLACK,
   DARK_GREY,
   LIGHT_GREY,
   FOXY,
   GREEN,
   YELLOW,
   BLUE,
   MAGENTA
}
public abstract class Animal {
   private String name;
   private Color color;
   private int age;

   public Animal(String name, Color color, int age) {
      this.name = name;
      this.color = color;
      this.age = age;
   }

   public String getName() {
      return name;
   }

   public Color getColor() {
      return color;
   }

   public int getAge() {
      return age;
   }
}
public class Cat extends Animal{
   public Cat(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Dog extends Animal {
   public Dog(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Parrot extends Animal {
   public Parrot(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Pig extends Animal {
   public Pig(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Snake extends Animal {
   public Snake(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Owner {
   private String name;
   private List<Animal> pets = new ArrayList<>();

   public Owner(String name) {
      this.name = name;
   }

   public List<Animal> getPets() {
      return pets;
   }
}

Теперь рассмотрим класс Selector, в котором будет производиться выбор по приведенным критериям:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Selector {
   private static List<Owner> owners;

   private static void initData() {
      final Owner owner1 = new Owner("Олег Малашков");
      owner1.getPets().addAll(List.of(
            new Cat("Барон", Color.BLACK, 3),
            new Cat("Султан", Color.DARK_GREY, 4),
            new Dog("Эльза", Color.WHITE, 0)
      ));

      final Owner owner2 = new Owner("Дмитрий Васильков");
      owner2.getPets().addAll(List.of(
            new Cat("Рыжик", Color.FOXY, 7),
            new Cat("Барсик", Color.FOXY, 5),
            new Parrot("Адмирал", Color.BLUE, 3)
      ));

      final Owner owner3 = new Owner("Наталия Криж");
      owner3.getPets().addAll(List.of(
            new Dog("Арнольд", Color.FOXY, 3),
            new Pig("Пылесос", Color.LIGHT_GREY, 8)
      ));

      final Owner owner4 = new Owner("Павел Мурахов");
      owner4.getPets().addAll(List.of(
            new Snake("Удав", Color.DARK_GREY, 2)
      ));

      final Owner owner5 = new Owner("Антон Федоренко");
      owner5.getPets().addAll(List.of(
            new Cat("Фишер", Color.BLACK, 16),
            new Cat("Зорро", Color.FOXY, 14),
            new Cat("Марго", Color.WHITE, 3),
            new Cat("Забияка", Color.DARK_GREY, 1)
      ));

      owners = List.of(owner1, owner2, owner3, owner4, owner5);
   }
}

Осталось дописать код метода main, в котором вначале вызовем метод initData(), который заполнит данными список владельцев животных в клубе, а потом выберет имена рыжих котов, отсортированных по возрасту в убывающем порядке.

Вначале рассмотрим код, не использующий стримы для решения этой задачи:

public static void main(String[] args) {
   initData();

   List<String> findNames = new ArrayList<>();
   List<Cat> findCats = new ArrayList<>();
   for (Owner owner : owners) {
      for (Animal pet : owner.getPets()) {
         if (Cat.class.equals(pet.getClass()) && Color.FOXY == pet.getColor()) {
            findCats.add((Cat) pet);
         }
      }
   }

   Collections.sort(findCats, new Comparator<Cat>() {
      public int compare(Cat o1, Cat o2) {
         return o2.getAge() - o1.getAge();
      }
   });

   for (Cat cat : findCats) {
      findNames.add(cat.getName());
   }

   findNames.forEach(System.out::println);
}

А теперь давайте посмотрим на альтернативный вариант:

public static void main(String[] args) {
   initData();

   final List<String> findNames = owners.stream()
           .flatMap(owner -> owner.getPets().stream())
           .filter(pet -> Cat.class.equals(pet.getClass()))
           .filter(cat -> Color.FOXY == cat.getColor())
           .sorted((o1, o2) -> o2.getAge() - o1.getAge())
           .map(Animal::getName)
           .collect(Collectors.toList());

   findNames.forEach(System.out::println);
}

Как видите, код значительно компактнее. Кроме этого, каждая строчка стрима – это какое-то одно действие, поэтому их можно читать, как предложения на английском языке:

.flatMap(owner -> owner.getPets().stream())
переход от Stream<Owner> к Stream<Pet>
.filter(pet -> Cat.class.equals(pet.getClass()))
в потоке данных оставляем только котов
.filter(cat -> Color.FOXY == cat.getColor())
в потоке данных оставляем только рыжих
.sorted((o1, o2) -> o2.getAge() - o1.getAge())
сортируем по возрасту в убывающем порядке
.map(Animal::getName)
берем имена
.collect(Collectors.toList())
результат складываем в список

3. Создание потоков

Среди методов класса Stream есть три метода, которые мы еще не рассмотрели. Задача этих трех методов — создавать новые потоки.

Метод Stream<T>.of(T obj)

Метод of() создает поток, состоящий из одного элемента. Обычно это нужно, если, допустим, функция принимает в качестве параметра объект типа Stream<T>, а у вас есть только объект типа T. Тогда вы можете легко и просто с помощью метода of() получить поток, состоящий из одного элемента.

Пример:

Stream<Integer> stream = Stream.of(1);

Метод Stream<T> Stream.of(T obj1, T obj2, T obj3, ...)

Метод of() создает поток, состоящий из переданных элементов. Количество элементов может быть любым. Пример:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

Метод Stream<T> Stream.generate(Supplier<T> obj)

Метод generate() позволяет задать правило, по которому будет генерироваться очередной элемент потока при его запросе. Например, можно каждый раз отдавать случайное число.

Пример:

Stream<Double> s = Stream.generate(Math::random);

Метод Stream<T> Stream.concat(Stream<T> a, Stream<T> b)

Метод concat() объединяет два переданных потока в один. При чтении данных сначала будут прочитаны данные из первого потока, а затем из второго. Пример:

Stream<Integer> stream1 = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> stream2 = Stream.of(10, 11, 12, 13, 14);
Stream<Integer> result = Stream.concat(stream1, stream2);

4. Фильтрация данных

Еще 6 методов создают новые потоки данных, позволяющие объединять потоки в цепочки разной сложности.

Метод Stream<T> filter(Predicate<T>)

Этот метод возвращает новый поток данных, который фильтрует данные из потока-источника согласно переданному правилу. Метод нужно вызывать у объекта типа Stream<T>.

Для задания правила фильтрации можно использовать лямбда-функцию, которая затем будет преобразована компилятором в объект типа Predicate<T>.

Примеры:

Цепочки потоков Пояснение
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> stream2 = stream.filter(x -> (x < 3));

Оставляем только числа меньше трех
Stream<Integer> stream = Stream.of(1, -2, 3, -4, 5);
Stream<Integer> stream2 = stream.filter(x -> (x > 0));

Оставляем только числа больше нуля

Метод Stream<T> sorted(Comparator<T>)

Этот метод возвращает новый поток данных, который сортирует данные из потока-источника. В качестве параметра можно передать компаратор, который будет задавать правила сравнения двух элементов потока данных.

Метод Stream<T> distinct()

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

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.distinct(); // 1, 2, 3, 4, 5

Метод Stream<T> peek(Consumer<T>)

Этот метод возвращает новый поток данных, хотя данные в нем те же, что и в потоке источнике. Но когда запрашивается очередной элемент из потока, для него вызывается функция, которую вы передали в метод peek().

Если в метод peek() передать функцию System.out::println, тогда все объекты будут выводиться на экран в момент, когда они будут проходить через поток.

Метод Stream<T> limit(int n)

Этот метод возвращает новый поток данных, который содержит только первые n данных из потока данных источника. Все остальные данные отбрасываются. Пример:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.limit(3); // 1, 2, 3

Метод Stream<T> skip(int n)

Этот метод возвращает новый поток данных, который содержит все те же данные, что и поток-источник, но пропускает (игнорирует) первые n данных. Пример:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.skip(3); // 4, 5, 2, 2, 2, 3, 4

undefined
19
Задача
Java Syntax Pro, 19 уровень, 4 лекция
Недоступна
Рейтинг языков программирования
Класс Language представляет собой модель языка программирования. В этом классе есть два поля: name - название языка программирования, ranking - рейтинг использования языка программирования по состоянию на сентябрь 2020 года, согласно TIOBE.
undefined
19
Задача
Java Syntax Pro, 19 уровень, 4 лекция
Недоступна
Будущее за электрокарами
Современный мир отказывается от двигателей внутреннего сгорания, и эра электрокаров не за горами. Нужно, чтобы наша программа могла отфильтровать электрокары от всех остальных автомобилей. Для этих целей есть метод onlyElectricCars(ArrayList<Car>), который фильтрует список автомобилей, получаемых в
undefined
19
Задача
Java Syntax Pro, 19 уровень, 4 лекция
Недоступна
Без повторов
Метод getDistinct(ArrayList<String>) получает список слов, в котором содержатся повторы. Твоя задача — реализовать этот метод так, чтобы он возвращал поток слов без повторов (каждое слово — по одному разу). В этом тебе поможет метод distinct() объекта типа Stream<String>. Метод main() не принимает у