1. Создаём поток
Чтобы использовать Stream API, нужно сначала получить поток из какой-то коллекции или массива.
Примеры создания потока
// Из списка
List<String> names = List.of("Anna", "Boris", "Alex", "Alina");
Stream<String> stream = names.stream();
// Из массива
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);
// Из отдельных значений
Stream<String> letters = Stream.of("A", "B", "C");
Коротко:
- list.stream() — для коллекций
- Arrays.stream(array) — для массивов
- Stream.of(...) — для отдельных значений
Пример в контексте нашего приложения
Допустим, у нас есть список пользователей:
List<String> users = List.of("Ivan", "Anna", "Petr", "Alexey");
Stream<String> userStream = users.stream();
Промежуточные и терминальные операции
Важный момент: операции в Stream API делятся на два типа.
- Промежуточные операции (например, filter, map, distinct) — описывают этапы обработки. Они возвращают новый поток, но сами по себе ничего не запускают.
- Терминальные операции (например, collect, forEach, count) — запускают конвейер и выдают результат.
Поток работает «лениво»: пока не вызвана терминальная операция, никаких вычислений не произойдёт. Именно поэтому мы часто заканчиваем цепочку collect(...) — это и есть точка, где поток превращается обратно в коллекцию или другой результат.
2. Операция filter: фильтруем элементы по условию
filter — это промежуточная операция, которая пропускает только те элементы, которые соответствуют заданному условию.
Сигнатура
Stream<T> filter(Predicate<? super T> predicate);
Predicate — это функциональный интерфейс, принимающий элемент и возвращающий true (оставить) или false (отсеять).
Пример: оставить только чётные числа
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // [2, 4, 6]
Что происходит?
- n -> n % 2 == 0 — лямбда-выражение, проверяет, делится ли число на 2 без остатка.
- filter оставляет только чётные числа.
Пример: фильтруем имена, начинающиеся с "A"
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");
List<String> aNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(aNames); // [Anna, Alex, Alina]
Важный момент: filter не меняет коллекцию — он создаёт новый поток, в котором есть только нужные элементы.
3. Операция map: превращаем элемент в нечто другое
map — это операция преобразования. Она берёт каждый элемент потока, применяет к нему функцию и возвращает новый элемент.
Сигнатура
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Function — это интерфейс, принимающий элемент и возвращающий что-то (может быть другого типа).
Пример: получить длины строк
List<String> names = List.of("Anna", "Boris", "Alex");
List<Integer> nameLengths = names.stream()
.map(name -> name.length())
.collect(Collectors.toList());
System.out.println(nameLengths); // [4, 5, 4]
Что происходит?
- map превращает строку в её длину (name -> name.length()).
- В результате получается поток чисел.
Пример: привести строки к верхнему регистру
List<String> names = List.of("Anna", "Boris", "Alex");
List<String> upperNames = names.stream()
.map(name -> name.toUpperCase())
.collect(Collectors.toList());
System.out.println(upperNames); // [ANNA, BORIS, ALEX]
4. Операция collect: собираем результат обратно в коллекцию
collect — это терминальная операция, то есть она завершает работу потока и собирает результат в коллекцию или другой контейнер.
Сигнатура
<R, A> R collect(Collector<? super T, A, R> collector)
Не пугайтесь страшной сигнатуры! В 99% случаев вы используете готовые коллекторы из класса Collectors.
Collectors — это утилитный класс с набором «сборщиков». Он говорит потоку, в какую форму собрать результат: список, множество, строку и т. д.
Примеры:
- Collectors.toList() — в List
- Collectors.toSet() — в Set
- Collectors.joining(", ") — в строку через запятую
То есть Collectors — это как набор коробок разной формы, в которые вы упаковываете элементы потока.
Пример: собрать результат в List
List<String> filtered = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
Пример: собрать результат в Set
Set<String> uniqueNames = names.stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());
Пример: собрать строки через запятую
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result); // Anna, Boris, Alex
5. Цепочка операций: фильтрация + преобразование + сбор результата
Самая большая сила Stream API — это возможность цеплять операции друг за другом.
Пример: получить длины имён, начинающихся с "A"
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");
List<Integer> aNameLengths = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::length)
.collect(Collectors.toList());
System.out.println(aNameLengths); // [4, 4, 5]
Пошагово:
- .stream() — создаём поток из списка.
- .filter(name -> name.startsWith("A")) — оставляем только имена, начинающиеся на "A".
- .map(String::length) — превращаем каждое имя в его длину.
- .collect(Collectors.toList()) — собираем результат в список.
Аналогичный императивный код
Вот как бы выглядело то же самое "по-старинке":
List<Integer> result = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
result.add(name.length());
}
}
Сравните: Stream API — одна строка, читается как «что делаем», а не «как делаем».
6. Практика: несколько коротких задач
Потренируемся! Все примеры можно запускать в одном файле — просто меняйте данные.
Задача 1: Оставить только нечётные числа и возвести их в квадрат
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);
List<Integer> oddSquares = numbers.stream()
.filter(n -> n % 2 != 0)
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(oddSquares); // [1, 9, 25, 49]
Задача 2: Из списка строк получить список их первых букв
List<String> names = List.of("Anna", "Boris", "Alex");
List<Character> initials = names.stream()
.map(name -> name.charAt(0))
.collect(Collectors.toList());
System.out.println(initials); // [A, B, A]
Задача 3: Отфильтровать строки длиной больше 3 и собрать их в Set
List<String> words = List.of("cat", "dog", "elephant", "ant", "bear");
Set<String> longWords = words.stream()
.filter(word -> word.length() > 3)
.collect(Collectors.toSet());
System.out.println(longWords); // [bear, elephant]
7. Типичные ошибки при работе с filter, map, collect
Ошибка № 1: забыли collect — результата нет!
Stream API ленивый, как кот на подоконнике: пока не вызовешь терминальную операцию (например, collect или forEach), ничего не произойдёт. Если написать только users.stream().filter(...).map(...); — это не выполнит никаких действий.
Ошибка № 2: filter и map перепутаны местами
Иногда новички сначала делают map, а потом filter. Например, names.stream().map(String::length).filter(len -> len > 3) — это даст числа, а не строки. Если вам нужны строки длиной больше 3, сначала фильтруйте, потом преобразовывайте.
Ошибка № 3: забыли про неизменяемость
Операции Stream API не меняют исходную коллекцию! Они возвращают новый результат. После List<String> upper = names.stream().map(String::toUpperCase).collect(Collectors.toList()); — коллекция names останется прежней.
Ошибка № 4: попытка использовать внешний изменяемый список
Не стоит делать так:
List<String> result = new ArrayList<>();
names.stream().filter(...).forEach(name -> result.add(name));
Лучше использовать collect — это безопаснее и короче.
Ошибка № 5: NullPointerException
Если в коллекции могут быть null-элементы, то вызов name.startsWith("A") на null даст ошибку. Добавляйте фильтр на null, если это возможно:
.filter(name -> name != null && name.startsWith("A"))
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