讲讲 Java中的特化数值流(IntStream, LongStream, DoubleStream)?
在 Java 8 中引入的 Stream API 极大地方便了集合数据的处理。然而,Java 的泛型是基于对象的,这意味着像 Stream<Integer> 这样的流,在操作过程中会产生大量的自动装箱(Boxing)和拆箱(Unboxing)开销,这在处理大量数值时会导致严重的性能问题。
为了解决这个问题,Java 引入了特化数值流(Primitive Streams):IntStream、LongStream 和 DoubleStream。它们分别专门用于处理原始类型 int、long 和 double,从而避免了装箱/拆箱的开销。
一、 为什么需要特化数值流?
假设我们要对一个数字列表求和:
java
// 使用普通的 Stream<Integer>
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum); // 背后隐藏着反复的装箱和拆箱
在上面的代码中,每一次加法运算,Java 都需要把 Integer 拆箱成 int,相加后,再把结果装箱成 Integer。
如果改用 IntStream:
java
int sum = numbers.stream()
.mapToInt(Integer::intValue) // 转换为 IntStream
.sum(); // 直接对原始类型 int 求和,无装箱拆箱,极其高效
二、 三大特化数值流介绍
Java 只提供了针对三种最常用数值类型的特化流:
IntStream:对应int类型。LongStream:对应long类型。DoubleStream:对应double类型。
注:对于 float、short、byte、char,通常可以使用 IntStream 来代替处理。
三、 如何创建数值流?
创建特化数值流的方法非常多样:
1. 从对象流转换(最常用)
通过 mapToInt、mapToLong、mapToDouble 将普通流转换为数值流。
java
List<User> users = Arrays.asList(new User("张三", 18), new User("李四", 20));
IntStream ageStream = users.stream().mapToInt(User::getAge);
2. 直接通过数值创建
java
IntStream s1 = IntStream.of(1, 2, 3, 4, 5);
DoubleStream s2 = DoubleStream.of(1.1, 2.2, 3.3);
3. 数值范围(Range)
这是 IntStream 和 LongStream 特有的方法,非常适合替代传统的 for 循环。
java
// range:左闭右开 [1, 100),不包含 100
IntStream range = IntStream.range(1, 100);
// rangeClosed:双闭区间 [1, 100],包含 100
IntStream rangeClosed = IntStream.rangeClosed(1, 100);
4. 从数组创建
java
int[] intArray = {1, 2, 3};
IntStream arrayStream = Arrays.stream(intArray);
四、 特化流的专属操作(普通流没有的)
特化数值流不仅免去了装箱开销,还提供了一些普通流不具备的便捷聚合方法:
sum():求和。如果流为空,返回0。average():求平均值。返回OptionalDouble(因为空流没有平均值)。max()/min():求最大/最小值。返回OptionalInt/OptionalLong/OptionalDouble。summaryStatistics():终极武器。一次性获取统计信息(个数、总和、最大值、最小值、平均值)。
示例:神奇的 summaryStatistics
java
IntStream ages = IntStream.of(18, 20, 25, 30, 45);
IntSummaryStatistics stats = ages.summaryStatistics();
System.out.println("最大年龄: " + stats.getMax());
System.out.println("最小年龄: " + stats.getMin());
System.out.println("平均年龄: " + stats.getAverage());
System.out.println("总人数: " + stats.getCount());
System.out.println("年龄总和: " + stats.getSum());
五、 类型转换:数值流与对象流的互转
在实际开发中,我们经常需要在数值流和对象流(如 Stream<Integer>)之间来回转换。
1. 数值流 -> 对象流 (装箱)
- 使用
boxed()方法:javaIntStream intStream = IntStream.of(1, 2, 3); Stream<Integer> stream = intStream.boxed(); // 转为 Stream<Integer> - 使用
mapToObj()进行自定义对象包装:javaIntStream.rangeClosed(1, 5) .mapToObj(i -> new User("User" + i, 18)) // 包装成 User 对象流 .forEach(System.out::println);
2. 数值流之间的转换
java
IntStream intStream = IntStream.of(1, 2, 3);
DoubleStream doubleStream = intStream.mapToDouble(i -> (double) i); // int 转 double
六、 性能对比(为什么它快?)
为什么数值流性能好?
- 内存占用极小:
- 一个
int只占 4 字节。 - 一个
Integer对象在 64 位 JVM 上通常占用 16 到 24 字节,并且由于是对象,它还包含对象头信息和内存对齐开销。
- 一个
- CPU 缓存友好:
- 原始类型的数组在内存中是连续存储的(Cache-line 友好)。
- 对象数组(如
Integer[])存储的是引用,这些引用指向堆内存中散落的各个对象,容易导致 CPU 缓存失效(Cache Miss)。
七、 总结与最佳实践
- 优先使用特化流:只要处理的是基本数值类型(
int,long,double),就应该毫不犹豫地选择IntStream,LongStream,DoubleStream。 - 替代
for-i循环:如果你需要写一个指定次数的循环,用IntStream.range(0, n)会比传统的for(int i=0; i<n; i++)更优雅,且易于并行化(.parallel())。 - 注意
Optional返回值:特化流的min,max,average返回的是OptionalInt等特化 Optional,使用时可以通过.orElse(defaultValue)安全地获取值。