$ sudo teach IT
МОДУЛЬ 10 · УРОК 5

Stream API

Осваиваем Stream API — мощный инструмент для обработки коллекций. Фильтрация, преобразование, сортировка и агрегация данных в один цепочки вызовов

~35 минут Для новичков Java

Что такое 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 вопросов

Stream API

Premium