Статьи

Полный курс по языку Java — от установки JDK до многопоточности и HTTP-запросов

Лямбды и Stream API

Страница 22 из 30 · 2026-10-25 · 6 мин чтения

Лямбда-выражения

Лямбда — краткая запись реализации функционального интерфейса, то есть интерфейса с одним абстрактным методом.

import java.util.ArrayList;import java.util.Collections;import java.util.List;public class LambdaBasics {    public static void main(String[] args) {        List<String> words = new ArrayList<>(List.of("слива", "яблоко", "киви"));        Collections.sort(words, (a, b) -> a.length() - b.length());        System.out.println(words);        Runnable task = () -> System.out.println("Задача выполнена");        task.run();    }}

Формы записи:

(a, b) -> a + bx -> x * x() -> 42(a, b) -> {    int sum = a + b;    return sum * 2;}

Лямбда может использовать локальные переменные из внешнего кода только если они final или фактически неизменяемые.

Стандартные функциональные интерфейсы

Пакет java.util.function:

ИнтерфейсМетодНазначение
Function<T, R>R apply(T)преобразование
Predicate<T>boolean test(T)проверка условия
Consumer<T>void accept(T)действие над значением
Supplier<T>T get()поставщик значения
BiFunction<T, U, R>R apply(T, U)функция двух аргументов
UnaryOperator<T>T apply(T)преобразование в тот же тип
BinaryOperator<T>T apply(T, T)свертка двух значений
import java.util.function.Consumer;import java.util.function.Function;import java.util.function.Predicate;import java.util.function.Supplier;public class FunctionalDemo {    public static void main(String[] args) {        Function<String, Integer> length = String::length;        Predicate<Integer> isEven = n -> n % 2 == 0;        Consumer<String> printer = s -> System.out.println("> " + s);        Supplier<Double> random = Math::random;        System.out.println(length.apply("привет"));        System.out.println(isEven.test(4));        printer.accept("текст");        System.out.println(random.get() < 1);        Function<Integer, Integer> doubled = x -> x * 2;        Function<Integer, Integer> plusOne = x -> x + 1;        System.out.println(doubled.andThen(plusOne).apply(5));        System.out.println(doubled.compose(plusOne).apply(5));        Predicate<String> notEmpty = s -> !s.isEmpty();        Predicate<String> shortWord = s -> s.length() < 5;        System.out.println(notEmpty.and(shortWord).test("abc"));        System.out.println(notEmpty.negate().test(""));    }}

Ссылки на методы

ВидЗаписьЭквивалент
статический методInteger::parseInts -> Integer.parseInt(s)
метод объектаSystem.out::printlnx -> System.out.println(x)
метод типаString::toUpperCases -> s.toUpperCase()
конструкторArrayList::new() -> new ArrayList<>()

Optional

Контейнер для значения, которого может не быть:

import java.util.List;import java.util.Optional;public class OptionalDemo {    static Optional<String> findFirstLong(List<String> words) {        return words.stream().filter(w -> w.length() > 5).findFirst();    }    public static void main(String[] args) {        Optional<String> result = findFirstLong(List.of("кот", "слон", "крокодил"));        System.out.println(result.isPresent());        System.out.println(result.get());        System.out.println(result.map(String::toUpperCase).orElse("нет"));        result.ifPresent(w -> System.out.println("Найдено: " + w));        Optional<String> none = Optional.empty();        System.out.println(none.orElse("значение по умолчанию"));        System.out.println(none.orElseGet(() -> "вычисленное значение"));    }}

get() без проверки бросит NoSuchElementException. Предпочитайте orElse, orElseGet, orElseThrow, ifPresent.

Stream API

Поток описывает цепочку операций над данными: создание, промежуточные операции, терминальная операция. Поток не хранит данные и не изменяет источник.

import java.util.List;public class StreamBasics {    public static void main(String[] args) {        List<Integer> numbers = List.of(5, 2, 8, 1, 9, 3, 6);        List<Integer> result = numbers.stream()            .filter(n -> n > 2)            .map(n -> n * n)            .sorted()            .toList();        System.out.println(result);    }}

Вывод: [9, 25, 36, 64, 81].

Промежуточные операции выполняются лениво и только при вызове терминальной.

Создание потоков

import java.util.Arrays;import java.util.stream.IntStream;import java.util.stream.Stream;public class StreamCreate {    public static void main(String[] args) {        Stream<String> a = Stream.of("x", "y", "z");        Stream<Integer> b = Arrays.asList(1, 2, 3).stream();        IntStream c = IntStream.range(0, 5);        IntStream d = IntStream.rangeClosed(1, 5);        Stream<Integer> e = Stream.iterate(1, x -> x * 2).limit(8);        IntStream f = Arrays.stream(new int[]{1, 2, 3});        System.out.println(e.toList());        System.out.println(d.sum());    }}

