1. Список методов типа Stream
Класс Stream
был создан для того, чтобы можно было легко конструировать цепочки потоков данных. Для этого у объекта типа Stream<T>
есть методы, которые возвращают новые объекты типа Stream
.
Каждый из этих потоков данных умеет делать одно простое действие, зато если их объединить в цепочки, да еще и добавить к этому такую интересную вещь как лямбда-функции, на выходе можно получить очень мощную вещь. Скоро вы в этом сами убедитесь.
Вот какие методы есть у класса Stream
(только самые основные):
Методы | Описание |
---|---|
|
Создает поток из набора объектов |
|
Генерирует поток по заданному правилу |
|
Объединяет вместе несколько потоков |
|
Фильтрует данные: пропускает только данные, которые соответствуют заданному правилу |
|
Удаляет дубликаты: не пропускает данные, которые уже были |
|
Сортирует данные |
|
Выполняет действие над каждым данным |
|
Обрезает данные после достижения лимита |
|
Пропускает первые n данных |
|
Преобразовывает данные из одного типа в другой |
|
Преобразовывает данные из одного типа в другой |
|
Проверяет, что среди данных потока есть хоть одно, которое соответствует заданному правилу |
|
Проверяет, что все данные в потоке соответствуют заданному правилу |
|
Проверяет, что никакие данные в потоке не соответствуют заданному правилу |
|
Возвращает первый найденный элемент, который соответствует правилу |
|
Возвращает любой элемент из потока, который соответствует правилу |
|
Ищет минимальный элемент в потоке данных |
|
Возвращает максимальный элемент в потоке данных |
|
Возвращает количество элементов в потоке данных |
|
Вычитывает все данные из потока и возвращает их в виде коллекции |
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);
}
Как видите, код значительно компактнее. Кроме этого, каждая строчка стрима – это какое-то одно действие, поэтому их можно читать, как предложения на английском языке:
|
переход от Stream<Owner> к Stream<Pet> |
|
в потоке данных оставляем только котов |
|
в потоке данных оставляем только рыжих |
|
сортируем по возрасту в убывающем порядке |
|
берем имена |
|
результат складываем в список |
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<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
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