Операции над потоками
У потоков десятки операций, но в повседневном коде чаще всего встречается десяток. В этом уроке разберём их по группам: отбор, преобразование, порядок и получение результата.
filter и map
filter оставляет элементы, для которых лямбда вернула true. map превращает каждый элемент в другой — например, строку в её длину:
void main() {
List<String> words = List.of("кот", "собака", "ёж", "попугай");
List<Integer> lengths = words.stream()
.map(w -> w.length())
.toList();
IO.println(lengths);
}Результат:
[3, 6, 2, 7]Тип элементов после map может поменяться: был поток строк, стал поток чисел. Метод toList() собирает результат в неизменяемый список.
Порядок и уникальность
sorted() сортирует элементы, distinct() убирает повторы, limit(n) оставляет первые n элементов, а skip(n) пропускает их:
void main() {
List<Integer> scores = List.of(7, 3, 9, 3, 10, 7, 5);
List<Integer> top = scores.stream()
.distinct()
.sorted(Comparator.reverseOrder())
.limit(3)
.toList();
IO.println(top);
IO.println(scores.stream().skip(5).toList());
}Результат:
[10, 9, 7]
[7, 5]sorted без аргументов сортирует по возрастанию, а с компаратором — как скажет компаратор. Компараторы подробно разобраны в главе о коллекциях.
Проверки и поиск
anyMatch, allMatch и noneMatch отвечают на вопрос «есть ли / все ли / нет ли». findFirst() возвращает первый элемент, завёрнутый в Optional, потому что подходящего элемента может и не быть:
void main() {
List<Integer> ages = List.of(15, 22, 17, 30);
IO.println(ages.stream().anyMatch(a -> a >= 18));
IO.println(ages.stream().allMatch(a -> a >= 18));
Optional<Integer> firstAdult = ages.stream()
.filter(a -> a >= 18)
.findFirst();
IO.println(firstAdult.orElse(0));
}Результат:
true
false
22Числовые потоки
Для чисел есть особые потоки IntStream, LongStream и DoubleStream. Перейти к ним помогает mapToInt, после чего доступны sum(), average(), max():
record Item(String name, int price) {}
void main() {
List<Item> cart = List.of(new Item("Хлеб", 3), new Item("Сыр", 12), new Item("Чай", 5));
int total = cart.stream().mapToInt(Item::price).sum();
double avg = cart.stream().mapToInt(Item::price).average().orElse(0);
IO.println("Итого: $" + total);
IO.println("Средняя цена: $" + avg);
}Результат:
Итого: $20
Средняя цена: $6.666666666666667Item::price — ссылка на метод, короткая запись лямбды i -> i.price(). average() возвращает OptionalDouble: у пустого потока среднего нет. Подробнее числовые потоки и flatMap разобраны в уроке flatMap и числовые потоки.
reduce
reduce сворачивает все элементы в одно значение. Ему передают начальное значение и функцию, которая объединяет накопленный результат со следующим элементом:
void main() {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int product = numbers.stream().reduce(1, (acc, n) -> acc * n);
IO.println(product);
}Результат:
120Для сумм, минимумов и максимумов удобнее готовые sum(), min(), max() — reduce нужен, когда готового метода нет.
Частая ошибка
Внутри лямбды нельзя менять обычную локальную переменную — компилятор это запрещает:
void main() {
int total = 0;
List.of(1, 2, 3).stream().forEach(n -> total += n);
IO.println(total);
}Результат:
Main.java:3: error: local variables referenced from a lambda expression must be final or effectively final
List.of(1, 2, 3).stream().forEach(n -> total += n);
^
1 error
error: compilation failedЛокальная переменная, используемая в лямбде, должна быть «фактически финальной» — не меняться после присваивания. Вместо накопления во внешней переменной верните результат из потока: int total = list.stream().mapToInt(n -> n).sum();.