Stream.filter: примеры (JAVA)

Примеры работы filter с потоками
Раздел: Потоки данных (Stream API) - промежуточные операции
Stream.filter(Predicate predicate): Stream

Общее описание метода Stream.filter

Метод Stream.filter из пакета java.util.stream применяется для отсеивания элементов потока по булевому условию. Это промежуточная операция, которая получает в качестве аргумента предикат и возвращает новый Stream, содержащий только те элементы исходного потока, для которых предикат возвращает true. Операция ленивaя: выполнение предиката откладывается до вызова терминальной операции.

Сигнатуры (основные):

  • Stream filter(Predicate predicate) - стандартный объектный поток.
  • IntStream filter(IntPredicate predicate), LongStream filter(LongPredicate predicate), DoubleStream filter(DoublePredicate predicate) - примитивные варианты для экономии упаковки.

Аргументы:

  • predicate: реализация функционального интерфейса Predicate (для объектов) или соответствующего примитивного предиката. Не допускается null; передача null приводит к NullPointerException.

Возвращаемое значение:

  • Новый промежуточный Stream того же типа, что и исходный (или примитивный поток для соответствующих перегрузок). Возврат всегда ненулевой ссылки.

Особенности поведения:

  • Операция не изменяет исходный источник данных, она создает цепочку промежуточных операций.
  • Выполнение предиката происходит только при выполнении терминальной операции.
  • Порядок прохода сохраняется для упорядоченных потоков, если не используется операция, отменяющая порядок.
  • Не является по определению короткозамыкающей: сама по себе фильтрация не завершит работу раньше, чем требует терминальная операция; однако совокупность операций (например, с findFirst или anyMatch) может приводить к раннему завершению.
  • Использование изменяемого состояния внутри предиката опасно при параллельной обработке.

Короткие примеры применения

Фильтрация простого списка строк, оставление тех, что длиннее 3 символов, и вывод результата:

List list = List.of("one", "two", "three", "four");
List res = list.stream()
    .filter(s -> s.length() > 3)
    .collect(Collectors.toList());
System.out.println(res);
[three, four]

Использование примитивного потока IntStream:

int[] arr = {1, 2, 3, 4, 5, 6};
int sumEven = IntStream.of(arr)
    .filter(n -> n % 2 == 0)
    .sum();
System.out.println(sumEven);
12

Отсечение null значений:

List maybeNull = Arrays.asList("a", null, "b");
List noNulls = maybeNull.stream()
    .filter(Objects::nonNull)
    .collect(Collectors.toList());
System.out.println(noNulls);
[a, b]

Комбинация предикатов (and / negate):

List nums = List.of(1,2,3,4,5,6);
Predicate even = n -> n % 2 == 0;
Predicate gtThree = n -> n > 3;
List out = nums.stream()
    .filter(even.and(gtThree))
    .collect(Collectors.toList());
System.out.println(out);
[4, 6]

Похожие механизмы в Java

Внутри экосистемы Java существуют связанные по смыслу конструкции:

  • Collection.removeIf - удаляет элементы из изменяемой коллекции по предикату; полезно при необходимости изменить саму коллекцию, тогда как Stream.filter создает новый результат.
  • Optional.filter - фильтрация значения внутри Optional; возвращает пустой Optional при несоответствии.
  • Stream.map + flatMap - при необходимости преобразования элементов перед фильтрацией иногда применяется последовательность map и filter или flatMap.
  • Collectors.filtering (добавлен в коллекторах) - позволяет выполнять фильтрацию внутри агрегирующих операций, например, при группировке, чтобы избежать дополнительной промежуточной фильтрации.

Выбор зависит от задачи: для немодифицируемого конвейера потоков предпочтителен Stream.filter. Для прямого удаления из коллекции - removeIf. Для опционального значения - Optional.filter. Для фильтрации внутри группирующих операций - Collectors.filtering.

Аналоги в других языках и отличия

Краткие сопоставления с примерами.

  • JavaScript - Array.prototype.filter:
    const arr = [1,2,3,4];
    const ev = arr.filter(n => n % 2 === 0);
    console.log(ev);
    [2, 4]
    Отличия: метод массивов немедленно возвращает новый массив и не ленив, в отличие от Java Stream.
  • Python - встроенная filter и списковые включения:
    nums = [1,2,3,4]
    even = list(filter(lambda x: x%2==0, nums))
    print(even)
    # или
    print([x for x in nums if x%2==0])
    [2, 4]
    [2, 4]
    Отличия: filter возвращает итератор в Python 3; списковые включения более питоничны.
  • PHP - array_filter:
    $a = [1,2,3,4];
    $r = array_filter($a, fn($x) => $x % 2 == 0);
    print_r(array_values($r));
    Array
    (
        [0] => 2
        [1] => 4
    )
    
    Отличия: возвращает массив, ключи по умолчанию сохраняются.
  • SQL - оператор WHERE:
    SELECT id FROM table WHERE active = 1;
    -- результат набора строк, отфильтрованных на уровне СУБД
    Отличия: фильтрация выполняется базой данных, может быть оптимизирована планировщиком.
  • C# - LINQ: Where:
    var nums = new[]{1,2,3,4};
    var ev = nums.Where(n => n % 2 == 0).ToList();
    Console.WriteLine(string.Join(", ", ev));
    2, 4
    Отличия: LINQ использует ленивую семантику, похожую на Java Stream, но семантика исполнения и интеграция с языком отличаются.
  • Kotlin - filter расширение коллекций:
    val a = listOf(1,2,3,4)
    val ev = a.filter { it % 2 == 0 }
    println(ev)
    [2, 4]
    Отличия: для коллекций Kotlin фильтрация немедленная; для последовательностей (Sequence) - ленивa, как в Java.
  • Go - универсального встроенного аналога нет:
    a := []int{1,2,3,4}
    var out []int
    for _, v := range a {
        if v%2 == 0 { out = append(out, v) }
    }
    fmt.Println(out)
    [2 4]
    Отличия: требуется явная итерация или сторонние библиотеки; начиная с generics реализация компактнее, но по-прежнему императивна.
  • Lua - цикл через таблицу:
    local a = {1,2,3,4}
    local out = {}
    for _,v in ipairs(a) do
      if v % 2 == 0 then table.insert(out, v) end
    end
    for _,v in ipairs(out) do io.write(v, " ") end
    2 4 

