1. 初识Stream filter()方法
第一次接触Java 8的Stream API时,我被filter()方法的简洁优雅深深吸引。记得当时正在处理一个用户列表筛选的需求,传统写法需要写一堆for循环和if判断,而用filter()只需要一行代码就搞定了。这种声明式的编程方式,让代码的可读性提升了不止一个档次。
filter()是Stream API中的一个中间操作,它的作用就像筛子一样,可以根据我们设定的条件过滤出想要的元素。方法签名非常简单:
Stream<T> filter(Predicate<? super T> predicate)
,接收一个Predicate函数式接口作为参数,返回一个新的Stream。
举个例子,假设我们有一个用户列表,要筛选出年龄大于18岁的用户。用传统写法可能是这样的:
List<User> adultUsers = new ArrayList<>();
for(User user : userList) {
if(user.getAge() > 18) {
adultUsers.add(user);
}
}
而用Stream filter()可以写成:
List<User> adultUsers = userList.stream()
.filter(user -> user.getAge() > 18)
.collect(Collectors.toList());
是不是简洁多了?这种链式调用的写法,就像在组装一条数据处理流水线,每个操作都是一个工序,数据像水流一样经过各个处理环节。
2. filter()基础用法实战
2.1 简单条件过滤
filter()最基本的用法就是根据单一条件过滤集合。比如我们有一个商品列表,要筛选出价格大于100元的商品:
List<Product> expensiveProducts = products.stream()
.filter(p -> p.getPrice() > 100)
.collect(Collectors.toList());
这里
p -> p.getPrice() > 100
就是一个Predicate,它定义了过滤条件。在实际项目中,我经常用这种方式快速过滤数据,比写for循环要高效得多。
2.2 多条件组合过滤
有时候我们需要组合多个条件进行过滤,这时可以用逻辑运算符连接多个Predicate。比如要筛选出价格在100到500元之间,且库存大于0的商品:
List<Product> availableMidPriceProducts = products.stream()
.filter(p -> p.getPrice() > 100 && p.getPrice() < 500 && p.getStock() > 0)
.collect(Collectors.toList());
也可以把条件拆分开来,这样可读性更好:
Predicate<Product> priceRange = p -> p.getPrice() > 100 && p.getPrice() < 500;
Predicate<Product> inStock = p -> p.getStock() > 0;
List<Product> result = products.stream()
.filter(priceRange.and(inStock))
.collect(Collectors.toList());
2.3 处理null值
在实际项目中,我们经常会遇到集合中包含null值的情况。如果不处理,可能会抛出NullPointerException。我吃过这个亏,所以现在都会先过滤掉null值:
List<String> notNullList = list.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
或者更严格一点,同时检查字符串不为空:
List<String> validStrings = list.stream()
.filter(s -> s != null && !s.isEmpty())
.collect(Collectors.toList());
3. filter()与其他Stream操作结合
3.1 与findFirst()/findAny()配合
findFirst()和findAny()都是终端操作,用于从流中获取元素。结合filter()可以很方便地查找符合条件的第一个元素。比如我们要找第一个年龄大于30的用户:
Optional<User> firstAdult = users.stream()
.filter(u -> u.getAge() > 30)
.findFirst();
findAny()在并行流中效率更高,因为它不保证返回的是第一个匹配元素。我曾经在一个大数据量处理的场景中,用findAny()替代findFirst(),性能提升了近30%。
3.2 与map()配合使用
map()用于转换流中的元素,经常和filter()一起使用。比如我们要获取所有管理员用户的姓名列表:
List<String> adminNames = users.stream()
.filter(u -> "admin".equals(u.getRole()))
.map(User::getName)
.collect(Collectors.toList());
这种组合在实际开发中非常常见,先过滤再转换,代码一气呵成。
3.3 与reduce()配合统计
reduce()可以用来做归约操作,结合filter()可以实现条件统计。比如计算所有VIP用户的消费总额:
double totalVipSpending = orders.stream()
.filter(o -> o.getUser().isVip())
.map(Order::getAmount)
.reduce(0.0, Double::sum);
4. 复杂业务场景实战
4.1 电商订单筛选
在电商系统中,订单筛选是非常常见的需求。比如我们要筛选出最近一个月内,金额大于500元且未发货的订单:
LocalDate oneMonthAgo = LocalDate.now().minusMonths(1);
List<Order> targetOrders = orders.stream()
.filter(o -> o.getCreateDate().isAfter(oneMonthAgo))
.filter(o -> o.getAmount() > 500)
.filter(o -> !o.isShipped())
.collect(Collectors.toList());
我曾经优化过一个老项目的订单查询接口,用Stream API替换了原来的多重for循环,代码量减少了60%,性能还提升了15%。
4.2 用户画像过滤
用户画像分析时,经常需要根据多个标签筛选用户。比如要找年龄在25-35岁之间,居住在北上广深,且最近有登录的女性用户:
Set<String> tier1Cities = Set.of("北京", "上海", "广州", "深圳");
List<User> targetUsers = users.stream()
.filter(u -> u.getAge() >= 25 && u.getAge() <= 35)
.filter(u -> tier1Cities.contains(u.getCity()))
.filter(u -> u.getLastLoginDate().isAfter(LocalDate.now().minusDays(30)))
.filter(u -> u.getGender() == Gender.FEMALE)
.collect(Collectors.toList());
4.3 日志分析处理
分析服务器日志时,我们可能需要筛选出特定级别的错误日志,并按时间排序:
List<LogEntry> errorLogs = logs.stream()
.filter(l -> Level.ERROR.equals(l.getLevel()))
.filter(l -> l.getTimestamp().isAfter(startTime))
.sorted(Comparator.comparing(LogEntry::getTimestamp))
.collect(Collectors.toList());
在处理一个线上问题时,我曾经用类似的代码快速定位到了问题时间段的错误日志,大大缩短了故障排查时间。
5. 性能优化与注意事项
5.1 避免重复操作
Stream的每个终端操作都会触发整个流水线的执行,所以要避免重复调用。我曾经见过这样的代码:
// 不好的写法 - 执行了两次终端操作
long count = list.stream().filter(...).count();
List<Result> result = list.stream().filter(...).collect(Collectors.toList());
应该改为:
List<Result> result = list.stream().filter(...).collect(Collectors.toList());
long count = result.size();
5.2 注意操作顺序
Stream操作顺序会影响性能。一般来说,filter()应该尽量靠前,这样可以减少后续操作的数据量。比如:
// 更好的顺序
List<String> names = users.stream()
.filter(u -> u.getAge() > 18) // 先过滤
.map(User::getName) // 再转换
.collect(Collectors.toList());
5.3 并行流的使用
对于大数据集,可以考虑使用并行流:
List<User> adultUsers = users.parallelStream()
.filter(u -> u.getAge() > 18)
.collect(Collectors.toList());
但要注意并行流不是万能的,它有一定的开销,对于小数据集可能反而更慢。我在一个包含百万级数据的处理任务中测试过,并行流比顺序流快了近3倍。
5.4 对象引用问题
使用filter()过滤对象集合时要注意,过滤后的集合中的对象仍然是原集合中对象的引用。这意味着如果修改过滤后的对象,原集合中的对象也会被修改。如果需要完全独立的副本,需要手动创建新对象:
List<User> copiedUsers = users.stream()
.filter(u -> u.getAge() > 18)
.map(u -> new User(u)) // 使用拷贝构造函数
.collect(Collectors.toList());
6. 常见问题与解决方案
6.1 如何处理过滤结果为空的情况
使用Stream API时,经常会遇到过滤结果可能为空的情况。Optional可以很好地处理这种情况:
Optional<User> optionalUser = users.stream()
.filter(u -> u.getId() == targetId)
.findFirst();
optionalUser.ifPresent(user -> {
// 处理找到的用户
});
6.2 复杂条件的封装
当过滤条件变得复杂时,可以考虑将Predicate封装成方法或类,提高代码可读性:
public Predicate<User> isEligibleForPromotion() {
return u -> u.getAge() >= 18
&& u.getPurchaseCount() >= 5
&& u.getLastPurchaseDate().isAfter(LocalDate.now().minusMonths(3));
}
List<User> eligibleUsers = users.stream()
.filter(isEligibleForPromotion())
.collect(Collectors.toList());
6.3 调试Stream操作
调试Stream操作可能不太直观,可以使用peek()方法查看流经的元素:
List<User> result = users.stream()
.peek(u -> System.out.println("Processing: " + u))
.filter(u -> u.getAge() > 18)
.peek(u -> System.out.println("After filter: " + u))
.collect(Collectors.toList());
在实际项目中,我经常用这个技巧来排查Stream处理过程中的问题。
367



被折叠的 条评论
为什么被折叠?



