1. 统计元素:count()
当你处理数字或对象的集合时,几乎总会遇到需要统计某些内容的任务:元素数量、总和、平均值、最大值或最小值。例如,公司员工数量、所有工资的总和、学生的平均年龄、最贵的产品、最大订单。有了 Stream API,一切变得更简单也更具表现力。
工作原理
方法 count() 是 Stream API 的终止操作,用于返回流中的元素数量。
long count = employees.stream().count();
如果只需要统计满足条件的元素——请使用过滤器 filter(...):
long richCount = employees.stream()
.filter(e -> e.getSalary() > 100_000)
.count();
我们应用中的示例:
假设有一个产品列表:
List<Product> products = List.of(
new Product("Milk", 80),
new Product("Cheese", 250),
new Product("Bread", 40)
);
统计价格高于 100 的产品数量:
long expensiveCount = products.stream()
.filter(p -> p.getPrice() > 100)
.count();
System.out.println("昂贵的产品数量: " + expensiveCount);
什么是 Optional
在 Java 中有一个特殊的容器——Optional。它是一个包装对象,可能包含一个值,也可能为空。当结果可能不存在且需要显式表达时,就会使用它。例如,当集合为空时,方法 min()、max() 和 average() 可能返回空结果。此时不返回 null,而是返回 Optional。
Optional 的主要方法
- isPresent() / isEmpty() — 检查是否存在值。
- orElse(值) — 返回当前值;若为空则返回默认值。
- orElseThrow() — 若为空则抛出异常。
- ifPresent(...) — 仅在存在值时执行操作。
示例:
OptionalInt min = products.stream()
.mapToInt(Product::getPrice)
.min();
if (min.isPresent()) {
System.out.println("最低价格: " + min.getAsInt());
} else {
System.out.println("列表为空");
}
或更简洁地写:
int minValue = min.orElse(-1);
System.out.println("最低价格: " + minValue);
这种方式可以避免 NullPointerException,让代码更安全。
2. 数值数据的总和、平均值、最小值和最大值
原始类型流:IntStream、DoubleStream、LongStream
对于数值数据,Stream API 提供了专门的流:IntStream、DoubleStream、LongStream。它们可以快速、便捷地计算总和、平均值、最小值和最大值。
转换为数值流
若要从对象流得到数值流,请使用 mapToInt、mapToDouble、mapToLong 方法。
int sum = products.stream()
.mapToInt(Product::getPrice)
.sum();
System.out.println("价格总和: " + sum);
方法
- sum() — 所有元素之和。
- average() — 算术平均值(返回 OptionalDouble)。
- min()、max() — 最小值与最大值(返回 OptionalInt/OptionalDouble/OptionalLong)。
示例:
// 所有价格之和
int total = products.stream()
.mapToInt(Product::getPrice)
.sum();
// 平均价格
OptionalDouble avg = products.stream()
.mapToInt(Product::getPrice)
.average();
// 最低价与最高价
OptionalInt min = products.stream()
.mapToInt(Product::getPrice)
.min();
OptionalInt max = products.stream()
.mapToInt(Product::getPrice)
.max();
如何从 Optional 中取值
double average = avg.orElse(0.0); // 如果集合为空,则为 0.0
int minValue = min.orElse(-1); // 如果集合为空,则为 -1
int maxValue = max.orElse(-1); // 如果集合为空,则为 -1
程序员小笑话:空列表并不是错误,只是你没有数据而已。但如果你在空的 Optional 上调用 getAsInt()——就会捕获到 NoSuchElementException。请始终使用 orElse 或 isPresent()!
3. 对象的聚合:Collectors.summingInt、averagingInt、maxBy、minBy
当你有一个对象流,并希望按某个属性进行聚合时,请使用 Collectors 类中的专用收集器。
summingInt、averagingInt
int totalSalary = employees.stream()
.collect(Collectors.summingInt(Employee::getSalary));
double avgSalary = employees.stream()
.collect(Collectors.averagingInt(Employee::getSalary));
minBy、maxBy
要根据某个字段找到最大/最小的对象:
Optional<Employee> richest = employees.stream()
.collect(Collectors.maxBy(Comparator.comparingInt(Employee::getSalary)));
Optional<Employee> poorest = employees.stream()
.collect(Collectors.minBy(Comparator.comparingInt(Employee::getSalary)));
重要:结果是 Optional,因为集合可能为空。
示例
创建类 Product:
public class Product {
private final String name;
private final int price;
public Product(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() { return name; }
public int getPrice() { return price; }
}
创建产品列表:
List<Product> products = List.of(
new Product("Milk", 80),
new Product("Cheese", 250),
new Product("Bread", 40)
);
找到最贵的产品:
Optional<Product> maxProduct = products.stream()
.collect(Collectors.maxBy(Comparator.comparingInt(Product::getPrice)));
maxProduct.ifPresent(p -> System.out.println("最贵的产品: " + p.getName()));
4. 与分组相结合
聚合方法常与分组一起使用。例如,可以按名称首字找出产品的平均价格:
Map<Character, Double> avgPriceByLetter = products.stream()
.collect(Collectors.groupingBy(
p -> p.getName().charAt(0),
Collectors.averagingInt(Product::getPrice)
));
System.out.println(avgPriceByLetter);
结果:
{Milk=80.0, 奶=250.0, 面=40.0}
5. 真实场景示例
示例 1:学生与分数
学生列表及其分数:
public class Student {
private final String name;
private final int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
}
List<Student> students = List.of(
new Student("Alisa", 90),
new Student("Bob", 75),
new Student("Basil", 100)
);
分数高于 80 的学生有多少?
long count = students.stream()
.filter(s -> s.getScore() > 80)
.count();
System.out.println("分数 > 80 的学生人数: " + count);
平均分:
double avg = students.stream()
.mapToInt(Student::getScore)
.average()
.orElse(0.0);
System.out.println("平均分: " + avg);
最优秀的学生:
students.stream()
.max(Comparator.comparingInt(Student::getScore))
.ifPresent(s -> System.out.println("最佳学生: " + s.getName()));
示例 2:唯一商品数量
long uniqueCount = products.stream()
.map(Product::getName)
.distinct()
.count();
System.out.println("不同商品的数量: " + uniqueCount);
6. 有用的细节
可视化流程:聚合方法如何工作
对象集合
│
▼
stream()
│
▼
mapToInt/Double/Long(如果需要)
│
▼
sum() / average() / min() / max() / count()
│
▼
结果(int、double、long、Optional)
处理 Optional:如何安全地取值
为什么这些方法返回 Optional?
如果集合为空(例如,你在空列表中查找最大值),会返回一个空的 Optional。此时如果直接调用 getAsInt()/get(),就会抛出异常。
正确做法:
- 使用 orElse(默认值)
- 使用 ifPresent(...),仅在存在值时执行操作
- 使用 orElseThrow(...),显式抛出异常
示例:
OptionalDouble avg = products.stream()
.mapToInt(Product::getPrice)
.average();
System.out.println("平均价格: " + avg.orElse(0.0));
何时使用原始类型流,何时使用 Collectors
- 如果只是需要对数值字段求和/平均/最小/最大——使用 mapToInt 及相应方法(sum、average、min、max)。
- 如果需要对对象进行聚合(例如找到字段最大的那个对象),使用收集器 maxBy/minBy。
- 如果需要按组统计聚合值——将 groupingBy 与聚合收集器组合使用。
7. 使用聚合方法时的常见错误
错误 №1:未处理 Optional。 新手常常忘记 min、max、average 会返回 Optional。结果是在空容器上取值时抛出 NoSuchElementException。务必使用 orElse、orElseThrow 或 ifPresent。
错误 №2:在需要 mapToInt/mapToDouble 的地方使用了普通 stream。 如果你写的是 stream().sum(),编译器会说:“没有这样的方法!”。对于总和、平均、最小与最大,需要使用原始类型流。
错误 №3:比较时类型不匹配。 当在对象中查找最大/最小时,请使用正确的比较器:Comparator.comparingInt(Employee::getSalary)。否则可能会得到编译错误或不正确的结果。
错误 №4:与 null 的对象比较。 如果集合中可能包含 null 元素,聚合操作可能抛出 NullPointerException。最好先过滤此类元素,或使用 Comparator.nullsLast(...)/nullsFirst(...)。
错误 №5:在处理空集合时,对 sum/average/min/max 的结果缺乏考虑。 如果集合为空,方法 sum() 会返回 0,而 average()/min()/max() 会返回空的 Optional。不要忘记处理这种情况。
GO TO FULL VERSION