1. 引言

本文将探讨如何在Java中过滤包含嵌套列表的集合。当处理复杂的数据结构(例如包含其他列表的对象列表)时,根据特定条件提取特定信息变得至关重要。我们将通过实际案例展示多种实现方案,帮助您优雅地解决这类问题。

2. 问题理解

我们使用一个简单的示例:包含User类和Order类的数据模型。User类包含nameOrder列表,Order类包含productprice。目标是基于嵌套订单的条件过滤用户列表。

数据模型结构如下:

class User {
    private String name;
    private List<Order> orders;
    
    public User(String name, List<Order> orders) {
        this.name = name;
        this.orders = orders;
    }

    // set get 方法
}

class Order {
    private String product;
    private double price;
    
    public Order(String product, double price) {
        this.product = product;
        this.price = price;
    }

    // set get 方法
}

为演示过滤逻辑,我们创建测试数据:

Order order1 = new Order("Laptop", 600.0);
Order order2 = new Order("Phone", 300.0);
Order order3 = new Order("Monitor", 510.0);
Order order4 = new Order("Monitor", 200.0);

User user1 = new User("Alice", Arrays.asList(order1, order4));
User user2 = new User("Bob", Arrays.asList(order3));
User user3 = new User("Mity", Arrays.asList(order2));

List users = Arrays.asList(user1, user2, user3);

假设我们要筛选出存在订单金额超过$500的用户。根据数据,Alice和Bob符合条件,预期结果应返回2个用户。

3. 传统循环方案

在Java 8引入Stream API之前,通常使用for循环实现过滤。下面展示嵌套循环的实现方式:

double priceThreshold = 500.0;

List<User> filteredUsers = new ArrayList<>();
for (User user : users) {
    for (Order order : user.getOrders()) {
        if (order.getPrice() > priceThreshold) {
            filteredUsers.add(user);
            break;
        }
    }
}

assertEquals(2, filteredUsers.size());

✅ 核心逻辑:

  • 遍历每个用户及其订单列表
  • 一旦发现符合条件的订单,立即添加用户并跳出内层循环(使用break

⚠️ 虽然这种方法直观易懂,但需要手动管理嵌套循环,且缺乏函数式编程的简洁性。

4. 使用Java Stream过滤

Java 8的Stream API提供了更优雅的解决方案。我们使用Stream实现相同的过滤逻辑:

double priceThreshold = 500.0;

List<User> filteredUsers = users.stream()
  .filter(user -> user.getOrders().stream()
    .anyMatch(order -> order.getPrice() > priceThreshold))
  .collect(Collectors.toList());

assertEquals(2, filteredUsers.size());

✅ 关键点:

  • 外层Stream处理用户列表
  • 内层Stream检查用户是否存在符合价格条件的订单
  • anyMatch()方法高效判断是否存在至少一个匹配项

5. 多条件过滤

当需要组合多个条件时(如产品类型+价格),可在anyMatch()内组合逻辑:

double priceThreshold = 500.0;
String productToFilter = "Laptop";

List<User> filteredUsers = users.stream()
  .filter(user -> user.getOrders().stream()
    .anyMatch(order -> order.getProduct().equals(productToFilter) 
      && order.getPrice() > priceThreshold))
  .collect(Collectors.toList());

assertEquals(1, filteredUsers.size());
assertEquals("Alice", filteredUsers.get(0).getName());

✅ 结果说明:

  • 只有Alice同时满足"购买Laptop"和"金额>$500"两个条件
  • 多条件通过逻辑运算符(&&/||)灵活组合

6. 使用自定义谓词(Predicate)

将过滤逻辑封装到自定义Predicate中可提升代码可读性和复用性:

Predicate<User> hasExpensiveOrder = user -> user.getOrders().stream()
  .anyMatch(order -> order.getPrice() > priceThreshold);

List<User> filteredUsers = users.stream()
  .filter(hasExpensiveOrder)
  .collect(Collectors.toList());

assertEquals(2, filteredUsers.size());

✅ 优势:

  • 谓词逻辑独立封装,主流程更清晰
  • 谓词可在多处复用,避免重复代码

7. 保留结构化过滤

当需要保留所有用户但过滤其订单列表时(而非直接移除用户),可采用以下方案:

List<User> filteredUsersWithLimitedOrders = users.stream()
  .map(user -> {
    List<Order> filteredOrders = user.getOrders().stream()
      .filter(order -> order.getPrice() > priceThreshold)
      .collect(Collectors.toList());
    user.setOrders(filteredOrders);
    return user;
  })
  .filter(user -> !user.getOrders().isEmpty())
  .collect(Collectors.toList());

assertEquals(2, filteredUsersWithLimitedOrders.size());
assertEquals(1, filteredUsersWithLimitedOrders.get(0).getOrders().size());
assertEquals(1, filteredUsersWithLimitedOrders.get(1).getOrders().size());

✅ 实现要点:

  • 使用map()修改用户对象
  • 先过滤订单列表,再更新用户
  • 最后移除无有效订单的用户

8. 使用flatMap()方案

通过flatMap()扁平化嵌套结构,可避免多层Stream调用:

List<User> filteredUsers = users.stream()
  .flatMap(user -> user.getOrders().stream()
    .filter(order -> order.getPrice() > priceThreshold)
    .map(order -> user)) 
  .distinct()
  .collect(Collectors.toList());

assertEquals(2, filteredUsers.size());

✅ 核心机制:

  • 将用户转换为订单流
  • 过滤订单后映射回用户
  • distinct()确保用户唯一性

⚠️ 踩坑提示:若用户有多个符合条件的订单,会产生重复用户,必须使用distinct()去重。

9. 边界情况处理

当用户可能没有订单时,需添加空值检查避免NullPointerException

User user1 = new User("Alice", Arrays.asList(order1, order2));
User user2 = new User("Bob", Arrays.asList(order3));
User user3 = new User("Charlie", new ArrayList<>());
List users = Arrays.asList(user1, user2, user3);

List<User> filteredUsers = users.stream()
  .filter(user -> user.getOrders() != null && !user.getOrders().isEmpty()) 
  .filter(user -> user.getOrders().stream()
    .anyMatch(order -> order.getPrice() > priceThreshold))
  .collect(Collectors.toList());

assertEquals(2, filteredUsers.size());

✅ 防御措施:

  • 首先检查订单列表非空
  • 确保后续操作在安全数据上执行

10. 总结

本文系统介绍了在Java中基于嵌套列表过滤的多种方案:

  • 传统循环:简单直接但代码冗长
  • Stream API:函数式风格,代码简洁
  • 自定义谓词:提升逻辑复用性
  • 结构化过滤:保留原始数据结构
  • flatMap():扁平化处理嵌套集合
  • 边界处理:增强代码健壮性

✅ 最佳实践建议:

  • 优先使用Stream API提升可读性
  • 复杂逻辑封装为谓词
  • 始终考虑空值等边界情况
  • 根据实际需求选择保留或丢弃用户

通过合理选择这些技术,可以优雅地处理各种嵌套集合过滤场景。


原始标题:Filter a List by Nested Lists in Java | Baeldung