作者: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);

