Java Stream.filter 全面解析:定义、原理与最常见使用场景

Java Stream.filter 全面解析:定义、原理与最常见使用场景

filter 是 Java Stream 最常用、也最容易理解错的操作之一。它看似简单------"过滤元素"------但底层机制、函数式定义、实际使用场景都值得深入理解。

本篇文章将从 函数定义 → 底层原理 → 使用场景 → 注意事项 ,系统地讲透 filter


一、filter 的函数定义是什么?

Java Stream 中的 filter 定义如下:

swift 复制代码
Stream<T> filter(Predicate<? super T> predicate)

把它拆开解释:

部分 含义
Stream<T> 输入是 T 类型流,输出仍然是 T 类型流
filter(...) 执行过滤操作
Predicate<? super T> 一个返回 boolean 的判断函数

✔ filter 的本质:保留满足条件的元素,丢掉不满足的。

Predicate 是一个函数接口:

arduino 复制代码
boolean test(T t);

你只需要告诉 filter

这个元素要不要留下?

  • 返回 true:保留
  • 返回 false:丢弃

二、filter 的底层执行原理

要真正理解 filter 的行为,需要理解 Stream 的流水线(Pipeline)模型惰性求值(Lazy Evaluation)

下面我把整个底层机制按"从源码角度也能看懂"的方式讲清楚。


1. Stream 是一条流水线(pipeline)

当你写:

python 复制代码
stream.filter(x -> x > 10).map(x -> x * 2).sorted()

其实底层不是立刻执行,而是构建出一条"操作链":

css 复制代码
StreamSource → Filter → Map → Sorted → TerminalOp

每个操作都只是往链上"挂一个步骤",没有真正处理任何数据


2. filter 不会"过滤掉元素",它只是包装一个 Predicate

底层并不创建新集合,也不删除原集合元素。

它做的是:

把你的 Predicate 包装成一个用于判断的处理器(Sink)放入流水线中。

底层结构可以简化理解为:

scss 复制代码
if (predicate.test(element))
    pushToNextStep(element)
else
    drop element

每个元素会沿着流水线向下传递,直到某个步骤决定丢弃它。

filter 就是这样的"拦截器(Interceptor)"。


3. 真正的执行是在终止操作(Terminal Operation)触发的

例如:

scss 复制代码
stream.filter(...).map(...).toList();

toList() 被调用时,Stream 才开始:

  1. 从数据源(List、数组......)逐个取元素
  2. 把元素交给 filter 判断
  3. 满足条件则传递到 map
  4. 再传递给最终的收集操作(collector)

4. 底层执行顺序是"逐个元素完整走链路",而不是"一个操作处理完再下一个"

例如:

scss 复制代码
List.of(1,2,3,4)
    .stream()
    .filter(x -> x % 2 == 0)
    .map(x -> x * 10)
    .toList();

执行流程不是:

arduino 复制代码
先过滤所有 → 再 map 所有

而是:

✔ 实际执行方式(逐个流动):

scss 复制代码
1 → filter(丢弃)
2 → filter(通过) → map → 收集
3 → filter(丢弃)
4 → filter(通过) → map → 收集

每个元素都是从头到尾完整走一遍流水线

这就是为什么 Stream 只需要"一次遍历",效率高的原因。


5. filter 在多步骤流水线中的位置非常关键

它越靠前,性能越高。

例如:

scss 复制代码
stream.filter(...)   // 过滤掉大量不需要的数据
      .map(...)      // 只对少量数据做 map
      .sorted();     // 排序的数据更少

如果你把 filter 放在后面:

scss 复制代码
stream.map(...)
      .sorted(...)   // 排序所有数据(成本高)
      .filter(...);

性能会显著下降,因为 map、sorted 都处理了大量本该被过滤掉的元素。


6. 底层真正调用的是一个叫 Sink 的组件

Java 会为每个中间操作创建一个 Sink,比如:

  • FilterSink
  • MapSink
  • SortedSink

FilterSink 的核心代码类似:

scss 复制代码
public void accept(T element) {
    if (predicate.test(element)) {
        downstream.accept(element);
    }
}

解释:

  • predicate = 你的过滤函数
  • downstream = 下一个操作(map、sorted、collect...)

只要 predicate 为 true,就把元素推给下一个 Sink。否则丢弃。


