← Назад к вопросам

Какие знаешь характеристики стрима в Java?

2.3 Middle🔥 81 комментариев
#Stream API и функциональное программирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Характеристики Stream API в Java: Полный обзор

Stream API (введён в Java 8) — это функциональный подход к обработке последовательностей данных. Потоки (streams) предоставляют декларативный способ работы с коллекциями вместо императивного кода с циклами.

Основные характеристики Stream

1. Функциональный подход (Declarative)

Вместо написания явных циклов:

// Императивный подход (старый способ)
List<String> names = new ArrayList<>();
for (Person person : people) {
    if (person.getAge() > 18) {
        names.add(person.getName());
    }
}
Collections.sort(names);

// Функциональный подход (Stream)
List<String> names = people.stream()
    .filter(p -> p.getAge() > 18)
    .map(Person::getName)
    .sorted()
    .collect(Collectors.toList());

Плюсы:

  • Более читаемо
  • Выражаешь ЧТО хочешь, а не КАК это делать
  • Легче параллелизировать

2. Ленивость (Lazy Evaluation)

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

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// map() не выполняется сразу!
Stream<Integer> doubled = numbers.stream()
    .map(n -> {
        System.out.println("Mapping: " + n);  // Не выпечатается
        return n * 2;
    });

// Только сейчас выполняются все операции
List<Integer> result = doubled.collect(Collectors.toList());
// Теперь выпечатает "Mapping: 1", "Mapping: 2", и т.д.

Преимущество ленивости:

  • Пропускает лишние вычисления
  • Может прерваться раньше
// Вычисляет только 3 элемента, хотя список больше
List<Integer> first3 = numbers.stream()
    .filter(n -> n > 1)
    .limit(3)
    .collect(Collectors.toList());

3. Неизменяемость (Immutability)

Stream НЕ модифицирует исходную коллекцию:

List<Integer> original = Arrays.asList(1, 2, 3, 4, 5);

List<Integer> result = original.stream()
    .filter(n -> n > 2)
    .collect(Collectors.toList());

System.out.println(original);  // [1, 2, 3, 4, 5] — не изменился!
System.out.println(result);    // [3, 4, 5] — новый список

4. Не переиспользуется (One-time Use)

Stream можно использовать только один раз:

Stream<Integer> stream = numbers.stream();

List<Integer> list1 = stream.collect(Collectors.toList());
List<Integer> list2 = stream.collect(Collectors.toList());  // ❌ IllegalStateException!

Правильно:

List<Integer> list1 = numbers.stream().collect(Collectors.toList());
List<Integer> list2 = numbers.stream().collect(Collectors.toList());  // OK

5. Параллелизм (Parallelism)

Stream легко распараллелить:

// Последовательный
List<Integer> result = numbers.stream()
    .map(n -> expensiveOperation(n))
    .collect(Collectors.toList());

// Параллельный (несколько потоков)
List<Integer> result = numbers.parallelStream()
    .map(n -> expensiveOperation(n))
    .collect(Collectors.toList());

Осторожность с parallelStream():

  • Overhead на создание потоков может быть больше выигрыша
  • Не потокобезопасные операции в map() станут проблемой
  • Хорошо работает с большими коллекциями и дорогими операциями

Типы операций

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

Возвращают Stream, могут быть chained:

// Трансформация
.map(x -> x * 2)              // преобразование
.flatMap(x -> Arrays.stream(x))  // раскладка

// Фильтрация
.filter(x -> x > 10)          // условие
.distinct()                    // удалить дубликаты

// Ограничение
.limit(10)                     // первые N элементов
.skip(5)                       // пропустить первые N

// Сортировка
.sorted()                      // естественный порядок
.sorted(Comparator.comparing(...))  // кастомный компаратор

// Другие
.peek(System.out::println)    // side effect для отладки
.takeWhile(x -> x < 100)      // пока условие true (Java 9+)
.dropWhile(x -> x < 100)      // пока условие true (Java 9+)

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

Возвращают конкретный результат, не Stream:

// Сбор результата
.collect(Collectors.toList())      // в List
.collect(Collectors.toSet())       // в Set
.collect(Collectors.toMap(...))    // в Map
.collect(Collectors.joining(", "))  // в String

// Агрегирование
.reduce((a, b) -> a + b)          // свертка
.sum()                             // сумма (только IntStream)
.average()                         // среднее
.count()                           // количество
.min()                             // минимум
.max()                             // максимум

// Поиск
.findFirst()                       // первый элемент
.findAny()                         // любой элемент
.anyMatch(x -> x > 10)            // есть ли совпадение?
.allMatch(x -> x > 0)             // все ли совпадают?
.noneMatch(x -> x < 0)            // нет ли совпадений?

