第6讲:Stream的筛选、切片、映射与查找匹配

75 阅读8分钟

[toc]

一、Stream操作概览:构建高效数据处理流水线

在Java 8的Stream API中,操作可以分为两大类:中间操作终端操作。理解这一分类对掌握Stream编程至关重要。

1.1 中间操作与终端操作的区别

中间操作(如filter、map、sorted)总是返回一个新的Stream,并且具有惰性求值特性——它们不会立即执行,只有在终端操作调用时才会真正处理数据。

终端操作(如collect、forEach、reduce)会触发实际计算,并产生具体结果或副作用。一旦调用了终端操作,Stream就被消费完毕,无法再次使用。

1.2 操作链的构建与执行

下面通过一个表格直观展示Stream操作链的构建过程:

操作类型方法示例功能描述返回结果
创建流stream()从集合创建流Stream<T>
中间操作filter()过滤不符合条件的元素Stream<T>
中间操作map()转换元素类型或值Stream<R>
终端操作collect()将流转换为集合Collection<T>

这种声明式编程风格让我们更关注"做什么"而不是"如何做",大大提高了代码的可读性和维护性。

二、筛选与切片操作实战

2.1 filter过滤:基于条件的元素筛选

filter方法是Stream中最常用的操作之一,它接受一个Predicate函数式接口作为参数,用于筛选满足条件的元素。

// 准备测试数据:产品列表
List<Product> products = Arrays.asList(
    new Product("Laptop", "Electronics", 999.99, 10),
    new Product("Smartphone", "Electronics", 699.99, 25),
    new Product("Shirt", "Clothing", 29.99, 50),
    new Product("Pants", "Clothing", 49.99, 30)
);

// 筛选价格大于100的电子产品
List<Product> expensiveElectronics = products.stream()
    .filter(p -> "Electronics".equals(p.getCategory())) // 筛选电子产品
    .filter(p -> p.getPrice() > 100) // 筛选价格>100
    .collect(Collectors.toList());

System.out.println("高价电子产品: " + expensiveElectronics);

在实际业务中,我们经常需要组合多个过滤条件:

// 复杂条件筛选:价格在50-500之间且库存充足的服装产品
Predicate<Product> isClothing = p -> "Clothing".equals(p.getCategory());
Predicate<Product> priceInRange = p -> p.getPrice() >= 50 && p.getPrice() <= 500;
Predicate<Product> hasStock = p -> p.getStock() > 10;

List<Product> suitableProducts = products.stream()
    .filter(isClothing.and(priceInRange).and(hasStock))
    .collect(Collectors.toList());

2.2 distinct去重:消除重复元素

distinct方法基于元素的equals()hashCode()方法来去重,返回一个元素各异的流。

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
List<Integer> distinctNumbers = numbers.stream()
    .filter(i -> i % 2 == 0) // 筛选偶数
    .distinct() // 去重
    .collect(Collectors.toList());

System.out.println("不重复的偶数: " + distinctNumbers); // 输出 [2, 4]

注意:如果要对自定义对象去重,必须正确重写equals()hashCode()方法。

2.3 limit与skip:数据分页的利器

limit(n)用于截取前n个元素,skip(n)用于跳过前n个元素,两者结合可以实现高效的数据分页。

// 分页查询:每页2条记录,获取第2页数据
int pageSize = 2;
int pageNumber = 2;

List<Product> secondPageProducts = products.stream()
    .filter(p -> p.getPrice() > 30) // 筛选条件
    .skip((pageNumber - 1) * pageSize) // 跳过前一页的数据
    .limit(pageSize) // 获取当前页的数据量
    .collect(Collectors.toList());

System.out.println("第2页产品: " + secondPageProducts);

这种分页方式在内存数据处理场景下非常高效,特别适合处理大数据集的分批处理。

三、映射操作深度解析

3.1 map映射:元素转换与提取

map方法是最常用的映射操作,它接受一个Function函数式接口,将流中的每个元素转换为另一个元素。

// 提取产品名称列表
List<String> productNames = products.stream()
    .map(Product::getName) // 方法引用,等价于 p -> p.getName()
    .collect(Collectors.toList());

// 转换数据类型:产品名称转为大写
List<String> upperCaseNames = products.stream()
    .map(Product::getName)
    .map(String::toUpperCase) // 再次映射:转换为大写
    .collect(Collectors.toList());