Главные различия: Java Stream ленивый, поддерживает параллелизм, примитивные специализированные потоки для производительности; многие языки дают немедленный результат или требуют явной итерации.

Типичные ошибки и примеры

NullPredicate и NullPointerException:

List l = List.of("a","b");
Predicate p = null;
l.stream().filter(p).count();
Exception in thread "main" java.lang.NullPointerException
    at java.base/java.util.Objects.requireNonNull(Objects.java:234)
    at java.base/java.util.stream.ReferencePipeline.filter(ReferencePipeline.java:...)

Повторное использование потока (IllegalStateException):

Stream s = Stream.of("a","b");
Stream f = s.filter(x -> true);
f.count();
f.forEach(System.out::println);
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

Побочные эффекты в предикате при параллельной обработке (негарантированное поведение):

List src = IntStream.range(0,100).boxed().collect(Collectors.toList());
List collected = Collections.synchronizedList(new ArrayList<>());
src.parallelStream()
   .filter(n -> { collected.add(n); return n % 2 == 0; })
   .count();
// collected порядок и содержимое могут быть неожиданны
-- результат зависит от выполнения, возможны дубликаты и непредсказуемый порядок

Проверка условия, подразумевающего выброс checked-исключения. Predicates не поддерживают checked исключения напрямую:

List files = ...;
files.stream().filter(p -> Files.isRegularFile(p))... // OK
// но если требуется вызвать метод, который бросает IOException, нужно обернуть
// Реализация должна перехватывать или оборачивать checked исключения в RuntimeException

Изменения и эволюция

Метод Stream.filter введен в Java 8 и с тех пор сохраняет прежний API. В более поздних версиях платформы происходили дополнения, которые расширяют возможности работы с потоками, но не меняют сигнатуру filter:

  • Java 9: добавлены новые операции потоков (takeWhile, dropWhile) и коллектора Collectors.filtering, что в сочетании с filter позволяет выразительнее строить конвейеры.
  • Улучшения производительности и оптимизаций конвейера в JVM в последующих релизах, которые могут влиять на скорость выполнения цепочек со filter, особенно в параллельных потоках.

Сигнатуры примитивных перегрузок (IntStream.filter и т. п.) также остаются без изменений.

Расширенные и нестандартные примеры

Фильтрация по типу с приведением (используется Class::isInstance и map):

Пример java
List mixed = List.of(1, "two", 3L, "four");
List strings = mixed.stream()
    .filter(String.class::isInstance)
    .map(String.class::cast)
    .collect(Collectors.toList());
System.out.println(strings);
[two, four]

Использование Collectors.filtering вместе с groupingBy:

Пример java
List names = List.of("Ann", "Bob", "Alice", "Bill");
Map> m = names.stream()
    .collect(Collectors.groupingBy(s -> s.charAt(0),
        Collectors.filtering(s -> s.length() > 3, Collectors.toList())));
System.out.println(m);
{A=[Alice], B=[Bill]}

Сочетание с flatMap для фильтрации вложенных коллекций:

Пример java
List> matrix = List.of(List.of(1,2), List.of(3,4));
List ev = matrix.stream()
    .flatMap(Collection::stream)
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
System.out.println(ev);
[2, 4]

Фильтрация с безопасной обработкой checked-исключений (обертка в функцию):

Пример java
static Predicate safeIsRegularFile() {
    return p -> {
        try {
            return Files.isRegularFile(p);
        } catch (Exception e) {
            throw new UncheckedIOException(new IOException(e));
        }
    };
}
// использование
// paths.stream().filter(safeIsRegularFile()).forEach(...);
-- эквивалентно: выброс Runtime-обертки вместо checked исключения

Параллельный поток и сохранение порядка при фильтрации (пример контролируемой параллельности):

Пример java
List nums = IntStream.range(0,1000).boxed().collect(Collectors.toList());
List res = nums.parallelStream()
    .filter(n -> n % 3 == 0)
    .sorted() // сортировка восстанавливает порядок
    .collect(Collectors.toList());
System.out.println(res.subList(0,5));
[0, 3, 6, 9, 12]

Фильтрация с запоминанием предыдущего элемента в потоке (stateful filter - редко рекомендуется):

Пример java
AtomicInteger last = new AtomicInteger(Integer.MIN_VALUE);
List seq = List.of(1,1,2,2,3,1,1);
List compact = seq.stream()
    .filter(i -> {
        int prev = last.getAndSet(i);
        return prev != i;
    })
    .collect(Collectors.toList());
System.out.println(compact);
[1, 2, 3, 1]

Комментарий: stateful-предикаты ломают параллельность и делают код менее прозрачным; применять осмотрительно.

джава Stream.filter function comments

En
Stream.filter Returns a stream consisting of the elements that match the given predicate