如何生成随机 boolean 值或双值 enum 的 Stream?
优先使用 Stream.generate:需要 Stream<Boolean> 时,传入 rng::nextBoolean;需要双值枚举时,就在生成函数中把 boolean 转成枚举。题目中的写法可以直接使用,直接生成枚举则能省去中间的装箱、拆箱和一个 map 阶段。
哪种方式最快,还要结合随机数生成器、JDK 和下游计算实测,代码短并不代表运行更快。
直接生成需要的类型
示例使用 Java 17 起提供的 RandomGenerator:
import java.util.random.RandomGenerator;
import java.util.stream.Stream;
enum CoinFace { HEADS, TAILS }
RandomGenerator rng = RandomGenerator.getDefault();
Stream<CoinFace> coinFlips = Stream.generate(
() -> rng.nextBoolean() ? CoinFace.HEADS : CoinFace.TAILS
);
每生成一个元素,生成函数就调用一次 nextBoolean(),返回对应的枚举常量。每次抛硬币都会复用已有的枚举常量,不会创建新的 CoinFace 对象。
如果下游需要 Boolean,可以写成:
Stream<Boolean> booleans = Stream.generate(rng::nextBoolean);
nextBoolean() 返回基本类型 boolean,这里会自动装箱为 Boolean。装箱时会复用 Boolean.TRUE 和 Boolean.FALSE,不会为每个元素分配新对象。
这两种流都是无限、惰性生成的,直到终端操作执行时才会生成元素。实际计算时,需要限制数量,或者使用能够短路的终端操作:
coinFlips
.limit(20)
.forEach(System.out::println);
题目中的写法有什么区别
原来的管道:
Stream<CoinFace> coinFlips =
Stream.generate(rng::nextBoolean)
.map(b -> b ? CoinFace.HEADS : CoinFace.TAILS);
涉及以下类型转换:
boolean → Boolean → boolean → CoinFace
其中,装箱发生在 Stream.generate 中,拆箱发生在三元表达式的判断条件处。
把转换合并进生成函数后,过程变为:
boolean → CoinFace
这样写更直接,也减少了一个管道阶段。不过,JIT 编译器可能消除部分中间开销,实际能提速多少仍需测量。
原表达式中的方法引用:
RandomGenerator.getDefault()::nextBoolean
在创建时只会调用一次 getDefault(),后续生成元素时都使用同一个生成器。把生成器保存为变量,主要是为了方便复用和配置。
也可以用 IntStream 表示硬币结果
如果后续计算只需要区分两种结果,可以用 0 和 1 表示,让管道继续使用基本类型:
import java.util.stream.IntStream;
IntStream flips = rng.ints(0, 2);
ints(0, 2) 的范围包含 0,不包含 2,所以每个元素只能是 0 或 1。例如,约定 1 表示正面,就可以直接统计正面次数:
long heads = rng.ints(0, 2)
.limit(1_000_000_000L)
.filter(value -> value == 1)
.count();
需要枚举时再转换:
Stream<CoinFace> coinFlips = rng.ints(0, 2)
.mapToObj(value ->
value == 1 ? CoinFace.HEADS : CoinFace.TAILS
);
这种写法可以让后续数值运算留在 IntStream 中。如果最终仍要转换成枚举,它未必比直接调用 nextBoolean() 更快。
随机性和性能需要分别考虑
nextBoolean() 的设计语义是让 true 和 false 近似等概率出现,适合模拟普通的伪随机抛硬币。映射成 HEADS、TAILS 不会改变这一分布。有限次实验中,两种结果出现的次数有差异是正常现象。
对于十亿次操作,应测量完整计算过程的耗时。题目中的管道除了生成随机值,还会更新游程状态、进行 Integer 装箱,并逐次构造 RunningTotal。这些步骤都可能影响耗时,只优化流的源头,收益未必明显。
如果最关心吞吐量,可以用 JMH 比较直接生成枚举、IntStream 和普通循环,比较时保持随机数算法、样本量和下游计算一致。普通循环可以直接更新基本类型变量,适合作为性能基线。
用于游程计算时的注意事项
题目中的游程统计依赖硬币结果的处理次序,应保持顺序执行。Stream.generate 创建的是顺序、无序流,这里的“无序”表示没有遭遇顺序保证。不要为了提速直接添加 .parallel(),也不要假定任意 RandomGenerator 实现都能安全地被多个线程共享。
达到 .limit(...) 指定的次数时,最后一段游程可能还没有满足 heads > tails。当前 gatherer 会丢弃这段未完成的数据,采用的估算方法需要与这一统计口径保持一致。
对于现有的 Gatherer<CoinFace, ?, Integer>,直接生成 CoinFace 最容易接入。如果准备同时调整下游计算,再考虑全程使用基本类型。
备注:内容仅供参考。
