1. 问题:为什么常规 Stream 并非总是高效
当你在 Java 中处理数字集合并使用常规流(Stream<Integer>、Stream<Double>)时,底层会发生将原始值“装箱”(boxing)与“拆箱”(unboxing)为包装对象(Integer、Double 等)的过程。这很方便,但并不总是高效:
- Boxing — 将原始类型(int)转换为对象(Integer)。
- Unboxing — 相反的操作:从对象取出原始值。
问题:
Boxing/unboxing 会引入额外操作和内存开销。在程序的热点(高频调用)区域,这可能导致明显的性能下降,尤其是在处理大型数字数组时。
示例:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().map(x -> x * 2).reduce(0, Integer::sum);
这里每个数字都是 Integer 对象,而不是原始类型 int。
2. 原始类型流:IntStream, LongStream, DoubleStream
为避免多余的 boxing/unboxing,Java 提供了原始类型流:
- IntStream — 用于 int
- LongStream — 用于 long
- DoubleStream — 用于 double
它们只处理原始类型,不会创建多余的包装对象。
如何创建原始类型流?
来自数组:
int[] arr = {1, 2, 3, 4, 5};
IntStream s = Arrays.stream(arr);
使用 range/rangeClosed:
IntStream.range(0, 10) // 0..9
IntStream.rangeClosed(1, 5) // 1..5(含)
生成随机数:
new Random().ints(5, 0, 100) // 5 个 0 到 99 的随机 int
示例:数组元素求和
int[] arr = {1, 2, 3, 4, 5};
int sum = Arrays.stream(arr).sum(); // 15
3. 在 Stream<T> 与原始类型流之间转换
有时你拿到的是常规流,有时是原始类型流。用于相互转换的方法如下:
- mapToInt、mapToLong、mapToDouble — 将常规流转换为原始类型流。
- boxed() — 将原始类型流转回对象流。
示例:
List<String> words = List.of("Java", "Stream", "API");
IntStream lengths = words.stream().mapToInt(String::length);
lengths.forEach(System.out::println); // 4 6 3
反向:
IntStream ints = IntStream.range(1, 5);
Stream<Integer> boxed = ints.boxed();
注意:
boxed() 是逆向操作,它会再次创建包装对象(Integer、Double 等)。如果你需要最大化性能——请在“热点”代码中尽量避免使用它。
4. 求和与汇总:sum, average, min, max, summaryStatistics
原始类型流提供了便捷的聚合方法:
- sum() — 所有元素之和
- average() — 平均值(返回 OptionalDouble)
- min()、max() — 最小值与最大值(OptionalInt、OptionalLong、OptionalDouble)
- summaryStatistics() — 返回包含完整统计信息的对象(总和、平均值、最小值、最大值、数量)
示例:
int[] arr = {1, 2, 3, 4, 5};
IntSummaryStatistics stats = Arrays.stream(arr).summaryStatistics();
System.out.println(stats.getSum()); // 15
System.out.println(stats.getAverage()); // 3.0
System.out.println(stats.getMin()); // 1
System.out.println(stats.getMax()); // 5
System.out.println(stats.getCount()); // 5
与 Collectors 对比:
对于常规流可以使用 Collectors.summarizingInt,但通常比原始类型流的方法更低效:
List<Integer> nums = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = nums.stream().collect(Collectors.summarizingInt(x -> x));
5. 避免自动装箱:何时重要以及如何测量
哪些场景更关键?
- 在循环和流中处理大量数字(例如数组处理、统计、数学运算、数据解析)。
- 在“热点”区域——被频繁调用且影响性能的代码。
为什么重要?
- 每次装箱都会创建一个新的包装对象(Integer、Double 等)。
- 这会增加垃圾回收(GC)的压力。
- 在微基准测试中,差异可能达到数倍!
如何衡量?
可以使用框架 JMH(Java Microbenchmark Harness)来进行公平的性能对比,评估带装箱与不带装箱代码的性能。
示例(伪代码):
@Benchmark
public int sumIntStream() {
return IntStream.range(0, 1_000_000).sum();
}
@Benchmark
public int sumStreamInteger() {
return Stream.iterate(0, n -> n + 1).limit(1_000_000).reduce(0, Integer::sum);
}
第二个变体会更慢,因为会不断创建 Integer 对象。
结论:
如果你关注性能——请使用原始类型流!
6. 何时原始类型流确实有用,何时不必复杂化
以下情况建议使用原始类型流:
- 你在处理大型数字数组/集合。
- 你需要快速计算总和、平均值、最小值、最大值。
- 你的代码对每一毫秒都很敏感(例如实时数据处理)。
以下情况无需刻意优化:
- 集合很小(只有几十或更少的元素)。
- 在类型间转换导致代码过于复杂。
- 性能并不关键(例如处理用户输入)。
示例:
List<Integer> smallList = List.of(1, 2, 3);
int sum = smallList.stream().mapToInt(x -> x).sum(); // 可以,不过普通的 reduce 也行
建议:不要为了微小的优化把代码变成“丛林”。在确实值得的地方使用原始类型流。
7. OptionalInt, OptionalDouble:安全地获取结果
原始类型流的 min()、max()、average() 等方法返回的不是直接的数值,而是包装类型——OptionalInt、OptionalDouble 等。这样可以安全地处理空流(例如数组为空时)。
示例:
int[] arr = {};
OptionalInt min = Arrays.stream(arr).min();
if (min.isPresent()) {
System.out.println("最小值: " + min.getAsInt());
} else {
System.out.println("数组为空!");
}
与常规 Optional 的对比:
- OptionalInt — 用于 int
- OptionalDouble — 用于 double
- OptionalLong — 用于 long
为什么不直接返回 0?
因为 0 可能是有效值,而空流是另一种情况。
average 示例:
double[] arr = {};
OptionalDouble avg = Arrays.stream(arr).average();
double result = avg.orElse(Double.NaN); // 若为空 — 返回 NaN
8. 实践:原始类型流的使用示例
示例 1:1 到 1000 的平方和
int sum = IntStream.rangeClosed(1, 1000)
.map(x -> x * x)
.sum();
System.out.println(sum);
示例 2:筛选并统计偶数
int[] arr = {1, 2, 3, 4, 5, 6};
long count = Arrays.stream(arr)
.filter(x -> x % 2 == 0)
.count();
System.out.println("偶数个数: " + count);
示例 3:与常规 Stream<Integer> 的对比
List<Integer> list = IntStream.range(0, 1_000_000)
.boxed()
.collect(Collectors.toList());
long t1 = System.currentTimeMillis();
int sum1 = list.stream().mapToInt(x -> x).sum();
long t2 = System.currentTimeMillis();
System.out.println("Stream<Integer>: " + (t2 - t1) + " ms");
t1 = System.currentTimeMillis();
int sum2 = IntStream.range(0, 1_000_000).sum();
t2 = System.currentTimeMillis();
System.out.println("IntStream: " + (t2 - t1) + " ms");
在大数据量下差异会很明显。
9. 使用原始类型流的常见错误
错误 1:在类型转换时忽略了装箱开销。
如果你使用了 boxed(),请记住它会重新创建包装对象。非必要时不要使用。
错误 2:用原始类型流处理对象。
IntStream 只适用于 int,不适用于对象。若需要处理对象——请使用常规 Stream<T>。
错误 3:忽视 OptionalInt/OptionalDouble。
调用 min()、max()、average() 时——务必先检查结果是否存在(isPresent()),否则会抛出异常。
错误 4:在 Stream<T> 与 IntStream 之间进行过于复杂的转换。
如果因为频繁的 mapToInt() → boxed() → mapToDouble() 导致代码难以阅读——也许应该简化逻辑。
错误 5:指望在小集合上获得“魔法般”的加速。
对于小集合,Stream 与 IntStream 的差异很小。不要为微不足道的收益而让代码变复杂。
GO TO FULL VERSION