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
4
Задача
Java Core, 6 уровень, 3 лекция
Недоступна
My first thread
Вот и наступил этот знаменательный момент! Крепитесь: вам предстоит создать свою собственную нить (или поток, если угодно). Создайте public static class TestThread — нить с интерфейсом Runnable. TestThread должен выводить в консоль "My first thread". Справитесь?
undefined
4
Задача
Java Core, 6 уровень, 3 лекция
Недоступна
My second thread
Продолжаем распутывать нити. На этот раз нам нужно создать public static класс TestThread, унаследованный от класса Thread. После этого создаем статический блок внутри TestThread, который выводит в консоль "it's a static block inside TestThread". Ну а метод run должен выводить в консоль "it's a run method".
undefined
9
Задача
Java Core, 6 уровень, 3 лекция
Недоступна
Список и нити
Множим нити снова и снова. В методе main добавьте в статический объект list пять нитей. Каждая нить должна быть новым объектом класса Thread, работающим со своим объектом класса SpecialThread. Метод run класса SpecialThread должен выводить "it's a run method inside SpecialThread".
undefined
4
Задача
Java Core, 6 уровень, 3 лекция
Недоступна
Вывод стек-трейса
Вы еще помните о трассировке стека и о том, что запускаемый в настоящий момент метод находится на вершине стека? Будем вспоминать, решая задачу: вам предстоит создать таск (public static класс SpecialThread, который реализует интерфейс Runnable). SpecialThread должен выводить в консоль свой стек-трейс.
undefined
9
Задача
Java Core, 6 уровень, 3 лекция
Недоступна
Поговорим о музыке
Тяга к искусству есть даже у роботов-программистов! Сегодня вот речь пойдёт о струнно-смычковых инструментах. У нас есть класс скрипка (Violin). Нужно его изменить так, чтобы он стал таском для нити. Для этого используйте интерфейс MusicalInstrument. А затем уже можно и "поиграть", и вывести продолжительность игры.