// 计算产品含税价格(税率10%)
List<Double> pricesWithTax = products.stream()
    .map(p -> p.getPrice() * 1.1) // 计算含税价格
    .collect(Collectors.toList());

map操作的强大之处在于可以链式调用,构建复杂的数据转换管道。

3.2 flatMap扁平化映射:处理嵌套结构的利器

flatMap是Stream API中最难理解但极其强大的操作之一。它解决了"一对多"映射产生的嵌套流问题。

经典问题:单词拆分场景

假设我们有一个句子列表,需要提取所有不重复的单词:

List<String> sentences = Arrays.asList("Hello world", "Java Stream API", "Hello Java");

// 错误做法:产生Stream<Stream<String>>嵌套结构
List<Stream<String>> wrongResult = sentences.stream()
    .map(sentence -> Arrays.stream(sentence.split(" "))) // 映射为Stream流
    .collect(Collectors.toList());

// 正确做法:使用flatMap扁平化处理
List<String> words = sentences.stream()
    .map(sentence -> sentence.split(" ")) // 将每个句子拆分为单词数组
    .flatMap(Arrays::stream) // 将每个数组转换为流并扁平化合并
    .distinct() // 去重
    .collect(Collectors.toList());

System.out.println("所有不重复单词: " + words); 
// 输出: [Hello, world, Java, Stream, API]

实际业务应用:订单项展开

考虑电商系统中订单与订单项的关系:

public class Order {
    private String orderId;
    private List<OrderItem> items; // 一个订单有多个订单项
    // 构造方法、getter/setter省略
}

public class OrderItem {
    private String productName;
    private Integer quantity;
    private Double price;
    // 构造方法、getter/setter省略
}

List<Order> orders = getOrders(); // 获取订单列表

// 提取所有订单项进行统计分析
List<OrderItem> allItems = orders.stream()
    .map(Order::getItems) // 这里得到List<List<OrderItem>>
    .flatMap(List::stream) // 扁平化为List<OrderItem>
    .collect(Collectors.toList());

// 计算所有订单的总销售额
double totalSales = allItems.stream()
    .mapToDouble(item -> item.getQuantity() * item.getPrice())
    .sum();

flatMap的本质是:将每个元素转换为流,然后将所有流连接成一个流。这在处理嵌套集合时非常有用。

四、查找与匹配操作实战

4.1 匹配检查:anyMatch、allMatch、noneMatch

这三种方法用于检查流中元素是否满足特定条件,均返回boolean结果,且支持短路求值

// 检查是否存在高价电子产品(anyMatch:至少一个匹配)
boolean hasExpensiveElectronics = products.stream()
    .anyMatch(p -> "Electronics".equals(p.getCategory()) && p.getPrice() > 1000);

// 检查是否所有电子产品都价格合理(allMatch:全部匹配)
boolean allElectronicsReasonable = products.stream()
    .filter(p -> "Electronics".equals(p.getCategory()))
    .allMatch(p -> p.getPrice() < 2000);

// 检查是否没有价格过高的产品(noneMatch:全部不匹配)
boolean noOverpricedProducts = products.stream()
    .noneMatch(p -> p.getPrice() > 5000);

System.out.println("是否存在高价电子产品: " + hasExpensiveElectronics);
System.out.println("是否所有电子产品价格合理: " + allElectronicsReasonable);
System.out.println("是否没有价格过高产品: " + noOverpricedProducts);

4.2 元素查找:findFirst与findAny

findFirst返回第一个元素,findAny返回任意元素,两者都返回Optional类型,避免空指针异常。

// 查找第一个高价电子产品
Optional<Product> firstExpensive = products.stream()
    .filter(p -> p.getPrice() > 500)
    .findFirst();

// 使用Optional安全处理结果
firstExpensive.ifPresent(product -> 
    System.out.println("第一个高价产品: " + product.getName()));

// 查找任意一个库存充足的产品(在并行流中效率更高)
Optional<Product> anyInStock = products.stream()
    .filter(p -> p.getStock() > 20)
    .findAny();

anyInStock.ifPresentOrElse(
    product -> System.out.println("找到库存充足产品: " + product.getName()),
    () -> System.out.println("没有找到库存充足的产品")
);

重要区别:在顺序流中两者行为相同,但在并行流中findAny性能更好,因为它不保证返回第一个元素。

五、综合实战:电商订单数据分析

