Stream API
Осваиваем Stream API — мощный инструмент для обработки коллекций. Фильтрация, преобразование, сортировка и агрегация данных в один цепочки вызовов
Что такое Stream API
Stream API — это механизм для обработки коллекций и других наборов данных в функциональном стиле. Вместо того чтобы писать циклы и вручную обрабатывать каждый элемент, вы описываете цепочку операций, которые нужно выполнить.
Аналогия из жизни: Представьте конвейер на заводе. Сырьё (данные) поступает на один конец, проходит через一系列 станций (операций), и на другом конце получается готовый продукт. Каждая станция делает одну операцию: фильтрует, преобразует, сортирует. Stream API — это такой конвейер для данных.
Важно: Stream не хранит данные. Это поток обработки, который проходит по элементам коллекции, применяя операции. После прохождения поток «используется» — его нельзя повторить.
Stream API появился в Java 8 и доступен для всех коллекций через метод stream().
Создание Stream
Есть несколько способов создать Stream:
1. Из коллекции:
import java.util.ArrayList;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Анна");
names.add("Борис");
names.add("Вика");
Stream<String> stream = names.stream();
}
}
2. Из массива:
import java.util.stream.Stream;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String[] fruits = {"Яблоко", "Банан", "Вишня"};
Stream<String> stream = Arrays.stream(fruits);
Stream<String> stream2 = Stream.of("Яблоко", "Банан", "Вишня");
}
}
3. Генерация числовых потоков:
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<Integer> range = IntStream.range(1, 6);
Stream<Integer> rangeClosed = IntStream.rangeClosed(1, 5);
Stream<Double> randoms = Stream.generate(Math::random).limit(5);
Stream<Integer> ones = Stream.iterate(1, n -> n + 1).limit(10);
}
}
Промежуточные операции (Intermediate Operations)
Промежуточные операции возвращают новый Stream и выполняются лениво — то есть они не обрабатывают элементы сразу, а только описывают, что нужно сделать. Элементы обрабатываются только при вызове терминальной операции.
filter() — фильтрация:
Оставляет только элементы, для которых предикат возвращает true:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
numbers.add(6);
List<Integer> even = numbers.stream()
.filter(n -> n % 2 == 0)
.toList();
System.out.println(even); // [2, 4, 6]
}
}
map() — преобразование:
Преобразует каждый элемент с помощью функции:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Анна");
names.add("Борис");
names.add("Вика");
List<Integer> lengths = names.stream()
.map(String::length)
.toList();
System.out.println(lengths); // [4, 5, 3]
}
}
sorted() — сортировка:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Вика");
names.add("Анна");
names.add("Борис");
List<String> sorted = names.stream()
.sorted()
.toList();
System.out.println(sorted); // [Анна, Борис, Вика]
List<String> sortedByLength = names.stream()
.sorted((a, b) -> Integer.compare(a.length(), b.length()))
.toList();
System.out.println(sortedByLength); // [Вика, Анна, Борис]
}
}
Другие полезные промежуточные операции:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Анна");
names.add("Борис");
names.add("Вика");
names.add("Дмитрий");
names.add("Елена");
List<String> firstThree = names.stream()
.limit(3)
.toList();
System.out.println(firstThree); // [Анна, Борис, Вика]
List<String> withoutFirst = names.stream()
.skip(1)
.toList();
System.out.println(withoutFirst); // [Борис, Вика, Дмитрий, Елена]
List<String> unique = names.stream()
.distinct()
.toList();
System.out.println(unique);
names.stream()
.filter(name -> name.length() > 4)
.forEach(name -> System.out.println(name));
}
}
Терминальные операции (Terminal Operations)
Терминальные операции запускают обработку потока и возвращают результат. После вызова терминальной операции поток закрывается и его нельзя использовать повторно.
forEach() — обход:
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Яблоко");
fruits.add("Банан");
fruits.add("Вишня");
fruits.stream()
.filter(f -> f.length() > 4)
.forEach(f -> System.out.println(f));
}
}
collect() — сбор в коллекцию:
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
List<Integer> doubled = numbers.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
System.out.println(doubled); // [2, 4, 6, 8, 10]
Set<String> uniqueLengths = numbers.stream()
.map(n -> String.valueOf(n))
.collect(Collectors.toSet());
System.out.println(uniqueLengths);
}
}
count() — подсчёт:
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Анна");
names.add("Борис");
names.add("Вика");
names.add("Дима");
long count = names.stream()
.filter(name -> name.length() > 4)
.count();
System.out.println("Имён длиннее 4 букв: " + count); // 2
}
}
Другие терминальные операции:
import java.util.ArrayList;
import java.util.Optional;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("Сумма: " + sum); // 15
Optional<Integer> max = numbers.stream()
.max(Integer::compareTo);
System.out.println("Максимум: " + max.orElse(0)); // 5
Optional<Integer> min = numbers.stream()
.min(Integer::compareTo);
System.out.println("Минимум: " + min.orElse(0)); // 1
boolean allEven = numbers.stream()
.allMatch(n -> n % 2 == 0);
System.out.println("Все чётные? " + allEven); // false
boolean anyEven = numbers.stream()
.anyMatch(n -> n % 2 == 0);
System.out.println("Есть чётные? " + anyEven); // true
boolean noneNegative = numbers.stream()
.noneMatch(n -> n < 0);
System.out.println("Нет отрицательных? " + noneNegative); // true
}
}
Цепочки операций (Pipeline)
Главная сила Stream API — возможность строить цепочки из промежуточных операций и завершать терминальной. Это создаёт читаемый и компактный код.
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Анна");
names.add("Борис");
names.add("Вика");
names.add("Дмитрий");
names.add("Елена");
names.add("Алексей");
List<String> result = names.stream()
.filter(name -> name.length() > 4)
.sorted()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(result);
// [АЛЕКСЕЙ, БОРИС, ДМИТРИЙ, ЕЛЕНА]
}
}
Цепочка читается сверху вниз: берём имена → фильтруем по длине → сортируем → переводим в верхний регистр → собираем в список.
Более сложный пример с несколькими промежуточными операциями:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= 20; i++) {
numbers.add(i);
}
List<String> result = numbers.stream()
.filter(n -> n % 2 == 0)
.filter(n -> n > 10)
.map(n -> "Число: " + n)
.limit(3)
.collect(Collectors.toList());
System.out.println(result);
// [Число: 12, Число: 14, Число: 16]
}
}
Collectors: сбор результатов
Класс Collectors предоставляет множество способов собрать результат потока. Давайте рассмотрим самые полезные из них:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
ArrayList<String> names = new ArrayList<>();
names.add("Анна");
names.add("Борис");
names.add("Вика");
names.add("Дмитрий");
names.add("Елена");
List<String> list = names.stream()
.filter(n -> n.length() > 4)
.collect(Collectors.toList());
System.out.println("List: " + list);
Set<String> set = names.stream()
.filter(n -> n.length() > 4)
.collect(Collectors.toSet());
System.out.println("Set: " + set);
String joined = names.stream()
.collect(Collectors.joining(", "));
System.out.println("Joined: " + joined);
Map<String, Integer> nameLengths = names.stream()
.collect(Collectors.toMap(n -> n, String::length));
System.out.println("Map: " + nameLengths);
long count = names.stream()
.collect(Collectors.counting());
System.out.println("Count: " + count);
}
}
Collectors.joining() собирает строки в одну, разделяя разделителем. Collectors.toMap() создаёт Map, где ключ — результат одной функции, значение — другой.
Практический пример: анализ данных
Давайте создадим программу для анализа списка студентов с оценками:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GradeAnalyzer {
public static void main(String[] args) {
ArrayList<Integer> grades = new ArrayList<>();
grades.add(85);
grades.add(92);
grades.add(78);
grades.add(95);
grades.add(88);
grades.add(72);
grades.add(91);
grades.add(65);
long excellent = grades.stream()
.filter(g -> g >= 90)
.count();
System.out.println("Отличников: " + excellent);
double average = grades.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
System.out.println("Средний балл: " + average);
List<Integer> sorted = grades.stream()
.sorted((a, b) -> b - a)
.collect(Collectors.toList());
System.out.println("По убыванию: " + sorted);
Map<String, List<Integer>> grouped = grades.stream()
.collect(Collectors.groupingBy(
g -> g >= 90 ? "отлично" : g >= 70 ? "хорошо" : "удовлетворительно"
));
System.out.println("Группировка: " + grouped);
}
}
Практический пример: обработка строк
Stream API отлично подходит для обработки текстовых данных:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class TextProcessor {
public static void main(String[] args) {
ArrayList<String> words = new ArrayList<>();
words.add("программирование");
words.add("java");
words.add("стримы");
words.add("коллекции");
words.add("алгоритмы");
List<String> result = words.stream()
.filter(w -> w.length() > 5)
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
System.out.println(result);
String longest = words.stream()
.max((a, b) -> Integer.compare(a.length(), b.length()))
.orElse("");
System.out.println("Длиннейшее: " + longest);
int totalChars = words.stream()
.mapToInt(String::length)
.sum();
System.out.println("Всего символов: " + totalChars);
}
}
Ленивые вычисления
Важная особенность Stream API — ленивые вычисления. Промежуточные операции не выполняются, пока не вызвана терминальная операция. Это значит, что Stream может быть очень эффективным.
import java.util.ArrayList;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= 1000000; i++) {
numbers.add(i);
}
long start = System.currentTimeMillis();
long count = numbers.stream()
.filter(n -> {
System.out.println("Фильтрация: " + n);
return n % 2 == 0;
})
.limit(5)
.count();
long end = System.currentTimeMillis();
System.out.println("Найдено: " + count);
System.out.println("Время: " + (end - start) + " мс");
}
}
Обратите внимание —尽管有 миллион элементов, фильтрация остановится на 10-м чётном числе, потому что limit(5) после filter остановит обработку. Stream не будет проверять все миллион элементов.
Ленивые вычисления — это как заказ в ресторане. Вы не готовите все блюда сразу — вы ждёте, пока клиент выберет. Stream не обрабатывает элементы, пока вы не попросите результат.
Stream vs Циклы
Давайте сравним классический цикл и Stream API для одной задачи:
import java.util.ArrayList;
import java.util.List;
public class Comparison {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
numbers.add(6);
numbers.add(7);
numbers.add(8);
numbers.add(9);
numbers.add(10);
List<Integer> result1 = new ArrayList<>();
for (int num : numbers) {
if (num % 2 == 0) {
result1.add(num * num);
}
}
System.out.println("Цикл: " + result1);
List<Integer> result2 = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.toList();
System.out.println("Stream: " + result2);
}
}
Оба варианта делают одно и то же, но Stream-версия чище и выразительнее. Вот сравнение:
| Критерий | Цикл | Stream |
|---|---|---|
| Читаемость | Зависит от сложности | Высокая |
| Производительность | Хорошая | Хорошая |
| Параллелизация | Сложно | parallelStream() |
| Лямбды | Нет | Да |
Итоги урока
- Stream API — конвейер для обработки коллекций в функциональном стиле
- Создаётся через stream() на коллекции или Arrays.stream()/Stream.of()
- Промежуточные операции (filter, map, sorted, limit, skip) возвращают новый Stream
- Терминальные операции (forEach, collect, count, reduce) запускают обработку
- Операции строятся в цепочки (pipeline) для чистого и компактного кода
- Collectors提供更多 способов сбора: toList, toSet, toMap, joining
- Ленивые вычисления — элементы обрабатываются только при вызове терминальной операции
- Stream не хранит данные — это поток обработки
- parallelStream() для параллельной обработки
- Stream API делает код чище, но не обязательно быстрее классических циклов
Тест по Stream API
5 вопросов