Промежуточные операции

ОперацияДействие
filter(Predicate)оставить подходящие
map(Function)преобразовать каждый элемент
flatMap(Function)преобразовать и «сплющить» вложенные потоки
distinct()убрать дубликаты
sorted() / sorted(Comparator)отсортировать
limit(n) / skip(n)взять первые / пропустить первые
peek(Consumer)подсмотреть значение, для отладки

Терминальные операции

ОперацияРезультат
forEach(Consumer)выполнить действие
collect(Collector)собрать в коллекцию
toList()неизменяемый список
count()количество
reduce(...)свертка
anyMatch, allMatch, noneMatchпроверка условий
findFirst, findAnyOptional с элементом
min, maxминимум и максимум по Comparator
import java.util.List;public class StreamTerminal {    public static void main(String[] args) {        List<Integer> numbers = List.of(4, 7, 1, 9, 3);        System.out.println(numbers.stream().count());        System.out.println(numbers.stream().anyMatch(n -> n > 8));        System.out.println(numbers.stream().allMatch(n -> n > 0));        System.out.println(numbers.stream().reduce(0, Integer::sum));        System.out.println(numbers.stream().max(Integer::compare).get());        System.out.println(numbers.stream().mapToInt(Integer::intValue).average().orElse(0));    }}

Примитивные потоки

IntStream, LongStream, DoubleStream избегают упаковки и имеют методы sum, average, min, max, summaryStatistics:

import java.util.IntSummaryStatistics;import java.util.stream.IntStream;public class IntStreamDemo {    public static void main(String[] args) {        int sum = IntStream.rangeClosed(1, 100).sum();        System.out.println(sum);        IntSummaryStatistics stats = IntStream.of(4, 8, 15, 16, 23, 42).summaryStatistics();        System.out.println(stats.getMin() + " " + stats.getMax() + " " + stats.getAverage());    }}

Преобразования: mapToInt, mapToObj, boxed.

Collectors

import java.util.List;import java.util.Map;import java.util.Set;import java.util.stream.Collectors;public class CollectorsDemo {    record Employee(String name, String department, double salary) {    }    public static void main(String[] args) {        List<Employee> staff = List.of(            new Employee("Анна", "IT", 3000),            new Employee("Борис", "IT", 3500),            new Employee("Вера", "HR", 2500),            new Employee("Глеб", "HR", 2700),            new Employee("Дина", "Sales", 2800)        );        List<String> names = staff.stream()            .map(Employee::name)            .collect(Collectors.toList());        System.out.println(names);        Set<String> departments = staff.stream()            .map(Employee::department)            .collect(Collectors.toSet());        System.out.println(departments);        String joined = staff.stream()            .map(Employee::name)            .collect(Collectors.joining(", ", "[", "]"));        System.out.println(joined);        Map<String, List<Employee>> byDepartment = staff.stream()            .collect(Collectors.groupingBy(Employee::department));        System.out.println(byDepartment.keySet());        Map<String, Long> counts = staff.stream()            .collect(Collectors.groupingBy(Employee::department, Collectors.counting()));        System.out.println(counts);        Map<String, Double> avgSalary = staff.stream()            .collect(Collectors.groupingBy(Employee::department, Collectors.averagingDouble(Employee::salary)));        System.out.println(avgSalary);        Map<Boolean, List<String>> partition = staff.stream()            .collect(Collectors.partitioningBy(                e -> e.salary() >= 2800,                Collectors.mapping(Employee::name, Collectors.toList())            ));        System.out.println(partition);        Map<String, Double> nameToSalary = staff.stream()            .collect(Collectors.toMap(Employee::name, Employee::salary));        System.out.println(nameToSalary);    }}

toMap бросит исключение при дублировании ключей. В этом случае передайте функцию слияния третьим аргументом.

flatMap

import java.util.Arrays;import java.util.List;import java.util.stream.Collectors;public class FlatMapDemo {    public static void main(String[] args) {        List<String> lines = List.of("раз два", "три четыре пять");        List<String> words = lines.stream()            .flatMap(line -> Arrays.stream(line.split(" ")))            .collect(Collectors.toList());        System.out.println(words);    }}

Параллельные потоки

import java.util.stream.IntStream;public class ParallelDemo {    public static void main(String[] args) {        long sum = IntStream.rangeClosed(1, 1_000_000)            .parallel()            .asLongStream()            .sum();        System.out.println(sum);    }}

Параллельный поток дает выигрыш только на больших объемах данных и независимых операциях. Не используйте его с побочными эффектами и общим изменяемым состоянием.

Правила хорошего стиля

  • Не изменяйте внешние переменные внутри лямбд потока.
  • Не используйте поток один раз повторно: после терминальной операции он закрыт.
  • Если цепочка получилась слишком сложной, разбейте ее на методы с понятными именами.
  • Для простых циклов обычный for часто читаемее.