// Обход
.forEach(System.out::println)      // выполнить для каждого
.forEachOrdered(...)              // гарантированный порядок

Типы Stream

// Основной Stream<T> для объектов
Stream<String> stream = list.stream();

// Примитивные потоки (оптимизированы)
IntStream intStream = numbers.stream().mapToInt(Integer::intValue);
LongStream longStream = numbers.stream().mapToLong(Long::valueOf);
DoubleStream doubleStream = numbers.stream().mapToDouble(Double::valueOf);

// Примитивные потоки имеют специальные методы
intStream.sum();
intStream.average();
intStream.statistics();  // min, max, sum, average, count

Практические примеры

// Группировка
Map<String, List<Person>> byDepartment = people.stream()
    .collect(Collectors.groupingBy(Person::getDepartment));

// Статистика
IntSummaryStatistics stats = numbers.stream()
    .mapToInt(Integer::intValue)
    .summaryStatistics();  // min, max, sum, average, count

// Partition (разбиение на две группы)
Map<Boolean, List<Person>> adults = people.stream()
    .collect(Collectors.partitioningBy(p -> p.getAge() >= 18));
List<Person> adultList = adults.get(true);
List<Person> childrenList = adults.get(false);

// Композитные фильтры
List<String> result = people.stream()
    .filter(p -> p.getAge() > 18)
    .filter(p -> p.getDepartment().equals("IT"))
    .map(Person::getName)
    .sorted()
    .collect(Collectors.toList());

// flatMap для раскладки
List<List<String>> lists = Arrays.asList(
    Arrays.asList("a", "b"),
    Arrays.asList("c", "d")
);
List<String> flat = lists.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());  // [a, b, c, d]

// Reduce
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b);  // начальное значение = 0

Optional<Integer> max = numbers.stream()
    .reduce((a, b) -> a > b ? a : b);

Optional: Работа с пустыми потоками

Optional<String> first = list.stream()
    .filter(s -> s.startsWith("A"))
    .findFirst();

if (first.isPresent()) {
    System.out.println(first.get());
}

// Более современный подход
first.ifPresentOrElse(
    System.out::println,
    () -> System.out.println("Not found")
);

// Или просто
String result = first.orElse("default");

Performance Considerations

// ❌ Неэффективно: создание Stream в цикле
for (int i = 0; i < 1000; i++) {
    list.stream().filter(...).collect(...);
}

// ✅ Эффективно: один Stream
list.stream().filter(...).collect(...);

// ❌ Параллелизм не всегда быстрее
List<Integer> huge = generateMillionNumbers();
List<Integer> result = huge.parallelStream()  // Overhead!
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());

// ✅ Параллелизм хорошо работает с большими коллекциями и дорогими операциями
List<Image> images = loadThousandsOfImages();
List<BufferedImage> processed = images.parallelStream()
    .map(img -> applyExpensiveFilter(img))  // дорогая операция
    .collect(Collectors.toList());

Pitfalls (Ловушки)

// ❌ Переиспользование Stream
Stream<Integer> stream = list.stream();
Stream<Integer> doubled = stream.map(n -> n * 2);  // OK
List<Integer> result1 = doubled.collect(Collectors.toList());  // OK
List<Integer> result2 = doubled.collect(Collectors.toList());  // ❌ IllegalStateException

// ❌ Side effects в map()
list.stream()
    .map(n -> {
        System.out.println(n);  // Плохо! Сложно отладить
        return n * 2;
    })
    .collect(Collectors.toList());

// ✅ Правильно: использовать peek() для отладки
list.stream()
    .peek(System.out::println)
    .map(n -> n * 2)
    .collect(Collectors.toList());

// ❌ Null в Stream
List<String> list = Arrays.asList("a", null, "c");
list.stream()
    .map(String::toUpperCase)  // NullPointerException на null!
    .collect(Collectors.toList());

// ✅ Фильтруй null
list.stream()
    .filter(Objects::nonNull)
    .map(String::toUpperCase)
    .collect(Collectors.toList());

Заключение

Stream API предоставляет мощный и выразительный способ работы с коллекциями. Ключевые характеристики:

  • Функциональный стиль — декларативность
  • Ленивость — эффективность
  • Неизменяемость — безопасность
  • Одноразовое использование — помнить нужно
  • Параллелизм — для тяжёлых операций
  • Богатый API — множество операций из коробки

Stream API — стандарт для работы с коллекциями в современной Java.

Какие знаешь характеристики стрима в Java? | PrepBro