作者:Java架构师 • 发布日期:2023年11月15日
Stream API概述
Java 8引入的Stream API彻底改变了Java中集合处理的方式,提供了声明式的函数式编程模型。与传统的迭代式操作相比,Stream API使代码更简洁、更易读,并且可以自动利用多核架构。
Stream的主要特点:
- 声明式编程:描述做什么而不是怎么做
- 函数式风格:使用Lambda表达式和方法引用
- 流水线操作:多个操作可以连接形成复杂的数据处理管道
- 自动并行化:只需调用parallel()方法即可实现并行处理
- 无存储:Stream本身不存储数据,数据来自数据源(集合、数组等)
Stream操作分为两类:中间操作(返回Stream)和终端操作(返回结果或副作用)。
Stream基础操作
创建Stream有多种方式,下面展示最常用的几种方法:
从集合创建Stream
import java.util.*; import java.util.stream.*; public class StreamCreation { public static void main(String[] args) { // 从集合创建Stream List names = Arrays.asList("Alice", "Bob", "Charlie", "David"); Stream nameStream = names.stream(); // 从数组创建Stream String[] nameArray = {"Eva", "Frank", "Grace"}; Stream arrayStream = Arrays.stream(nameArray); // 使用Stream.of()创建 Stream directStream = Stream.of("Henry", "Ivy", "Jack"); // 使用Stream.generate()创建无限流 Stream randomStream = Stream.generate(Math::random).limit(5); randomStream.forEach(System.out::println); // 使用Stream.iterate()创建无限流 Stream evenNumbers = Stream.iterate(0, n -> n + 2).limit(10); evenNumbers.forEach(n -> System.out.print(n + " ")); } }
基本Stream操作示例
public class BasicStreamOperations { public static void main(String[] args) { List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 过滤偶数并计算平方 List result = numbers.stream() .filter(n -> n % 2 == 0) // 中间操作:过滤偶数 .map(n -> n * n) // 中间操作:计算平方 .collect(Collectors.toList()); // 终端操作:收集结果 System.out.println("偶数的平方: " + result); // 计算所有数字的和 int sum = numbers.stream() .reduce(0, Integer::sum); // 终端操作:归约求和 System.out.println("总和: " + sum); } }
中间操作详解
中间操作是Stream处理管道中的构建块,它们返回新的Stream,可以连接多个操作。
filter() – 过滤元素
List names = Arrays.asList("Alice", "Bob", "Anna", "Alex", "Charlie"); // 过滤以'A'开头的名字 List aNames = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList()); System.out.println("以A开头的名字: " + aNames);
map() – 元素转换
List words = Arrays.asList("java", "stream", "api", "lambda"); // 将单词转换为大写 List upperCaseWords = words.stream() .map(String::toUpperCase) .collect(Collectors.toList()); System.out.println("大写单词: " + upperCaseWords); // 获取单词长度 List wordLengths = words.stream() .map(String::length) .collect(Collectors.toList()); System.out.println("单词长度: " + wordLengths);
flatMap() – 扁平化流
List<List> numberLists = Arrays.asList( Arrays.asList(1, 2, 3), Arrays.asList(4, 5, 6), Arrays.asList(7, 8, 9) ); // 将多个列表扁平化为单个流 List allNumbers = numberLists.stream() .flatMap(List::stream) .collect(Collectors.toList()); System.out.println("所有数字: " + allNumbers);
distinct()和sorted() – 去重和排序
List numbers = Arrays.asList(5, 3, 8, 2, 5, 9, 1, 3, 8); // 去重并排序 List uniqueSorted = numbers.stream() .distinct() .sorted() .collect(Collectors.toList()); System.out.println("去重排序: " + uniqueSorted); // 自定义排序 List names = Arrays.asList("Charlie", "Alice", "Bob", "David"); List sortedByNameLength = names.stream() .sorted(Comparator.comparingInt(String::length)) .collect(Collectors.toList()); System.out.println("按长度排序: " + sortedByNameLength);
终端操作实战
终端操作会消耗Stream,产生最终结果或副作用。
collect() – 结果收集
List names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva"); // 收集到List List list = names.stream() .filter(name -> name.length() > 3) .collect(Collectors.toList()); // 收集到Set Set set = names.stream() .collect(Collectors.toSet()); // 收集到Map Map nameLengthMap = names.stream() .collect(Collectors.toMap( Function.identity(), // 键:名字本身 String::length // 值:名字长度 )); // 连接字符串 String joined = names.stream() .collect(Collectors.joining(", ", "[", "]")); System.out.println("连接结果: " + joined);
reduce() – 归约操作
List numbers = Arrays.asList(1, 2, 3, 4, 5); // 求和 int sum = numbers.stream() .reduce(0, Integer::sum); // 求乘积 int product = numbers.stream() .reduce(1, (a, b) -> a * b); // 求最大值 Optional max = numbers.stream() .reduce(Integer::max); System.out.println("和: " + sum); System.out.println("积: " + product); max.ifPresent(m -> System.out.println("最大值: " + m));
匹配和查找操作
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 检查是否所有元素都大于0 boolean allPositive = numbers.stream() .allMatch(n -> n > 0); // 检查是否存在偶数 boolean hasEven = numbers.stream() .anyMatch(n -> n % 2 == 0); // 查找第一个大于5的数 Optional firstLarge = numbers.stream() .filter(n -> n > 5) .findFirst(); System.out.println("所有正数: " + allPositive); System.out.println("存在偶数: " + hasEven); firstLarge.ifPresent(n -> System.out.println("第一个大于5的数: " + n));
并行流与性能优化
Java Stream API使得并行处理变得非常简单,但需要谨慎使用以避免性能陷阱。
创建并行流
List numbers = IntStream.rangeClosed(1, 1000000) .boxed() .collect(Collectors.toList()); // 顺序流处理 long startTime = System.currentTimeMillis(); long sequentialCount = numbers.stream() .filter(n -> n % 2 == 0) .count(); long sequentialTime = System.currentTimeMillis() - startTime; // 并行流处理 startTime = System.currentTimeMillis(); long parallelCount = numbers.parallelStream() .filter(n -> n % 2 == 0) .count(); long parallelTime = System.currentTimeMillis() - startTime; System.out.println("顺序处理时间: " + sequentialTime + "ms"); System.out.println("并行处理时间: " + parallelTime + "ms"); System.out.println("结果相同: " + (sequentialCount == parallelCount));
并行流注意事项
// 有状态操作可能导致错误结果 List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 错误的使用方式:有状态的Lambda List badResult = numbers.parallelStream() .sorted() // 在并行流中,sorted是有状态操作,需要谨慎使用 .map(n -> { // 避免在并行流中使用有状态的操作 return n * 2; }) .collect(Collectors.toList()); // 正确的方式:确保操作是无状态的 List goodResult = numbers.parallelStream() .map(n -> n * 2) // 无状态操作 .collect(Collectors.toList()); System.out.println("正确结果: " + goodResult);
高级技巧与模式
掌握一些高级技巧可以让Stream API更加强大和灵活。
自定义收集器
// 创建自定义收集器计算平均值 Collector averagingCollector = Collector.of( () -> new double[2], // 供应器:创建容器 [sum, count] (acc, value) -> { // 累加器:累加值和计数 acc[0] += value; acc[1]++; }, (acc1, acc2) -> { // 组合器:合并两个容器(用于并行流) acc1[0] += acc2[0]; acc1[1] += acc2[1]; return acc1; }, acc -> acc[0] / acc[1] // 完成器:计算平均值 ); List numbers = Arrays.asList(1, 2, 3, 4, 5); double average = numbers.stream() .collect(averagingCollector); System.out.println("平均值: " + average);
使用Stream处理IO操作
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.stream.Stream; public class FileStreamExample { public static void main(String[] args) { // 使用Stream读取文件 try (Stream lines = Files.lines(Paths.get("data.txt"))) { long wordCount = lines .flatMap(line -> Stream.of(line.split("\s+"))) // 分割单词 .filter(word -> !word.isEmpty()) // 过滤空字符串 .count(); System.out.println("单词总数: " + wordCount); } catch (IOException e) { e.printStackTrace(); } } }
实战案例:电商数据分析
下面是一个完整的电商数据分析案例,展示Stream API在实际业务中的应用。
import java.util.*; import java.util.stream.*; // 定义订单和商品类 class Product { private String name; private String category; private double price; public Product(String name, String category, double price) { this.name = name; this.category = category; this.price = price; } public String getName() { return name; } public String getCategory() { return category; } public double getPrice() { return price; } } class Order { private int id; private List products; private Date orderDate; public Order(int id, List products, Date orderDate) { this.id = id; this.products = products; this.orderDate = orderDate; } public int getId() { return id; } public List getProducts() { return products; } public Date getOrderDate() { return orderDate; } public double getTotalAmount() { return products.stream() .mapToDouble(Product::getPrice) .sum(); } } public class ECommerceAnalysis { public static void main(String[] args) { // 创建示例数据 List products = Arrays.asList( new Product("Laptop", "Electronics", 1200.0), new Product("Smartphone", "Electronics", 800.0), new Product("Book", "Education", 30.0), new Product("Desk", "Furniture", 250.0), new Product("Chair", "Furniture", 150.0) ); List orders = Arrays.asList( new Order(1, Arrays.asList(products.get(0), products.get(2)), new Date()), new Order(2, Arrays.asList(products.get(1), products.get(3), products.get(4)), new Date()), new Order(3, Arrays.asList(products.get(0), products.get(1)), new Date()), new Order(4, Arrays.asList(products.get(2), products.get(4)), new Date()), new Order(5, Arrays.asList(products.get(3)), new Date()) ); // 数据分析 // 1. 计算总销售额 double totalSales = orders.stream() .mapToDouble(Order::getTotalAmount) .sum(); System.out.println("总销售额: $" + totalSales); // 2. 按类别统计销售额 Map salesByCategory = orders.stream() .flatMap(order -> order.getProducts().stream()) .collect(Collectors.groupingBy( Product::getCategory, Collectors.summingDouble(Product::getPrice) )); System.out.println("按类别销售额: " + salesByCategory); // 3. 找出最畅销的商品 Optional<Map.Entry> bestSeller = orders.stream() .flatMap(order -> order.getProducts().stream()) .collect(Collectors.groupingBy( Function.identity(), Collectors.counting() )) .entrySet().stream() .max(Map.Entry.comparingByValue()); bestSeller.ifPresent(entry -> System.out.println("最畅销商品: " + entry.getKey().getName() + ", 销售数量: " + entry.getValue()) ); // 4. 计算平均订单价值 double averageOrderValue = orders.stream() .mapToDouble(Order::getTotalAmount) .average() .orElse(0.0); System.out.println("平均订单价值: $" + averageOrderValue); // 5. 找出包含电子产品的订单 List electronicsOrders = orders.stream() .filter(order -> order.getProducts().stream() .anyMatch(p -> "Electronics".equals(p.getCategory()))) .collect(Collectors.toList()); System.out.println("包含电子产品的订单数: " + electronicsOrders.size()); } }
最佳实践与性能考量
使用Stream API时,遵循最佳实践可以确保代码既高效又可维护。
性能优化建议
- 优先使用基本类型流(IntStream, LongStream, DoubleStream)避免装箱开销
- 对于小型集合,顺序流通常比并行流更快
- 避免在流操作中使用synchronized块或方法
- 谨慎使用有状态的操作(如sorted, distinct),它们可能需要缓存所有元素
代码可读性建议
- 使用方法引用代替简单的Lambda表达式提高可读性
- 将复杂的流操作分解为多个步骤或提取为方法
- 避免在流操作中编写过于复杂的逻辑
- 为流操作添加适当的注释,说明每个步骤的意图
调试技巧
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 使用peek()方法调试流 List result = numbers.stream() .filter(n -> { System.out.println("过滤: " + n); return n % 2 == 0; }) .map(n -> { System.out.println("映射: " + n); return n * n; }) .peek(n -> System.out.println("处理后: " + n)) // 调试操作 .collect(Collectors.toList()); System.out.println("最终结果: " + result);
异常处理
List numberStrings = Arrays.asList("1", "2", "3", "four", "5"); // 处理可能抛出异常的流操作 List numbers = numberStrings.stream() .flatMap(s -> { try { return Stream.of(Integer.parseInt(s)); } catch (NumberFormatException e) { System.err.println("无法解析数字: " + s); return Stream.empty(); // 返回空流而不是抛出异常 } }) .collect(Collectors.toList()); System.out.println("解析后的数字: " + numbers);