简单一句话总结底层执行原理:

filter 并不会修改数据,它是把 predicate 包装成一个"判断关卡",元素在流水线经过这个关卡时被决定"留下还是丢弃"。真正执行在终止操作触发时逐元素进行。


三、filter 的典型使用方式

下面是实际开发中最常见、最有代表性的场景。

1. 过滤年龄大于 18 岁用户

ini 复制代码
List<User> adults = users.stream()
        .filter(u -> u.getAge() > 18)
        .toList();

2. 过滤非空字符串

ini 复制代码
List<String> list = strs.stream()
        .filter(s -> s != null && !s.isEmpty())
        .toList();

常见写法:

vbscript 复制代码
.filter(Objects::nonNull)
.filter(s -> !s.isEmpty())

3. 过滤偶数

ini 复制代码
List<Integer> evens = nums.stream()
        .filter(n -> n % 2 == 0)
        .toList();

4. 多条件过滤(业务常见)

scss 复制代码
List<Order> validOrders = orders.stream()
        .filter(o -> o.getStatus().equals("PAID"))
        .filter(o -> o.getAmount().compareTo(BigDecimal.ZERO) > 0)
        .filter(o -> o.getCustomerName() != null)
        .toList();

多条件拆成多行,可读性更强。


5. 过滤 null 字段(实体属性过滤)

ini 复制代码
List<User> withPhone = users.stream()
        .filter(u -> u.getPhone() != null)
        .toList();

6. 按关键字过滤字符串

ini 复制代码
List<String> result = list.stream()
        .filter(s -> s.contains("Java"))
        .toList();

7. 去空、去重后的过滤

css 复制代码
List<String> cleaned = list.stream()
        .filter(Objects::nonNull)
        .filter(s -> !s.isBlank())
        .distinct()
        .toList();

8. 过滤以大写字母开头的字符串

ini 复制代码
List<String> upper = list.stream()
        .filter(s -> Character.isUpperCase(s.charAt(0)))
        .toList();

9. 查询参数合法性过滤

常用于后端搜索条件校验:

css 复制代码
.filter(p -> p.getQuery() != null)
.filter(p -> !p.getQuery().isBlank())
.filter(p -> p.getPageSize() > 0)

四、filter 的注意事项(容易踩的坑)

❌ 1. filter 内不要写副作用(修改外部状态)

csharp 复制代码
.filter(u -> {
    list.add(u); // 副作用,不推荐
    return u.getAge() > 18;
})

在并行流下会产生线程安全问题,且违背函数式思想。


❌ 2. filter 的 predicate 不能抛 checked exception

less 复制代码
.filter(u -> doSomething(u)) // 如果 doSomething 抛异常,无法编译

需要自己封装异常处理。


❌ 3. filter 是惰性的,不会立即过滤

必须搭配终止操作:

scss 复制代码
users.stream().filter(...); // 什么都不会执行
scss 复制代码
users.stream().filter(...).toList(); // 真正执行过滤

五、三句话总结

filter = 用 Predicate 判断哪些元素保留。

返回 true → 保留;返回 false → 过滤。

底层是遍历 + 判断,执行是惰性的。

掌握了这三点,就彻底吃透了 filter


相关推荐
用户03048059126339 分钟前
# 【Maven避坑】源码去哪了?一文看懂 Maven 工程与打包后的目录映射关系
java·后端
绫语宁1 小时前
以防你不知道LLM小技巧!为什么 LLM 不适合多任务推理?
人工智能·后端
q***18841 小时前
Spring Boot中的404错误:原因、影响及处理策略
java·spring boot·后端
用户69371750013841 小时前
17.Kotlin 类:类的形态(四):枚举类 (Enum Class)
android·后端·kotlin
h***34631 小时前
MS SQL Server 实战 排查多列之间的值是否重复
android·前端·后端
用户69371750013841 小时前
16.Kotlin 类:类的形态(三):密封类 (Sealed Class)
android·后端·kotlin
马卡巴卡1 小时前
MySQL权限管理的坑你踩了没有?
后端
4***17541 小时前
Spring Boot整合WebSocket
spring boot·后端·websocket
Penge6661 小时前
Elasticsearch 集群必看:为什么 3 个 Master 节点是生产环境的 “黄金配置”?
后端