[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的筛选、切片、映射和查找匹配操作为我们提供了强大的数据处理能力。关键要点总结:
- 筛选切片:
filter、distinct、limit、skip用于数据过滤和分页 - 映射转换:
map用于元素转换,flatMap解决嵌套结构扁平化 - 查找匹配:
anyMatch、allMatch、noneMatch用于条件检查,findFirst、findAny用于元素查找 - 性能优化:注意操作顺序,避免重复计算,合理使用短路操作
掌握这些操作后,你会发现处理集合数据变得前所未有的简洁和高效。Stream API的真正威力在于操作组合,通过链式调用构建复杂的数据处理管道,让代码既简洁又富有表达力。
思考题:在你的项目中,哪些集合处理场景最适合用Stream API重构?尝试用本讲介绍的操作优化一个复杂的循环逻辑!
下期预告:第7讲:Stream的归约与数值流——数据聚合
更多技术干货欢迎关注微信公众号“科威舟的AI笔记”~
【转载须知】:转载请注明原文出处及作者信息