让我们通过一个完整的电商订单分析案例,综合运用各种Stream操作:

public class OrderAnalysisService {
    
    public void analyzeOrders(List<Order> orders) {
        // 1. 数据筛选:2023年的有效订单
        List<Order> valid2023Orders = orders.stream()
            .filter(order -> order.getYear() == 2023)
            .filter(order -> order.getIsValid() == 1)
            .collect(Collectors.toList());
        
        // 2. 销售统计:按类别分组计算销售额
        Map<String, Double> salesByCategory = valid2023Orders.stream()
            .flatMap(order -> order.getItems().stream()) // 展开订单项
            .collect(Collectors.groupingBy(
                OrderItem::getCategory,
                Collectors.summingDouble(item -> item.getQuantity() * item.getPrice())
            ));
        
        // 3. 热销商品分析:销量前十的商品
        List<String> topSellingProducts = valid2023Orders.stream()
            .flatMap(order -> order.getItems().stream())
            .collect(Collectors.groupingBy(
                OrderItem::getProductName,
                Collectors.summingInt(OrderItem::getQuantity)
            ))
            .entrySet().stream()
            .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
            .limit(10)
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());
        
        // 4. 客户消费分析:高价值客户识别
        Map<Integer, Double> customerSpending = orders.stream()
            .filter(order -> order.getYear() == 2023)
            .collect(Collectors.groupingBy(
                Order::getUserId,
                Collectors.summingDouble(Order::getTotalAmount)
            ));
        
        List<Integer> valuableCustomers = customerSpending.entrySet().stream()
            .filter(entry -> entry.getValue() > 10000) // 消费超过10000的客户
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());
        
        // 输出分析结果
        System.out.println("2023年有效订单数: " + valid2023Orders.size());
        System.out.println("按类别销售额: " + salesByCategory);
        System.out.println("热销商品TOP10: " + topSellingProducts);
        System.out.println("高价值客户数量: " + valuableCustomers.size());
    }
}

六、性能优化与最佳实践

6.1 操作顺序优化

Stream操作的顺序会影响性能,应该优先使用过滤操作减少数据处理量:

// 不推荐的顺序:先映射后过滤
List<String> names = products.stream()
    .map(Product::getName)        // 所有产品都执行映射
    .filter(name -> name.length() > 5) // 然后过滤
    .collect(Collectors.toList());

// 推荐的顺序:先过滤后映射
List<String> namesOptimized = products.stream()
    .filter(p -> p.getName().length() > 5) // 先过滤减少数据量
    .map(Product::getName)                 // 只对过滤后的数据映射
    .collect(Collectors.toList());

6.2 避免重复计算

对于需要多次使用的Stream结果,应该收集结果复用它

// 错误:重复创建流
long expensiveCount = products.stream().filter(p -> p.getPrice() > 500).count();
List<String> expensiveNames = products.stream()
    .filter(p -> p.getPrice() > 500)
    .map(Product::getName)
    .collect(Collectors.toList());

// 正确:一次处理,重复使用
List<Product> expensiveProducts = products.stream()
    .filter(p -> p.getPrice() > 500)
    .collect(Collectors.toList());

long expensiveCount = expensiveProducts.size();
List<String> expensiveNames = expensiveProducts.stream()
    .map(Product::getName)
    .collect(Collectors.toList());

七、总结

Stream API的筛选、切片、映射和查找匹配操作为我们提供了强大的数据处理能力。关键要点总结:

  1. 筛选切片filterdistinctlimitskip用于数据过滤和分页
  2. 映射转换map用于元素转换,flatMap解决嵌套结构扁平化
  3. 查找匹配anyMatchallMatchnoneMatch用于条件检查,findFirstfindAny用于元素查找
  4. 性能优化:注意操作顺序,避免重复计算,合理使用短路操作

掌握这些操作后,你会发现处理集合数据变得前所未有的简洁和高效。Stream API的真正威力在于操作组合,通过链式调用构建复杂的数据处理管道,让代码既简洁又富有表达力。

思考题:在你的项目中,哪些集合处理场景最适合用Stream API重构?尝试用本讲介绍的操作优化一个复杂的循环逻辑!

下期预告:第7讲:Stream的归约与数值流——数据聚合


更多技术干货欢迎关注微信公众号“科威舟的AI笔记”~

【转载须知】:转载请注明原文出处及作者信息