Lambda 与 Stream API 与 Optional 与函数式编程

共 89 题
📑 题目列表 89 题
#
★★★

1. Collector 标记为 CONCURRENT 时还需要满足哪些线程安全条件,UNORDERED 特征有何作用

Collector 标记为 CONCURRENT 时还需要满足哪些线程安全条件?UNORDERED 特征有何作用?

  • Collector.CONCURRENT 特征
  • 线程安全条件
  • UNORDERED 特征

Collector 标记为 CONCURRENT 表示"结果容器可被并发修改",允许并行流在多个线程同时累加。但需满足:1)accumulator 必须线程安全(可并发调用,如 ConcurrentHashMap 的 put);2)combiner 可被抛(或返回与 accumulator 一致的结果);3)结果容器必须支持并发修改(如 ConcurrentHashMap、线程安全集合)。若违反(如用普通 HashMap 但标 CONCURRENT),并行累加会产生竞态/丢数据。UNORDERED 特征表示"结果不依赖输入顺序",允许并行流优化(跳过顺序合并、减少屏障),并行执行更快。CONCURRENT 省去合并(各线程直接累加同一容器),UNORDERED 允许无序处理提升并行度。

CONCURRENT 要求累加器线程安全(容器并发可用),UNORDERED 允许无序处理优化并行。二者联合让并行流高效且安全。

// 线程安全条件 + UNORDERED
Collector<T, ?, ConcurrentHashMap<K,V>> c = Collectors.toConcurrentMap(
    keyFn, valFn, mergeFn, ConcurrentHashMap::new);   // 容器线程安全
// 自定义 collector 标注 CONCURRENT + UNORDERED 时需保证线程安全
#
★★★

2. Collector.CONCURRENT 与 UNORDERED 特征在并行流中的线程安全要求与性能影响

Collector.CONCURRENT 与 UNORDERED 特征在并行流中的线程安全要求与性能影响是什么?

  • CONCURRENT 线程安全
  • UNORDERED 性能
  • 并行流优化

在并行流中,CONCURRENT 特征允许各线程直接累加到同一结果容器(无需合并),提高性能,但要求容器与 accumulator 线程安全(如 ConcurrentHashMap),否则并行累加竞态。UNORDERED 特征表示结果不依赖顺序,允许并行流跳过顺序合并与有序屏障,减少同步开销,提升并行度。二者结合:CONCURRENT + UNORDERED 的 collector 可"无合并、并发累加、无序处理",并行流最高效。线程安全要求:容器支持并发写、accumulator 无副作用。性能影响:CONCURRENT 省合并、UNORDERED 省顺序同步,均提升并行性能,但前提是满足线程安全与无序语义。

CONCURRENT 省合并(并发累加同一容器)、UNORDERED 省顺序同步,提升并行性能,前提是容器线程安全、结果无序可接受。

// CONCURRENT + UNORDERED:无合并、并发累加
Map<String, Long> m = list.parallelStream().collect(
    Collectors.toConcurrentMap(k -> k, v -> 1L, Long::sum));  // 线程安全 + 无序
#
★★★

3. Gatherers.mapConcurrent 如何限制并发度和保持遇见顺序,映射函数阻塞时有什么边界

Gatherers.mapConcurrent 如何限制并发度和保持遇见顺序?映射函数阻塞时有什么边界?

  • Gatherers.mapConcurrent
  • 并发度限制
  • 遇见顺序

Gatherers.mapConcurrent(JDK 22+,Stream Gatherers)是一个有界并发映射操作:用固定并发度(parallelism)映射元素,同时保持遇见顺序(encounter order)。它内部用有界队列管理并发:最多 parallelism 个映射函数同时执行,结果按输入顺序输出(即使映射函数完成顺序不同)。边界:映射函数阻塞时,若并发度固定,前一个元素阻塞会阻塞后续(因为要保持顺序,前面的结果未就绪,后面的结果不能输出),导致吞吐受限;并发度越大可容纳更多阻塞,但消耗资源。因此 mapConcurrent 适合"并发 I/O 但保持顺序"的场景,映射函数阻塞时并发度限制总吞吐。

mapConcurrent 用固定并发度并行映射并保持遇见顺序,映射函数阻塞时受并发度限制(有序输出使阻塞元素卡住后续)。

// 并发度 4,保持顺序
List<String> results = urls.stream()
    .gather(Gatherers.mapConcurrent(4, url -> fetch(url)))  // 并发 4,有序输出
    .toList();
#
★★★

4. Lambda 捕获的局部变量为何必须有效 final,捕获可变对象引用后又有哪些并发风险

Lambda 捕获的局部变量为何必须有效 final?捕获可变对象引用后又有哪些并发风险?

  • 有效 final 变量
  • 捕获变量
  • 可变对象并发风险

Lambda 捕获的局部变量必须有效 final(effectively final,即被捕获后不再赋值),因为 Lambda 可能被延迟执行(在变量作用域结束后),若捕获可变变量,其值无法确定;有效 final 保证捕获的值稳定。捕获可变对象引用(final 引用但对象可变)的并发风险:虽然引用是 final,但被引用的对象可被修改,若 Lambda 在并发执行或异步执行,可能无法看到其他线程的修改(内存可见性)或产生竞态(若 Lambda 修改对象)。因此捕获可变对象引用时需注意线程安全(同步/不可变对象/volatile)。引用 final 保证"引用不变",但不保证对象状态安全。

有效 final 是捕获的保证(值稳定),但捕获可变对象引用时对象可变仍有并发风险,需线程安全处理。

int x = 10;          // 有效 final
Runnable r = () -> System.out.println(x);   // 捕获
// x = 20;  // 编译错误:x 不再有效 final
List<String> list = new ArrayList<>();   // final 引用,对象可变
Runnable r2 = () -> list.add("a");       // 捕获可变对象,并发需同步
#
★★★

5. Spliterator 在并行流(Parallel Stream)中的拆分策略与工作线程调度

Spliterator 在并行流(Parallel Stream)中的拆分策略与工作线程调度是什么?

  • Spliterator 拆分
  • 并行流工作线程
  • ForkJoinPool 调度

并行流利用 Spliterator 的 trySplit 递归拆分数据源成多个子块(拆分策略),每个子块交给一个工作线程处理。拆分策略:trySplit 把数据分成左右两部分,递归直到达到拆分粒度(estimateSize 阈值),形成分治树。工作线程调度:并行流用 ForkJoinPool.commonPool(默认),Spliterator 拆分的子任务通过 ForkJoinTask 提交,由 commonPool 的工作线程执行(work-stealing 调度,空闲线程窃取其他线程的任务)。拆分平衡与 Spliterator 特征(SIZED、SUBSIZED)决定并行度与负载均衡。工作线程数 = CPU 核数(commonPool 默认)。

并行流用 Spliterator.trySplit 递归拆分数据源,ForkJoinPool.commonPool 用 work-stealing 调度子任务,拆分平衡决定并行度。

// 并行流:Spliterator 拆分 + ForkJoinPool.commonPool
List<Integer> list = ...;
list.parallelStream().map(x -> x * 2).forEach(System.out::println);
// 内部:Spliterator.trySplit 拆分,ForkJoinTask 提交 commonPool
#
★★★

6. Stream 与集合转换(toList/toUnmodifiableList)的演进

Stream 与集合转换(toList/toUnmodifiableList)的演进是什么?

  • toList(JDK 16+)
  • toUnmodifiableList
  • 转换演进

Stream 转集合的演进:早期用 collect(Collectors.toList())(可变 List);JDK 10 引入 Collectors.toUnmodifiableList()(不可变 List);JDK 16 引入 Stream.toList()(返回不可变 List,更简洁,且不保证有序性可选)。演进:toList()(JDK 16)更简洁,返回不可变 List(不允许 null);toUnmodifiableList() 也是不可变;Collectors.toList() 返回可变 List。选择:需要不可变结果用 toList()/toUnmodifiableList(),需要可变用 Collectors.toList()。toList() 是 JDK 16+ 推荐,避免了 Collectors 的样板。

Stream.toList()(JDK 16)返回不可变 List,更简洁;toUnmodifiableList 不可变;Collectors.toList() 可变。转换演进趋向简洁不可变。

// JDK 16+:toList 返回不可变 List
List<String> l1 = list.stream().filter(x -> x != null).toList();
// 不可变
List<String> l2 = list.stream().collect(Collectors.toUnmodifiableList());
// 可变
List<String> l3 = list.stream().collect(Collectors.toList());
#
★★★

7. Stream 并行流(parallelStream)的线程模型与坑点

Stream 并行流(parallelStream)的线程模型与坑点是什么?

  • 并行流线程模型
  • ForkJoinPool.commonPool
  • 坑点

并行流(parallelStream)用 ForkJoinPool.commonPool 的线程执行(默认线程数 = CPU 核数 - 1),每个线程处理 Spliterator 拆分的子块。坑点:1)commonPool 共享:所有并行流共用 commonPool,阻塞 I/O 会占满线程,影响其他并行任务;2)阻塞操作:在并行流中做阻塞 I/O(如网络调用)会占用 commonPool 线程,导致并行度下降、其他任务饥饿;3)共享可变状态:并行流中修改共享可变对象(如普通 List)会竞态,需线程安全集合或避免共享状态;4)顺序依赖:有状态操作(sorted、distinct、limit)在并行下需屏障,性能下降;5)结果顺序:并行流不保证遇见顺序(除非 ordered)。避免在并行流中做阻塞 I/O 与共享可变状态。

并行流用共享 commonPool,阻塞 I/O 占线程、共享可变状态竞态、有状态操作屏障,是主要坑点。

// 坑点:阻塞 I/O 占 commonPool
list.parallelStream().forEach(x -> remoteCall(x));   // 阻塞占线程,影响其他并行任务
// 坑点:共享可变状态
List<String> shared = new ArrayList<>();
list.parallelStream().forEach(x -> shared.add(x));   // 竞态!
// 正确:用线程安全集合或 collect
List<String> res = list.parallelStream().map(x -> x + "").toList();
#
★★★

8. 即使使用线程安全集合仍有何风险

即使使用线程安全集合(在并行流中)仍有何风险?

  • 线程安全集合
  • 并行流副作用
  • 残留风险

即使在并行流中使用线程安全集合(如 ConcurrentHashMap、CopyOnWriteArrayList)作为副作用容器,仍有风险:1)副作用隐藏:线程安全集合消除竞态,但"流中使用副作用做操作"本身是反模式(违背纯函数、难并行、难排查),order 不保证;2)性能:并发集合的同步/复制开销大,并行流中大量写入反而慢;3)顺序:结果顺序不保证(线程安全集合不保序);4)状态依赖:若操作依赖流的顺序元素(有序累加),并行下结果可能错误;5)迭代不保证反映所有元素。正确做法:用纯函数(map/filter/reduce)或 collect 归约,避免副作用收集。

线程安全集合消除竞态但副作用反模式、性能开销、顺序不保证仍存在,应避免副作用,用纯函数/collect。

// 反模式:副作用收集(即使线程安全)
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
list.parallelStream().forEach(x -> map.put(x, x.length()));   // 副作用反模式
// 正确:用 collect 归约
Map<String, Integer> m = list.parallelStream().collect(Collectors.toConcurrentMap(x -> x, x -> x.length()));
#
★★★

9. 序列化 Lambda 为什么依赖 SerializedLambda 与实现细节,持久化任务定义时应采用什么替代方案

序列化 Lambda 为什么依赖 SerializedLambda 与实现细节?持久化任务定义时应采用什么替代方案?

  • 序列化 Lambda
  • SerializedLambda
  • 持久化任务替代

序列化 Lambda 依赖 SerializedLambda(JDK 8+):当可序列化 Lambda(Serializable 函数式接口)被序列化时,不是序列化 Lambda 生成的类实例,而是序列化为 SerializedLambda 对象(记录捕获参数、实现类、实现方法名、签名等)。反序列化时由 writeReplace 重新构造 Lambda。实现细节:SerializedLambda 保存"捕获的实参 + 函数式接口方法 + 实现类/方法",依赖 JVM 能重建 Lambda。风险:序列化 Lambda 脆弱(依赖类定义、签名、捕获参数),跨版本/跨 JVM 不稳定。持久化任务定义替代方案:用显式任务描述(任务名、参数、版本),而非序列化 Lambda;用 JSON/模板描述任务;用可序列化的任务对象(含任务类型与参数)。避免序列化 Lambda 做持久化。

Lambda 序列化走 SerializedLambda(记录捕获参数与实现方法),脆弱不稳定;持久化任务用显式任务描述(任务名+参数)而非序列化 Lambda。

// Lambda 序列化:Serializable 函数式接口
@FunctionalInterface interface Task extends Serializable { void run(); }
Task t = (Task) () -> System.out.println("hi");
// 序列化时用 SerializedLambda(writeReplace)
// 持久化任务替代:任务名 + 参数
{"task":"print","arg":"hi"}
#
★★★

10. @FunctionalInterface 的真实价值

@FunctionalInterface 的真实价值是什么?

  • @FunctionalInterface 注解
  • 编译期校验
  • 契约

@FunctionalInterface 用于标注"函数式接口"(只有一个抽象方法的接口),真实价值:1)编译期校验:编译器检查接口确实只有一个抽象方法,若有多个抽象方法则报错,防止接口被破坏为多抽象方法(Lambda 无法使用);2)文档/契约:明确标注"该接口用于 Lambda 方法引用",清晰的语义;3)可读性:帮助开发者理解接口是函数式接口。它不是必需的(只有单抽象方法即可用于 Lambda),但标注后编译器强制保证"单抽象方法"约束,防止未来加方法破坏 Lambda 兼容性。价值在于"编译期约束 + 文档契约"。

@FunctionalInterface 的价值是编译期强制单抽象方法约束 + 文档契约,防止接口被破坏,保护 Lambda 兼容性。

@FunctionalInterface
interface MyFunc { int apply(int x); }   // 单抽象方法
// 若再加一个抽象方法 -> 编译错误
// 不加注解也能用 Lambda,但注解提供编译期保证
MyFunc f = x -> x * 2;
#
★★★

11. @FunctionalInterface 的语义化命名

@FunctionalInterface 的语义化命名是什么?

  • 函数式接口命名
  • 语义化命名
  • 标准函数式接口

@FunctionalInterface 的语义化命名指"函数式接口应使用清晰、语义化的命名来表达其用途",如标准库的 Function(转换)、Consumer(消费)、Supplier(供给)、Predicate(断言)、Runnable(运行)、Callable(调用返回结果)等。命名反映抽象方法的功能语义(Function 的 apply、Consumer 的 accept、Supplier 的 get、Predicate 的 test)。自定义函数式接口应遵循语义化命名:命名体现"做什么"(如 Transformer、Validator、Factory),避免模糊命名。语义化命名让接口用途自明,提高可读性与可维护性。标准函数式接口覆盖常见场景,优先复用。

函数式接口命名应语义化(Function/Consumer/Supplier/Predicate 体现用途),优先复用标准接口,自定义命名体现功能。

// 标准函数式接口语义化命名
Function<String, Integer> f = s -> s.length();   // 转换
Consumer<String> c = s -> System.out.println(s); // 消费
Supplier<String> s = () -> "x";                  // 供给
Predicate<String> p = s -> s.length() > 0;       // 断言
// 自定义:语义化命名
@FunctionalInterface interface IdGenerator { String next(); }
#
★★★

12. BiFunction/BiPredicate 的限制与替代方案

BiFunction/BiPredicate 的限制与替代方案是什么?

  • BiFunction/BiPredicate
  • 参数限制
  • 替代方案

BiFunction<T,U,R> 接收两个参数,BiPredicate<T,U> 接收两个参数返回 boolean。限制:最多两个参数(BiFunction 只有 two-arg),没有"三参数"的标准函数式接口(无 TriFunction)。若需要三个及以上参数,需自定义函数式接口或用"参数对象"(把多个参数封装成对象)或柯里化(返回函数的函数)。替代方案:1)自定义多参数函数式接口(@FunctionalInterface TriFunction<T,U,V,R>);2)用参数对象(把多个参数封装成 DTO);3)柯里化/部分应用(第一个参数返回函数);4)用 BiFunction 组合(嵌套)。BiFunction 限制是标准接口只到两参,多参需自定义或封装。

BiFunction/BiPredicate 最多两参,多参需自定义函数式接口、参数对象或柯里化。

// BiFunction:两参
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
// 三参:自定义
@FunctionalInterface interface TriFunction<A,B,C,R> { R apply(A a, B b, C c); }
TriFunction<Integer,Integer,Integer,Integer> add3 = (a,b,c) -> a+b+c;
// 或用参数对象
#
★★★

13. BinaryOperator/UnaryOperator 与 Function 的关系及 IntBinaryOperator 等特化类型的性能价值

BinaryOperator/UnaryOperator 与 Function 的关系及 IntBinaryOperator 等特化类型的性能价值是什么?

  • BinaryOperator/UnaryOperator
  • Function 关系
  • 原始类型特化

BinaryOperator 是 BiFunction<T,T,T> 的特化(两个同类型参数返回同类型),UnaryOperator 是 Function<T,T> 的特化(同类型单参返回同类型),它们约束"同类型"语义,减少泛型混乱。性能价值:原始类型特化(IntBinaryOperator、IntUnaryOperator、LongSupplier 等)避免装箱/拆箱:用 int/long 基本类型直接操作,避免 Integer/Long 装箱开销,在大量数值运算中提升性能(减少对象分配与 GC)。选择:同类型运算用 BinaryOperator/UnaryOperator,数值运算用原始类型特化(IntBinaryOperator 等)避免装箱。

BinaryOperator/UnaryOperator 是 BiFunction/Function 的同类型特化,原始类型特化(IntBinaryOperator 等)避免装箱提升数值性能。

// UnaryOperator = Function<T,T>
UnaryOperator<String> upper = s -> s.toUpperCase();
// BinaryOperator = BiFunction<T,T,T>
BinaryOperator<Integer> max = Math::max;
// 原始类型特化:避免装箱
IntBinaryOperator sum = (a, b) -> a + b;   // int 直接运算,无装箱
#
★★★

14. Collectors.teeing 如何在一次遍历中组合两个下游结果,两个收集器的状态是否共享

Collectors.teeing 如何在一次遍历中组合两个下游结果?两个收集器的状态是否共享?

  • Collectors.teeing
  • 两个下游
  • 状态共享

Collectors.teeing(downstream1, downstream2, merger)(JDK 12+)在"一次遍历"中同时收集两个下游结果,最后用 merger 组合。实现:内部维护一个包含两个下游收集器的容器(双层结构),每个元素同时喂给两个 accumulator(两次累加),一次遍历完成两个收集,最后用 biFunction 合并两个结果。两个收集器的状态不共享:teeing 内部各自维护独立的下游收集器状态(各自容器),互不干扰;合并只发生在最终 merger 阶段。适用:一次遍历中同时求多个聚合(如同时求 min 与 max、求和与计数)。teeing 避免两次遍历。

teeing 一次遍历同时喂两个下游收集器,各自状态独立,最终 merger 合并,避免多次遍历。

// 一次遍历同时求 min 和 max
Record result = list.stream().collect(Collectors.teeing(
    Collectors.minBy(Integer::compareTo),
    Collectors.maxBy(Integer::compareTo),
    (min, max) -> new Record(min.orElse(0), max.orElse(0))
));
#
★★★

15. Comparator 的函数式构造(comparing/thenComparing)

Comparator 的函数式构造(comparing/thenComparing)是什么?

  • Comparator.comparing
  • thenComparing
  • 链式比较

Comparator 的函数式构造:Comparator.comparing(keyExtractor) 根据"键提取器"生成比较器(按某个字段比较),comparing(keyExtractor, keyComparator) 可指定键的比较器。thenComparing 链式添加次级比较条件:先按主键比较,相等再用次级键,实现复合排序(如先按姓名再按年龄)。comparing 返回 Comparator,可配合 reversed()(逆序)、nullsFirst/nullsLast(null 处理)。这些函数式构造让排序声明式、可读、可组合。示例:Comparator.comparing(User::name).thenComparing(User::age)。

comparing 按键生成比较器,thenComparing 链式添加次级条件,reversed/nullsFirst 组合排序,声明式可读。

Comparator<User> byName = Comparator.comparing(User::getName);
Comparator<User> byNameThenAge = Comparator.comparing(User::getName)
    .thenComparing(User::getAge);   // 先姓名后年龄
Comparator<User> byAgeDesc = Comparator.comparing(User::getAge).reversed();
// null 处理
Comparator.comparing(User::getName, Comparator.nullsFirst(String::compareTo));
#
★★★

16. Function.compose/andThen 的执行顺序

Function.compose/andThen 的执行顺序是什么?

  • compose
  • andThen
  • 执行顺序

Function.compose 与 andThen 都是组合函数,执行顺序不同:f.compose(g) 表示先执行 g 再执行 f(f(g(x)),g 先执行);f.andThen(g) 表示先执行 f 再执行 g(g(f(x)),f 先执行)。等价:f.compose(g) == f.andThen(g) 的相反顺序。compose 是"先应用参数函数再应用自身",andThen 是"先应用自身再应用参数函数"。选择:逻辑上先做哪个操作决定用 compose 还是 andThen。示例:square.compose(plus1) 表示先加 1 再平方;square.andThen(plus1) 表示先平方再加 1。

compose 先执行参数函数再自身(f(g(x))),andThen 先自身再参数函数(g(f(x))),顺序相反。

Function<Integer, Integer> sq = x -> x * x;
Function<Integer, Integer> inc = x -> x + 1;
sq.compose(inc).apply(2);   // (2+1)^2 = 9(先 inc 后 sq)
sq.andThen(inc).apply(2);   // 2^2 + 1 = 5(先 sq 后 inc)
#
★★★

17. Gatherer 的 Integrator 如何发出多个、一个或零个元素,并通过返回值请求上游短路

Gatherer 的 Integrator 如何发出多个、一个或零个元素,并通过返回值请求上游短路?

  • Gatherer.Integrator
  • 发出元素
  • 短路(downstream push 返回值)

Gatherer.Integrator(JDK 22+)的 integrate 方法接收元素、状态、downstream,处理元素并可通过 downstream.push() 发出 0/1/多个元素。发出多个:在 integrate 中循环调用 downstream.push() 多次;发出一个:push 一次;发出零个:不 push。短路:integrate 返回 boolean——返回 false 表示"请求上游短路"(停止继续处理后续元素),true 表示继续。短路机制用于提前终止(如找到目标后停止)。Integrator 是 Gatherer 的核心,负责状态化逐元素处理与输出控制。

Integrator 通过 downstream.push 发出 0/1/多个元素,返回 false 请求上游短路停止处理。

Gatherer<Integer, ?, Integer> collect = Gatherer.of(
    (state, element, downstream) -> {
        if (element > 100) return false;         // 短路:停止上游
        downstream.push(element);                // 发出一个元素
        downstream.push(element * 2);            // 可发多个
        return true;                             // 继续
    });
#
★★★

18. Gatherers.scan 与 fold 都能维护状态,它们输出中间结果和最终结果的语义有何差异

Gatherers.scan 与 fold 都能维护状态,它们输出中间结果和最终结果的语义有何差异?

  • Gatherers.scan
  • Gatherers.fold
  • 中间结果 vs 最终结果

Gatherers.scan 与 Gatherers.fold 都维护状态,但语义不同:scan(前缀扫描)输出"每个元素处理后的中间累加结果"(prefix sum 风格),输出流与输入流长度相同(每个输入元素产生一个累加结果);fold(折叠)只输出"最终结果"(一个元素),把所有元素归约为一个结果。scan 输出中间结果序列(保留每个前缀),fold 只输出最终归约。差异:scan 是"流式前缀归约"(输出中间),fold 是"整体归约"(输出最终单个)。适用:scan 用于需要每个前缀状态(如累积和、状态监测),fold 用于只需最终聚合。

scan 输出每个元素的中间累加结果(前缀扫描,长度同输入),fold 只输出最终归约结果(单个),语义不同。

// scan:输出中间前缀和
List<Integer> prefix = list.stream().gather(Gatherers.scan(() -> 0, (a, b) -> a + b)).toList();
// 输入 [1,2,3] -> [1,3,6]
// fold:输出最终累加
Gatherers.fold(() -> 0, (a, b) -> a + b);   // 输出单个最终值
#
★★★

19. IntStream 等原始类型流如何减少装箱,转换为 boxed 后在哪些操作中会重新产生对象

IntStream 等原始类型流如何减少装箱?转换为 boxed 后在哪些操作中会重新产生对象?

  • 原始类型流
  • 减少装箱
  • boxed 重新装箱

IntStream/LongStream/DoubleStream 直接操作基本类型(int/long/double),避免 Stream 的装箱(Integer 对象),减少对象分配与 GC,提升数值运算性能。boxed() 把原始流转回 Stream (重新装箱,产生 Integer 对象)。转换为 boxed 后的操作中会重新产生对象:任何需要泛型/对象的操作(collect 到 List 、toArray、map 返回对象、与 Stream 交互)都会装箱产生 Integer 对象。原始类型流保持基本类型避免装箱,但无法用于泛型(如 List 需 boxed)。boxed 后对象开销回归。

原始类型流避免装箱提升性能,boxed() 转回对象流重新装箱产生 Integer,需对象的操作才 boxed。

// 原始流:避免装箱
int sum = IntStream.range(1, 100).map(x -> x * 2).sum();   // 无装箱
// boxed 重新装箱
List<Integer> list = IntStream.range(1, 10).boxed().collect(Collectors.toList());  // 产生 Integer
// 单元素操作(sum 等)优先用原始流
#
★★★

20. JEP 485 与现有 Stream 操作(map/filter)

JEP 485 与现有 Stream 操作(map/filter)的关系是什么?

  • JEP 485 Stream Gatherers
  • 与 map/filter 关系
  • 自定义中间操作

JEP 485(Stream Gatherers,JDK 24 定稿)为 Stream 引入自定义中间操作的能力(Gatherer),是对现有中间操作(map、filter、flatMap、distinct 等)的扩展。关系:map/filter 是内置的中间操作,Gatherers 是"自定义中间操作"的通用机制——用 Gatherer 实现 map/filter 无法直接表达的状态化/窗口化/多输出操作。Gatherer 可组合(andThen)实现目录化操作。现有 map/filter 仍是常用操作,Gatherers 补充了"内置之外的定制"能力(如 mapConcurrent、windowFixed、scan、fold)。JEP 485 让 Stream 支持自定义中间操作,与内置 map/filter 互补。

JEP 485 引入 Gatherers(自定义中间操作),补充 map/filter 等内置操作无法表达的状态化/窗口化操作,二者互补。

// 内置 map/filter
list.stream().filter(x -> x > 0).map(x -> x * 2).toList();
// JEP 485:自定义中间操作
list.stream().gather(Gatherers.windowFixed(3)).toList();   // 窗口化
list.stream().gather(Gatherers.mapConcurrent(4, x -> fetch(x))).toList();
#
★★

21. IntStream.range/rangeClosed 的边界差异

IntStream.range/rangeClosed 的边界差异是什么?

  • range 开区间
  • rangeClosed 闭区间
  • 边界

IntStream.range(start, end) 生成 [start, end) 的整数流(含 start,不含 end);IntStream.rangeClosed(start, end) 生成 [start, end](含 start 和 end)。差异:range 是开区间(不含 end),rangeClosed 是闭区间(含 end)。示例:range(1, 5) 生成 1,2,3,4;rangeClosed(1, 5) 生成 1,2,3,4,5。选择:需要包含上界用 rangeClosed,不含用 range。注意:若 start > end,两者都为空流;rangeClosed(start, end) 当 start==end 生成一个元素(end),range 即使 start==end 也空(不含 end)。

range 左闭右开(不含 end),rangeClosed 闭区间(含 end),按是否需要包含上界选择。

IntStream.range(1, 5).forEach(System.out::print);        // 1234
IntStream.rangeClosed(1, 5).forEach(System.out::print);  // 12345
// range(5,5) 空;rangeClosed(5,5) 一个元素 5
#
★★

22. IntStream/LongStream/DoubleStream 特化的性能价值

IntStream/LongStream/DoubleStream 特化的性能价值是什么?

  • 原始类型流
  • 避免装箱
  • 性能价值

IntStream/LongStream/DoubleStream 是原始类型特化流,直接操作基本类型(int/long/double),性能价值:1)避免装箱/拆箱:不用 Integer/Long/Double 对象,减少对象分配与 GC 压力;2)减少内存:基本类型紧凑,无对象头;3)提升数值运算性能:大量数值计算(sum、map、filter)直接操作基本类型。相比 Stream 的装箱,原始流在数值密集场景(大数据量、循环)性能显著提升。边界:原始流无法用于泛型(如 collect 到 List 需 boxed),且缺少部分操作。合适场景:数值型数据流用原始流。

原始类型流避免装箱、内存紧凑、提升数值性能,用于数值密集场景,但需对象时 boxed。

// 原始流避免装箱
long sum = IntStream.range(0, 1_000_000).asLongStream().sum();
// 对比 Stream<Integer> 会装箱
int s = list.stream().mapToInt(x -> x).sum();   // mapToInt 转原始流
#
★★

23. JDK 21+ Optional.or 与 ifPresentOrElse 的工程价值

JDK 21+ Optional.or 与 ifPresentOrElse 的工程价值是什么?

  • Optional.or(JDK 9)
  • ifPresentOrElse(JDK 9)
  • 工程价值

Optional.or(JDK 9+)允许在 Optional 为空时返回另一个 Optional(Supplier),用于链式"回退"可选值(如从缓存查不到再从 DB 查);ifPresentOrElse(valueConsumer, emptyAction) 在有值时执行值处理、空时执行空动作,用函数式方式处理两种分支。工程价值:1)or 支持"可选链式回退"(多个可选来源);2)ifPresentOrElse 把"有值/无值"两分支用函数式表达,避免 isPresent/get 命令式;3)提升可读性与函数式风格。或(or)用于"空则取另一个可选值",ifPresentOrElse 用于"有值/无值分别处理"。

or 提供空值回退(可选链),ifPresentOrElse 提供有值/无值两分支的函数式处理,提升可读性。

// or:空则回退另一个可选值
Optional<String> fromCache = cache.get(k);
Optional<String> val = fromCache.or(() -> db.get(k));   // 缓存空则查 DB
// ifPresentOrElse:两分支
user.ifPresentOrElse(u -> send(u), () -> log("no user"));
#
★★

24. Java 9 Optional.stream() 与 Optional.or 的工程价值

Java 9 Optional.stream() 与 Optional.or 的工程价值是什么?

  • Optional.stream()
  • Optional.or
  • 工程价值

Java 9 Optional.stream() 把 Optional 转成 Stream(有值→单元素流,无值→空流),工程价值:与 Stream 无缝协作——用 flatMap 把多个 Optional 展平,避免手动过滤空值(如 List<Optional > 展平为 Stream)。Optional.or 提供"空则返回另一个 Optional"的回退。二者工程价值:1)stream() 让 Optional 融入流式管道(Optional.flatMap(Stream) 展平);2)or 支持可选链式回退;3)提升函数式风格,避免 if 判断。示例:list.stream().map(...).flatMap(Optional::stream) 展平可选值。

Optional.stream() 把 Optional 转 Stream 展平可选值,or 提供空值回退,二者提升函数式管道协作。

// stream() 展平可选值
List<Optional<String>> opts = List.of(Optional.of("a"), Optional.empty());
List<String> vals = opts.stream().flatMap(Optional::stream).toList();  // ["a"]
// or 回退
Optional<String> v = opt.or(() -> getDefault());
#
★★

25. Java 9 Stream.takeWhile/dropWhile 的应用

Java 9 Stream.takeWhile/dropWhile 的应用是什么?

  • takeWhile
  • dropWhile
  • 条件截断

Java 9 的 takeWhile/predicate 与 dropWhile/predicate 是条件截断操作:takeWhile(predicate) 从流开头持续取满足谓词的元素,直到遇到不满足的(遇到第一个不满足即停止,短路);dropWhile(predicate) 从流开头持续丢弃满足谓词的元素,直到遇到不满足的(然后保留剩余全部)。应用:对已排序/有序流按条件截断(如取小于 100 的元素),用于有序数据的条件子集。注意:takeWhile/dropWhile 依赖"前段满足后段不满足"的有序假设,对无序流语义不明确。takeWhile 用于"取满足条件的前缀",dropWhile 用于"丢弃满足条件的前缀"。

takeWhile 取满足条件的前缀(到第一个不满足停止),dropWhile 丢弃满足条件的前缀,适用于有序流。

List<Integer> sorted = List.of(1, 2, 3, 4, 5, 6);
sorted.stream().takeWhile(x -> x < 4).toList();    // [1,2,3]
sorted.stream().dropWhile(x -> x < 4).toList();    // [4,5,6]
#
★★

26. Java 函数式编程的核心思想(纯函数、引用透明、柯里化)

Java 函数式编程的核心思想(纯函数、引用透明、柯里化)是什么?

  • 纯函数
  • 引用透明
  • 柯里化

Java 函数式编程核心思想:1)纯函数:相同输入总是相同输出,无副作用(不修改外部状态、不依赖可变状态),可预测、可测试、可并行;2)引用透明:表达式可被其值替换而不改变程序行为(纯函数的体现),利于推理与缓存;3)柯里化:把多参数函数转换为一系列单参数函数(f(x,y) -> f(x)(y)),利于部分应用与函数组合。Java 用 Lambda 与函数式接口支持这些思想,但 Java 非纯函数式语言(有副作用)。实践中:优先纯函数(无副作用)、用不可变对象、用 Optional/Stream 处理空与集合、用函数式组合(Function 组合)。柯里化在 Java 中不常用(需手动实现),但概念重要。

纯函数(无副作用、确定)、引用透明(可替换)、柯里化(多参数转单参数)是函数式核心,Java 用 Lambda 支持但非纯函数式。

// 纯函数:无副作用
static int square(int x) { return x * x; }   // 无副作用,引用透明
// 柯里化(Java 手动实现)
Function<Integer, Function<Integer, Integer>> curried = a -> b -> a + b;
curried.apply(1).apply(2);   // 3
#
★★

27. Lambda 中的 this 与外部作用域

Lambda 中的 this 与外部作用域是什么?

  • Lambda this 指向
  • 外部作用域
  • 与匿名类区别

Lambda 中的 this 指向"外部类实例"(Lambda 所在的外层对象),而不是 Lambda 自身(Lambda 没有自己的 this)。这与匿名内部类不同(匿名类 this 指向匿名类实例)。Lambda 的外部作用域:Lambda 可以访问外层类的 this、外层方法的参数、外层局部变量(有效 final)、外层类的静态成员。Lambda 捕获外层 this 意味着 Lambda 可访问外层实例字段(this.field)。this 指向外层类,使 Lambda 与外层对象绑定(若被存为字段且有状态,可能持有外部引用)。区别:匿名类 this 是自身,Lambda this 是外层。

Lambda 的 this 指向外层类实例(无自身 this),与匿名类(this 是自身)相反,Lambda 可访问外层 this 与字段。

class Outer {
    int x = 10;
    Runnable r = () -> System.out.println(this.x);   // this = Outer 实例
    Runnable r2 = new Runnable() { public void run() { System.out.println(this); } };  // this = 匿名类
}
#
★★

28. Lambda 序列化的局限

Lambda 序列化的局限是什么?

  • Lambda 序列化
  • 局限
  • 替代

Lambda 序列化的局限:1)只有符合 Serializable 的函数式接口才能序列化(如 Runnable、Function 实现 Serializable 版本),且需显式 cast;2)依赖 SerializedLambda(记录捕获参数、实现类/方法),反序列化需能重建 Lambda,跨版本/跨 JVM/跨类加载器不稳定;3)不保证序列化兼容(实现细节可能变);4)捕获的变量/对象需可序列化;5)Lambda 序列化形式脆弱,不适合持久化。局限源于 SerializedLambda 对实现细节的依赖。替代:持久化任务用显式描述(任务名+参数),避免序列化 Lambda。

Lambda 序列化限定 Serializable 接口且依赖 SerializedLambda 实现细节,跨版本不稳定,不用于持久化。

// 必须 cast 为 Serializable 函数式接口
Runnable r = (Runnable & Serializable) () -> System.out.println("hi");
// 序列化走 SerializedLambda,跨版本不稳定
// 持久化用任务描述 {task, args}
#
★★

29. Lambda 表达式的实现原理(invokedynamic + LambdaMetafactory)

Lambda 表达式的实现原理(invokedynamic + LambdaMetafactory)是什么?

  • invokedynamic
  • LambdaMetafactory
  • 实现原理

Lambda 表达式编译为 invokedynamic 调用指令 + LambdaMetafactory 引导方法:编译时把 Lambda 体包装到一个私有方法(如 lambda$main$0),生成 invokedynamic 调用点,Metafactory 在运行期生成函数式接口实例(新类),其方法调用该私有方法。好处:1)惰性生成:首次调用 details 才生成实现类,避免编译期生成类文件;2)性能:生成的小类可被 JIT 内联,比匿名类更高效;3)不捕获外部实例时复用同一实现(无 new 对象)。捕获变量以参数传递给私有方法。实现原理:invokedynamic + LambdaMetafactory 动态生成函数式接口实现,替代匿名内部类。

Lambda 用 invokedynamic + LambdaMetafactory 动态生成函数式接口实例,惰性、可内联、比匿名类高效。

// Lambda 编译为 invokedynamic + LambdaMetafactory
// 体包装为私有方法 lambda$main$0
// 运行期生成函数式接口实例
Runnable r = () -> System.out.println("hi");
// 等价于:LambdaMetafactory 生成类实现 Runnable,调用私有方法
#
★★

30. Optional 与 Stream 协作(stream()/filter)

Optional 与 Stream 协作(stream()/filter)是什么?

  • Optional.stream()
  • Optional.filter
  • 与 Stream 协作

Optional 与 Stream 协作:Optional.stream()(JDK 9+)把 Optional 转成 Stream(有值→单元素流,无值→空流),用 flatMap 展平多个 Optional;Optional.filter(predicate) 根据谓词过滤 Optional(不满足返回空 Optional)。协作模式:1)List<Optional > 用 stream().flatMap(Optional::stream) 展平为 Stream(丢弃空值);2)Optional 链式 map/flatMap/filter 后再转 Stream;3)Optional 作为 Stream 元素处理。Optional.filter 条件过滤,Optional.stream 接入流式管道。这些让 Optional 在函数式管道中无缝使用。

Optional.stream() 转 Stream 展平、filter 条件过滤,配合 Stream 管道实现可选值的函数式处理。

// 展平可选值
List<Optional<String>> opts = List.of(Optional.of("a"), Optional.empty());
opts.stream().flatMap(Optional::stream).toList();   // ["a"]
// filter
Optional<String> o = Optional.of("abc").filter(s -> s.length() > 2);  // 保留
#
★★

31. Optional 与 Stream 在链式调用中空值处理的协作模式

Optional 与 Stream 在链式调用中空值处理的协作模式是什么?

  • Optional 空值处理
  • Stream 空值处理
  • 协作模式

Optional 与 Stream 在链式调用中空值处理协作模式:1)Optional 用 flatMap/map/filter 处理"可为空的值",避免 NPE;2)Stream 用 flatMap(Optional::stream) 展平"可选值集合",丢弃空值;3)Optional 转 Stream 后可用 Stream 的过滤/转换;4)多级可选值用链式 flatMap 组合。协作模式核心:Optional 处理"单个值可能的空",Stream 处理"集合",用 flatMap(Optional::stream)、Optional.flatMap 把二者衔接,避免嵌套 if 与空值检查。示例:从用户可选对象中取可选地址的过滤,用 Stream 展平。

Optional 处理单值空、Stream 处理集合,用 flatMap(Optional::stream)/Optional.flatMap 衔接,避免嵌套空值检查。

// 协作:从用户列表取非空地址
List<String> addrs = users.stream()
    .map(User::getAddress)                 // Optional<Address>
    .filter(Optional::isPresent)
    .flatMap(Optional::stream)             // Stream<Address>
    .map(Address::getCity)
    .toList();
#
★★

32. Optional 与 null 在序列化上的差异

Optional 与 null 在序列化上的差异是什么?

  • Optional 序列化
  • null 序列化
  • 差异

Optional 与 null 的序列化差异:1)Optional 本身可序列化(实现 Serializable),但序列化 Optional 会把容器(含空/有值)序列化,体积和语义与"直接 null"不同;2)null 序列化是"无值"(JSON null、Java 序列化 null),Optional.empty 序列化为一个容器对象(空 Optional),不是 null;3)跨语言/json 序列化时 Optional 语义不统一(Jackson 默认把 Optional 序列化为值或忽略,需配置);4)Optional 作为字段序列化是反模式(Optional 不应作字段)。差异:Optional 是容器对象(序列化含容器结构),null 是"无值";JSON 中 Optional 序列化需处理(可能序列化为 null 或值或忽略)。工程上避免 Optional 作字段/序列化。

Optional 是容器对象(序列化含容器结构),null 是"无值",JSON 序列化 Optional 需特殊处理,避免 Optional 作字段。

// Optional 序列化:容器对象
Optional<String> o = Optional.of("a");
// 序列化为 {"value":"a"} 或 "a"(视配置)
// null 序列化为 null
// JSON:Jackson 默认 Optional 序列化为值,空 Optional 序列化为 null
#
★★

33. Optional 作为方法参数/字段的反模式

Optional 作为方法参数/字段的反模式是什么?

  • Optional 参数反模式
  • Optional 字段反模式
  • 正确使用

Optional 作为方法参数或字段是反模式:1)方法参数:Optional 作为参数会强制调用方 construct Optional,且不能放 null(语义混乱),不如用"可空参数 + 校验"或重载;设计上参数的可空性用注解或文档表达。2)字段:Optional 作为字段会强制每个实例持有 Optional 容器(浪费),且 Optional 不适合可变状态(不可序列化友好),应使用可空字段 + null 检查。正确使用:Optional 主要用于"返回值的可空表示"(方法返回 Optional 提示可能无值),不适合参数/字段。反模式根源:Optional 是"返回值容器",不是"字段类型"。

Optional 适合返回值(提示可空),作参数强制构造降低可读性、作字段浪费容器且可变,都是反模式。

// 反模式:Optional 作参数
void process(Optional<String> name) { ... }   // 调用方被迫构造 Optional
// 反模式:Optional 作字段
class User { Optional<String> name; }   // 容器浪费,可变
// 正确:返回值 Optional
Optional<String> findName(int id) { ... }
#
★★

34. Optional 的本质(容器对象)及其使用边界

Optional 的本质(容器对象)及其使用边界是什么?

  • Optional 本质
  • 容器对象
  • 使用边界

Optional 的本质是一个"容器对象":包装一个可能空的值,提供 map/flatMap/filter/orElse 等方法链式处理空值,避免 NPE 与 if 判断。使用边界:1)适合"返回值"(方法返回 Optional 提示可能无值);2)不适合字段(浪费容器、可变)、方法参数(强制构造)、序列化(不友好);3)不适合"空值本身有业务含义"(如 0、空字符串)——Optional 只表达"存在/不存在",不表达"空值有效";4)不适合集合元素(用空集合表示无)——Optional 不该用于集合内。边界:Optional 用于"返回值可空"且空值无业务意义,避免字段/参数/集合。

Optional 是容器对象,适合返回值表示可空,不适合字段/参数/集合/有业务含义的空值,需明确边界。

// 正确:返回值 Optional
Optional<User> findUser(int id) { ... }
// 正确:list 空值用空集合而非 Optional
List<User> getUsers() { return Collections.emptyList(); }  // 而非 Optional<List>
// 反模式:Optional 作字段/参数/集合
#
★★

35. Optional 的正确使用与反模式(isPresent/get)

Optional 的正确使用与反模式(isPresent/get)是什么?

  • Optional 正确使用
  • isPresent/get 反模式
  • 函数式方法

Optional 正确使用是用函数式方法(map、flatMap、filter、orElse、orElseGet、orElseThrow、ifPresent)处理空值,避免命令式检查。反模式:isPresent() 配 get()——先判断再取值,等于又把 Optional 用成"可空引用",且 get() 在空时抛 NoSuchElementException,违背 Optional 的设计(应用 orElse/orElseGet 或 map)。正确:用 map 转换、orElse/orElseGet 提供默认、orElseThrow 抛异常、ifPresent 副作用。避免 isPresent/get 组合,因为它绕过了 Optional 的链式处理。其他反模式:orElse(expensive())(orElse 总是求值,应 orElseGet Supplier)、嵌套 Optional。

Optional 正确用法是函数式链式(map/orElse/orElseGet),isPresent+get 是反模式(get 空抛异常),orElse 总是求值应 orElseGet。

// 反模式:isPresent + get
if (opt.isPresent()) { String s = opt.get(); }   // 反模式
// 正确:函数式
String s = opt.orElse("default");
String s2 = opt.orElseGet(() -> expensive());   // 惰性,避免总是求值
opt.map(x -> x.toUpperCase()).ifPresent(System.out::println);
#
★★

36. Optional.map/flatMap 与 stream 链的语义差异

Optional.map/flatMap 与 stream 链的语义差异是什么?

  • Optional.map/flatMap
  • Stream map/flatMap
  • 语义差异

Optional.map/flatMap 与 Stream 的 map/flatMap 语义相似但应用于不同结构:Optional.map(f):有值则转换(f 返回 T),空则返回空 Optional(f 返回普通值,不展平);Optional.flatMap(f):f 返回 Optional(展平,避免嵌套 Optional);Stream.map(f):每个元素转换(f 返回 T);Stream.flatMap(f):f 返回 Stream(展平多个元素)。差异:Optional 处理"单值可选",flatMap 展平 Optional 嵌套;Stream 处理"多元素",flatMap 展平元素流。语义:Optional.map 不展平(f 返回普通值),Optional.flatMap 展平(f 返回 Optional);Stream 同理。用于链式避免嵌套。

map 转换不展平(f 返回普通值),flatMap 展平(f 返回 Optional/Stream),Optional 处理单值、Stream 处理多元素。

// Optional.map:不展平
Optional<User> u = Optional.of(user);
Optional<String> name = u.map(User::getName);   // String
// Optional.flatMap:展平
Optional<String> city = u.flatMap(x -> x.getAddress());   // getAddress 返回 Optional
// Stream.map/flatMap 同理处理多元素
#
★★

37. Optional.of/ofNullable/empty 的合法参数

Optional.of/ofNullable/empty 的合法参数是什么?

  • Optional.of
  • ofNullable
  • empty

Optional 三种工厂方法:Optional.of(value):value 必须非 null(为 null 抛 NPE),用于"确定非空"的值;Optional.ofNullable(value):value 可为 null(null 返回 Optional.empty()),用于"可能为空"的值;Optional.empty():直接创建空 Optional(不接受参数)。合法参数:of 要求非 null(传 null 抛 NPE),ofNullable 接受 null(返回空 Optional),empty 无参数。选择:确定非空用 of(快速失败),可能为空用 ofNullable,需要空用 empty。of 传 null 会抛 NPE 是陷阱(应确认非空才用 of)。

of 要求非空(null 抛 NPE)、ofNullable 接受 null(null 返回 empty)、empty 无参数,按值是否确定非空选择。

Optional<String> a = Optional.of("x");        // 非空
Optional<String> b = Optional.ofNullable(maybeNull);  // null -> empty
Optional<String> c = Optional.empty();        // 空
// Optional.of(null) 抛 NPE
#
★★

38. Optional.orElse/orElseGet/orElseThrow 的差异

Optional.orElse/orElseGet/orElseThrow 的差异是什么?

  • orElse(总是求值)
  • orElseGet(惰性)
  • orElseThrow

Optional 三个取值方法差异:orElse(value):空时返回给定的 value,但 value 总是被求值(即使 Optional 非空,参数也先计算),适合"常量/轻量"默认值;orElseGet(Supplier):空时才调用 Supplier 求值(惰性),适合"昂贵/需计算"的默认值,避免不必要的计算;orElseThrow(Supplier):空时抛异常(Supplier 提供异常),用于"不该为空"的失败快速失败。差异核心:orElse 总是求值、orElseGet 惰性求值、orElseThrow 空时抛异常(可选默认异常)。性能:昂贵默认值用 orElseGet,避免 orElse 总是求值。

orElse 总是求值(适合常量)、orElseGet 惰性(适合昂贵默认)、orElseThrow 空时抛异常(快速失败)。

String s1 = opt.orElse("default");              // "default" 总是求值
String s2 = opt.orElseGet(() -> expensive());   // 惰性,仅空时求值
User u = opt.orElseThrow(() -> new NotFoundException("user"));  // 空抛异常
#
★★

39. OptionalInt/OptionalLong/OptionalDouble 的价值

OptionalInt/OptionalLong/OptionalDouble 的价值是什么?

  • 原始类型 Optional
  • 避免装箱
  • 价值

OptionalInt/OptionalLong/OptionalDouble 是原始类型特化的 Optional(对应 int/long/double),价值:1)避免装箱:不包装 Integer/Long/Double,直接持有基本类型,减少对象分配;2)内存紧凑:无 Optional 容器 + 对象包装开销;3)用于原始流(IntStream 等)的 reduce 结果、min/max 等返回(如 IntStream.max() 返回 OptionalInt)。它们与 Optional 的区别是避免装箱。价值:数值型的"可能无值"结果用原始 Optional 避免装箱开销。注意:原始 Optional 没有 orElse 返回 Optional 的链式(方法较少),主要用于流式聚合结果。

原始类型 OptionalInt/OptionalLong/OptionalDouble 避免装箱,用于原始流聚合结果(min/max/reduce),性能好。

// 原始流聚合返回原始 Optional
OptionalInt max = IntStream.of(1, 2, 3).max();   // 避免装箱
int m = max.orElse(-1);
// Stream<Integer> 用 Optional<Integer>(会装箱)
#
★★

40. groupingBy 默认返回的 Map 实现与迭代顺序,如何用 LinkedHashMap 或自定义 Map 工厂保持遇见顺序

groupingBy 默认返回的 Map 实现与迭代顺序,如何用 LinkedHashMap 或自定义 Map 工厂保持遇见顺序?

  • groupingBy 默认 Map
  • 迭代顺序
  • 保持遇见顺序

Collectors.groupingBy 默认返回 HashMap(无序),迭代顺序不保证(不保持遇见顺序)。若需保持遇见顺序(分组键按首次出现顺序),用三个参数的 groupingBy(classifier, mapFactory, downstream),mapFactory 提供 LinkedHashMap(或自定义 Map 工厂),使分组的键按插入顺序(遇见顺序)排列。示例:groupingBy(Function, LinkedHashMap::new, downstream)。保持遇见顺序:LinkedHashMap 保插入顺序,分组键按首次遇到的顺序。若无序可接受用默认 HashMap;需要有序分组用 LinkedHashMap 工厂。

groupingBy 默认 HashMap 无序,用 LinkedHashMap::new 作为 mapFactory 可保持分组键的遇见顺序。

// 默认:HashMap 无序
Map<String, List<Item>> m1 = items.stream().collect(Collectors.groupingBy(Item::getType));
// 保持遇见顺序:LinkedHashMap 工厂
Map<String, List<Item>> m2 = items.stream().collect(
    Collectors.groupingBy(Item::getType, LinkedHashMap::new, Collectors.toList()));
#
★★

41. Predicate 的复合(and/or/negate)

Predicate 的复合(and/or/negate)是什么?

  • Predicate and/or/negate
  • 默认方法
  • 复合谓词

Predicate 的默认方法支持复合:and(other)(与)、or(other)(或)、negate()(非)。这些方法返回新的 Predicate,组合多个条件。示例:p1.and(p2) 表示 p1 且 p2;p1.or(p2) 表示 p1 或 p2;p.negate() 表示非 p。复合谓词让多个条件声明式组合,可读、可复用。注意:and/or 默认方法组合,negate 取反。也可用 lambda 直接组合(x -> p1.test(x) && p2.test(x)),但 and/or 更简洁。Predicate 复合用于流式过滤等多个条件。

Predicate 的 and/or/negate 默认方法组合谓词(与/或/非),声明式组合多个条件。

Predicate<String> nonEmpty = s -> !s.isEmpty();
Predicate<String> length = s -> s.length() > 3;
Predicate<String> valid = nonEmpty.and(length);   // 非空且长度>3
Predicate<String> other = valid.negate();          // 取反
list.stream().filter(valid).toList();
#
★★

42. Spliterator 的特征(SIZED/DISTINCT/SORTED)对并行的影响

Spliterator 的特征(SIZED/DISTINCT/SORTED)对并行的影响是什么?

  • Spliterator 特征
  • SIZED/DISTINCT/SORTED
  • 并行影响

Spliterator 的特征(characteristics)告知 Stream 数据源的性质,影响并行:SIZED(元素数已知):允许精确拆分(estimateSize 精确),并行拆分更均衡;DISTINCT(元素唯一):允许 distinct 操作优化(跳过去重计算);SORTED(已排序):允许 sorted 操作跳过排序、支持有序优化(如 takeWhile 等)。若 Spliterator 错误声明特征,Stream 会做出错误假设,导致并行结果错误(如声称 SIZED 但实际元素数不同)。特征让 Stream 优化并行与操作(如 SORTED 跳过排序、DISTINCT 跳过去重),正确声明特征提升并行性能与正确性。

SIZED/DISTINCT/SORTED 特征让 Stream 优化拆分、去重、排序,错误声明会导致并行结果错误。

// Spliterator 特征
int SIZED = 0x40, DISTINCT = 0x1, SORTED = 0x4;
// SIZED:精确拆分均衡
// DISTINCT:distinct 跳过去重
// SORTED:sorted 跳过排序
// 错误声明特征 -> 并行结果错误
#
★★

43. Stream 与 ForkJoinPool 的关系(默认 commonPool)

Stream 与 ForkJoinPool 的关系(默认 commonPool)是什么?

  • 并行流线程池
  • commonPool
  • 自定义池

并行流(parallelStream)默认使用 ForkJoinPool.commonPool(共享的公共 ForkJoinPool),线程数默认 = CPU 核数 - 1。commonPool 是 JVM 共享的,所有并行流共用,因此并行流任务都提交到 commonPool。关系:并行流通过 commonPool 的 work-stealing 调度执行拆分任务。影响:commonPool 共享导致阻塞 I/O 会占满线程,影响其他并行任务;可通过 ForkJoinPool.commonPool() 或提交到自定义池(parallelStream 不能直接指定池,需自定义 Spliterator 或 use ForkJoinPool.submit 包裹)。自定义池:用 ForkJoinPool 包裹并行流,可控制并发度与隔离。

并行流默认用共享 commonPool,阻塞会互相影响,需控制并发时可提交到自定义 ForkJoinPool。

// 默认 commonPool
list.parallelStream().forEach(...);   // ForkJoinPool.commonPool()
// 自定义池:ForkJoinPool 包裹
ForkJoinPool pool = new ForkJoinPool(4);
pool.submit(() -> list.parallelStream().forEach(...)).join();
#
★★

44. Stream 中副作用(side effect)的反模式

Stream 中副作用(side effect)的反模式是什么?

  • 副作用反模式
  • Stream 副作用
  • 正确做法

Stream 中副作用(side effect)反模式:在 Stream 操作(如 map、forEach、filter)中修改外部可变状态(如外部 List、Map、计数器),违背纯函数与惰性求值,导致:1)结果不确定(惰性求值使副作用时机不可控);2)并行流下共享状态竞态;3)难以调试与维护。反模式示例:forEach 中向外部 List 添加(应 collect);peek 中做副作用(应避免)。正确做法:用纯函数(map/filter 无副作用)、用 collect 归约收集结果、避免在流中修改外部状态。副作用应放在终止操作(如 forEach 的边界)且明确。

Stream 副作用反模式(修改外部可变状态)违背纯函数与惰性,结果不确定、并行竞态,应改用 collect/纯函数。

// 反模式:forEach 中副作用
List<String> external = new ArrayList<>();
list.stream().forEach(x -> external.add(x));   // 副作用,并行竞态
// 正确:collect
List<String> result = list.stream().collect(Collectors.toList());
#
★★

45. Stream 中状态(stateful)与无状态操作的顺序敏感性

Stream 中状态(stateful)与无状态操作的顺序敏感性是什么?

  • 有状态操作
  • 无状态操作
  • 顺序敏感性

Stream 操作分有状态(stateful)与无状态(stateless):无状态操作(map、filter、mapToInt 等)每个元素独立处理,不依赖其他元素,顺序不敏感,可并行高效;有状态操作(distinct、sorted、limit/skip、takeWhile/dropWhile 部分)依赖整体状态或元素顺序,处理时需要缓冲/屏障,顺序敏感,并行下需保持顺序(有序屏障)或产生额外开销。顺序敏感性:有状态操作在并行流中需保持遇见顺序(ordered),引入屏障,降低并行度;无状态操作可自由并行。选择:并行流中避免过多有状态操作或接受其开销。

无状态操作(map/filter)元素独立可并行,有状态操作(distinct/sorted/limit)依赖整体或顺序,并行需屏障降低效率。

// 无状态:map/filter 并行高效
list.parallelStream().filter(x -> x > 0).map(x -> x * 2).toList();
// 有状态:distinct/sorted 并行需屏障
list.parallelStream().distinct().sorted().toList();   // 有状态,并行开销大
#
★★

46. Stream 中间操作与终止操作的惰性求值机制

Stream 中间操作与终止操作的惰性求值机制是什么?

  • 惰性求值
  • 中间操作
  • 终止操作

Stream 是惰性求值的:中间操作(map、filter、sorted 等)不立即执行,只是登记操作;直到终止操作(terminal:forEach、collect、count、reduce 等)调用时才真正执行整个管道。惰性机制:终止操作触发时,Stream 按中间操作顺序逐元素处理(管道化),且可能短路(如 limit、findFirst 提前停止)。好处:1)避免中间结果存储(无中间集合);2)支持无限流(配合 limit);3)短路优化。中间操作不执行、终止操作触发执行,是惰性求值的核心。注意:中间操作无副作用时机(惰性),终止操作才执行。

中间操作惰性登记不执行,终止操作触发整个管道执行,支持短路与无限流,避免中间结果。

// 中间操作惰性:不执行
Stream<String> s = list.stream().filter(x -> x.length() > 2);   // 未执行
// 终止操作触发
s.count();   // 才执行 filter
// 短路:limit 提前停止
Stream.generate(() -> 1).limit(5).count();   // 无限流+limit 终止
#
★★

47. Stream 的 lazy evaluation 与短路操作(Short-circuit)在内存占用上的边界

Stream 的 lazy evaluation 与短路操作(Short-circuit)在内存占用上的边界是什么?

  • 惰性求值内存
  • 短路操作
  • 内存边界

Stream 惰性求值避免中间结果存储,内存占用边界:1)无状态管道(filter/map)逐元素处理,几乎不占额外内存(流式);2)有状态操作(sorted、distinct、limit 需缓冲)会缓冲元素,内存占用与缓冲大小相关;3)短路操作(limit、findFirst、anyMatch)在满足条件后提前停止,减少处理元素数,降低内存占用;4)无限流 + limit 短路(内存可控),但无限流 + 有状态操作(sorted)会无限缓冲导致 OOM。边界:短路操作降低内存占用,但有状态操作(sorted/distinct)与消耗性操作会缓冲,需注意内存。惰性求值让内存占用取决于"终止操作与有状态操作"。

惰性求值流式处理,无状态操作低内存,短路提前停止降内存,但有状态操作(sorted)缓冲元素可能 OOM。

// 短路:findFirst 提前停止
list.stream().map(x -> x * 2).filter(x -> x > 10).findFirst();
// 内存边界:无限流 + sorted 会 OOM
// Stream.generate(() -> 1).sorted().count();  // OOM(无限缓冲)
// 边界:无限流 + limit 短路可
Stream.generate(() -> 1).limit(100).count();
#
★★

48. Stream 的 null 元素处理(Objects::nonNull)

Stream 的 null 元素处理(Objects::nonNull)是什么?

  • null 元素
  • Objects::nonNull
  • 过滤

Stream 可包含 null 元素(如 List 含 null),若后续操作(如 toList、map)对 null 敏感会 NPE。处理:用 Objects::nonNull 过滤 null 元素(filter(Objects::nonNull))或 Objects::isNull 保留 null。示例:list.stream().filter(Objects::nonNull).map(...).toList() 过滤 null 后再处理。注意:Stream.toList() 不允许 null 元素(抛 NPE),Collectors.toList() 允许。Collection 的 Stream 可能含 null,需显式过滤。Objects::nonNull 是标准方法引用,简洁过滤 null。

Stream 可能含 null,用 Objects::nonNull 过滤,避免后续操作 NPE;toList() 禁 null 需过滤。

List<String> list = Arrays.asList("a", null, "b");
list.stream().filter(Objects::nonNull).toList();   // ["a","b"],过滤 null
// Stream.toList() 禁 null;Collectors.toList() 允许
#
★★

49. Stream 的 peek 调试用途与生产禁用

Stream 的 peek 调试用途与生产禁用是什么?

  • peek 用途
  • 调试
  • 生产禁用

Stream.peek(Consumer) 用于调试:在管道中间查看元素(如打印每个元素),不改变元素。它是存在副作用(Consumer)的中间操作,主要用于调试(观察中间过程)。生产禁用原因:1)peek 的副作用违背纯函数(惰性求值下时机不定,元素可能被忽略);2)peek 不保证执行(短路时部分元素可能不经过 peek);3)生产代码用 peek 做业务副作用是反模式(结果不可依赖)。调试用 peek 打印中间元素,生产不应依赖 peek 的副作用,应用 map/filter 或终止操作处理。调试后可移除。

peek 用于调试观察中间元素,其副作用在生产不可依赖(惰性、短路、违背纯函数),生产禁用。

// 调试:观察中间元素
list.stream().map(x -> x * 2).peek(System.out::println).filter(x -> x > 5).toList();
// 生产禁用:peek 副作用不可依赖
// 应使用 map/filter 或终止操作
#
★★

50. Stream 的 short-circuit 操作(findAny/anyMatch)的性能意义

Stream 的 short-circuit 操作(findAny/anyMatch)的性能意义是什么?

  • 短路操作
  • findAny/anyMatch
  • 性能优势

short-circuit 操作(findAny、findFirst、anyMatch、allMatch、noneMatch、limit)在满足条件后提前终止,不必处理全部元素,性能意义:大数据集/无限流下大幅减少处理。findAny:返回任意匹配元素(不保证顺序,并行最佳),anyMatch:是否存在匹配元素,两者在找到第一个匹配后停止。性能:短路避免全量扫描,尤其匹配发生早或数据大时;无限流 + 短路可终止。findAny 在并行的最优(不保序),findFirst 需保序(并行开销)。选择:不关心顺序用 findAny(性能好),需保序用 findFirst。

短路操作(findAny/anyMatch)提前终止减少处理,性能好,findAny 并行最优(不保序),findFirst 需保序。

// 短路:找到即停
boolean found = list.stream().anyMatch(x -> x > 100);   // 找到即停
Optional<Integer> any = list.parallelStream().findAny();  // 并行最优,不保序
Optional<Integer> first = list.stream().findFirst();     // 保序
#
★★

51. Stream 管道中抛出受检异常时,包装成运行时异常、返回结果类型与提前校验应如何取舍

Stream 管道中抛出受检异常时,包装成运行时异常、返回结果类型与提前校验应如何取舍?

  • 受检异常包装
  • 返回结果类型
  • 提前校验

Stream 管道中不能直接抛受检异常(函数式接口不声明),取舍:1)包装成运行时异常:把受检异常包装为 RuntimeException(new RuntimeException(e) 或自定义),简单但丢失类型、调用方捕获不便;2)返回结果类型:用 Result/Optional/自定义结果类型归约失败,把"失败"作为结果而非异常(如 map 返回 Result,flatMap 过滤失败);3)提前校验:在进入 Stream 前先校验数据(如校验非空、格式),避免管道内异常。取舍:业务失败(可预期)用 Result/提前校验,真正的异常(意外)用包装运行时异常;包装时保留 cause 并在边界解包。避免在管道内吞异常。

受检异常在管道中:可预期失败用 Result/提前校验,意外异常包装运行时异常并保留 cause,避免吞异常。

// 包装运行时异常
try { list.stream().map(s -> parse(s))... }  // parse 抛受检,需包装
// 提前校验
list.stream().filter(Objects::nonNull).map(s -> parse(s));  // 先校验
// 结果类型:返回 Result
list.stream().map(s -> tryParse(s)).filter(Result::isOk).map(Result::get).toList();
#
★★

52. Stream 顺序与并行的选择准则在 CPU 密集型与 I/O 密集型下的取舍

Stream 顺序与并行的选择准则在 CPU 密集型与 I/O 密集型下的取舍是什么?

  • 顺序 vs 并行
  • CPU 密集型
  • I/O 密集型

Stream 顺序与并行选择准则:CPU 密集型(纯计算、无 I/O):数据量大、元素独立、拆分容易时用并行(parallelStream)可提升吞吐(利用多核),但需注意共同池与拆分开销;I/O 密集型:元素阻塞 I/O(网络、文件),并行流会占满 commonPool 线程且阻塞,不应用并行流(用虚拟线程或自定义池),因为并行流线程数有限(CPU 核数)且阻塞浪费。取舍:CPU 密集用并行(元素多、拆分好),I/O 密集避免并行流(用虚拟线程/异步),小数据用顺序(并行拆分开销大于收益)。准则:数据量大 + 元素独立 + 拆分好 + CPU 密集才考虑并行。

CPU 密集大数组用并行提升吞吐,I/O 密集避免并行流(阻塞占线程),小数据顺序即可,并行有拆分开销。

// CPU 密集:并行
long sum = LongStream.range(0, 1_000_000).parallel().sum();
// I/O 密集:避免并行流(阻塞占 commonPool)
// 用虚拟线程或异步
list.stream().map(x -> fetch(x)).toList();   // 顺序或虚拟线程
#
★★

53. Stream.iterate 与 generate 的有状态行为差异

Stream.iterate 与 generate 的有状态行为差异是什么?

  • iterate
  • generate
  • 有状态

Stream.iterate(seed, f) 生成无限流:从 seed 开始,每元素用 f 从前一元素生成(有状态,依赖前值),如 iterate(0, n -> n + 1) 生成 0,1,2,...;iterate(seed, predicate, f) 可带终止条件。Stream.generate(Supplier) 生成无限流:每次调用 Supplier 生成元素,Supplier 可无状态(随机、常量)或有状态(需自行维护状态,因为不依赖前值)。差异:iterate 是有状态的(依赖前值,前元素推导后元素),可预测;generate 每次从 Supplier 取(无前值依赖,需 Supplier 自行维护状态)。选择:规律序列用 iterate,独立生成用 generate。

iterate 依赖前值生成(有状态、可预测),generate 每次调 Supplier(无前值依赖,状态需自行维护)。

// iterate:依赖前值
Stream.iterate(0, n -> n + 1).limit(5).toList();   // [0,1,2,3,4]
// generate:每次调 Supplier
AtomicInteger ai = new AtomicInteger();
Stream.generate(ai::incrementAndGet).limit(5).toList();  // [1,2,3,4,5]
#
★★

54. Stream.onClose 的处理器何时执行,为什么仅调用终止操作并不会自动关闭普通 Stream

Stream.onClose 的处理器何时执行?为什么仅调用终止操作并不会自动关闭普通 Stream?

  • onClose 处理器
  • 执行时机
  • 自动关闭

Stream.onClose(handler) 注册处理器,在 Stream 被 close() 时执行(否则不执行)。处理器执行时机:只有显式调用 close()(或在 try-with-resources 中)时才执行 onClose 处理器。普通终止操作(collect、forEach 等)不会自动关闭 Stream,因此 onClose 处理器不会被调用。原因:普通 Stream 不持有资源,终止操作不视为"关闭",需显式 close() 才触发 onClose。若 Stream 是资源型(如 Files.lines 返回的 Stream),需 try-with-resources 或显式 close 以释放资源并执行 onClose。因此 onClose 处理器依赖显式 close,普通 Stream 不自动关闭。

onClose 处理器仅在显式 close/try-with-resources 时执行,普通终止操作不关闭 Stream,资源型 Stream 需关闭。

Stream<String> s = Files.lines(path);   // 资源型 Stream
s.onClose(() -> System.out.println("closed"));
s.forEach(System.out::println);   // 不关闭,不执行 onClose
s.close();   // 执行 onClose
// 正确:try-with-resources
try (Stream<String> lines = Files.lines(path)) { lines.forEach(...); }
#
★★

55. Supplier 与懒求值的工程应用

Supplier 与懒求值的工程应用是什么?

  • Supplier
  • 懒求值
  • 工程应用

Supplier 表示"无参返回值的供应者",工程应用在于懒求值(lazy evaluation):延迟计算直到需要时,避免不必要的计算。应用:1)Optional.orElseGet(Supplier) 延迟默认值(仅空时求值);2)Stream.generate(Supplier) 生成元素;3)日志/配置懒加载(惰性初始化);4)依赖注入的懒加载(Bean 延迟创建);5)计算昂贵的值延迟到需要(如缓存)。Supplier 是无参、可延迟、可重复调用的生成器,用于把"计算"推迟到消费时。工程价值:避免昂贵计算、按需初始化、延迟绑定。

Supplier 提供懒求值,把计算延迟到消费时,用于 Optional.orElseGet、Stream.generate、惰性初始化等避免昂贵计算。

// 懒求值:orElseGet 延迟
String s = Optional.ofNullable(x).orElseGet(() -> expensive());   // 仅空时计算
// 惰性初始化
Supplier<Heavy> heavy = () -> new Heavy();   // 延迟创建
// Stream.generate
Stream.generate(() -> Math.random()).limit(3).toList();
#
★★

56. collect、Collectors 与归约(reduce)的取舍

collect、Collectors 与归约(reduce)的取舍是什么?

  • collect
  • Collectors
  • reduce

collect 与 reduce 都是终止归约操作:collect 用 Collector 把流归约为可变容器(List、Map、Set 等),适合"收集到集合/映射";reduce 用二元操作把流归约为单个值(求和、求最大、连接字符串),适合"数值/标量归约"。Collectors 提供预置收集器(toList、groupingBy、joining 等)。取舍:收集到集合/映射用 collect(Collectors),归约为标量值用 reduce;reduce 适合无状态可结合的二值归约(求和、最大),collect 适合复杂收集(分组、映射)。reduce 的 identity 需满足结合律与恒等,否则结果不确定。选择:标量归约 reduce,集合/映射收集 collect。

reduce 归约为标量(求和/最大,需可结合),collect/Collectors 收集到集合/映射(分组/映射),按目标选择。

// reduce:标量归约
int sum = list.stream().reduce(0, Integer::sum);   // 求和
// collect:收集到集合
List<String> l = list.stream().collect(Collectors.toList());
Map<String, List<X>> m = list.stream().collect(Collectors.groupingBy(X::getType));
#
★★

57. distinct、sorted 和 limit 等有状态操作如何影响并行管道的缓冲、屏障和内存占用

distinct、sorted 和 limit 等有状态操作如何影响并行管道的缓冲、屏障和内存占用?

  • 有状态操作
  • 并行缓冲/屏障
  • 内存占用

distinct、sorted、limit 等有状态操作在并行管道中影响:1)缓冲:这些操作需要缓冲元素(distinct 用 HashSet,sorted 用数组,limit 需计数),在并行下各线程缓冲自己的子集,需合并;2)屏障:有状态操作引入"会合屏障"(rendezvous barrier)——并行子任务先各自处理,再在屏障处合并(distinct 去重合并、sorted 排序合并),破坏流水线,降低并行度;3)内存占用:缓冲元素占用内存(distinct 存哈希、sorted 存数组),大数据量下内存压力大。有状态操作在并行下牺牲流水线(屏障)与内存(缓冲),性能下降。设计并行管道时避免过多有状态操作。

有状态操作(distinct/sorted/limit)在并行下缓冲元素、引入会合屏障、占用内存,破坏流水线降低并行度。

// 有状态操作并行:缓冲+屏障
list.parallelStream().distinct().sorted().limit(100).toList();
// 各线程先各自 distinct/sorted,再屏障合并,内存占用大
#
★★

58. findFirst 与 findAny 在并行流中的确定性和短路成本有何差异,何时可选择后者

findFirst 与 findAny 在并行流中的确定性和短路成本有何差异?何时可选择后者?

  • findFirst 确定性
  • findAny 非确定性
  • 并行成本

findFirst 与 findAny 差异:findFirst 返回"遇见顺序的第一个"匹配元素(确定性、保序),在并行流中需保持顺序(各线程结果按顺序合并),有额外排序/同步成本;findAny 返回"任意"匹配元素(非确定性、不保序),在并行流中哪个线程先找到就返回,无顺序合并成本,性能更好。短路成本:两者都短路(找到即停),但 findFirst 并行需保证返回顺序第一个,成本高于 findAny。选择:不关心顺序(只想要任意匹配)用 findAny(并行性能好),需要顺序第一个用 findFirst。findAny 在并行下更优。

findFirst 保序确定(并行有顺序成本),findAny 任意元素(并行无顺序成本性能好),不关心顺序用 findAny。

// findFirst:保序,并行需顺序合并成本
Optional<Integer> first = list.parallelStream().findFirst();
// findAny:任意,并行最快返回
Optional<Integer> any = list.parallelStream().findAny();
#
★★

59. flatMap 创建的子流在消费后如何关闭,子流绑定文件资源时应怎样注册清理动作

flatMap 创建的子流在消费后如何关闭?子流绑定文件资源时应怎样注册清理动作?

  • flatMap 子流
  • 子流关闭
  • 资源清理

flatMap 创建的子流在消费后由 Stream 内部关闭(flatMap 会消费子流并关闭)。但若子流绑定资源(如 Files.lines 返回的流),需要注册清理:1)子流用 try-with-resources 或 onClose 注册清理;2)flatMap 消费子流后通常自动关闭,但资源型子流需显式处理。推荐:在子流上使用 onClose 注册清理动作,且外层流也需关闭;或用 try-with-resources 包裹。资源型子流(File 流)应在 flatMap 内用 try-with-resources 或 onClose 确保关闭,避免文件句柄泄漏。改进:嵌套流资源管理复杂,可考虑替代(如读取后收集)。

flatMap 消费子流通常自动关闭,但资源型子流需 onClose/try-with-resources 注册清理,避免句柄泄漏。

// 子流绑定资源:用 onClose 注册清理
list.stream().flatMap(path -> {
    try {
        Stream<String> lines = Files.lines(path);
        return lines.onClose(() -> System.out.println("closed " + path));
    } catch (IOException e) { throw new RuntimeException(e); }
}).forEach(...);
#
★★

60. flatMap、mapMulti(JDK 16+)的差异与一对一/多对多场景

flatMap、mapMulti(JDK 16+)的差异与一对一/多对多场景是什么?

  • flatMap
  • mapMulti
  • 差异与场景

flatMap 与 mapMulti(JDK 16+)都用于"一个元素产出多个元素"(展平),差异:flatMap 每个元素返回一个 Stream(创建新的 Stream 对象,开销大),mapMulti 用回调(Consumer 接收产出的每个元素)避免创建临时 Stream,开销小。场景:一对一(map 即可)、一对多(flatMap 或 mapMulti)。mapMulti 适合"少量展开"(如每个元素产 0-2 个元素)或不想创建 Stream 的开销;flatMap 适合"每个元素产出一个流"(如嵌套集合展平)。mapMulti 更高效(无临时 Stream),但语义略复杂(回调)。选择:大量展开或嵌套 Stream 用 flatMap,少量展开、性能敏感用 mapMulti。

flatMap 每元素返回 Stream(临时对象),mapMulti 用回调避免临时 Stream 更高效,适合少量展开。

// flatMap:每元素返回 Stream
list.stream().flatMap(x -> x.getItems().stream()).toList();
// mapMulti:回调产出,避免临时 Stream
list.stream().mapMulti((x, consumer) -> { for (Item i : x.getItems()) consumer.accept(i); }).toList();
#
★★

61. groupingBy 与 partitioningBy 的差异与下游收集器

groupingBy 与 partitioningBy 的差异与下游收集器是什么?

  • groupingBy
  • partitioningBy
  • 下游收集器

groupingBy 与 partitioningBy 都是分组收集器:groupingBy(classifier) 按分类键分组(键可任意类型,返回 Map<K, List >);partitioningBy(predicate) 按布尔谓词分成两组(true/false,返回 Map<Boolean, List>)。差异:partitioningBy 是布尔二分(固定 true/false 两组),groupingBy 是任意键分组(多组)。partitioningBy 性能略优(固定两组)。下游收集器(downstream):两个都支持下游收集器(第二个参数),对每组内的元素做进一步收集(如 toList、toSet、counting、mapping、summingInt)。示例:groupingBy(type, counting()) 统计每组数量;partitioningBy(pred, mapping(...)) 对每组映射。

groupingBy 任意键分组、partitioningBy 布尔二分,都支持下游收集器对每组进一步收集(toList/counting/mapping)。

// groupingBy:任意键分组
Map<String, List<Item>> byType = items.stream().collect(Collectors.groupingBy(Item::getType));
// partitioningBy:布尔二分
Map<Boolean, List<Item>> byOk = items.stream().collect(Collectors.partitioningBy(Item::isOk));
// 下游收集器
Map<String, Long> counts = items.stream().collect(Collectors.groupingBy(Item::getType, Collectors.counting()));
#
★★

62. groupingByConcurrent 在有序并行流中是否一定更快,热点分组键会造成怎样的竞争

groupingByConcurrent 在有序并行流中是否一定更快?热点分组键会造成怎样的竞争?

  • groupingByConcurrent
  • 并行性能
  • 热点键竞争

groupingByConcurrent 在并行流中用 ConcurrentHashMap 并发分组,不保证更快:1)若并行流是有序的(遇到顺序),仍需保持顺序(会合并/同步),性能不一定优于普通分组;2)并发分组有同步与合并开销,小数据或顺序要求下可能更慢。热点分组键竞争:若许多元素映射到同一个分组键(热点键),该键的 ConcurrentHashMap 桶会成为竞争热点,多个线程并发写同一桶,产生锁竞争/冲突,降低并行度。因此 groupingByConcurrent 在"结果无序可接受 + 分组键分布均匀"时最快,热点键(分布不均)会竞争。选择:无序 + 键均匀才用 groupingByConcurrent。

groupingByConcurrent 在有序流需保持顺序不一定更快,热点分组键(分布不均)造成 ConcurrentHashMap 桶竞争。

// 无序并行:groupingByConcurrent 用 ConcurrentHashMap
Map<String, List<Item>> m = items.parallelStream()
    .collect(Collectors.groupingByConcurrent(Item::getType));
// 热点键:大量元素同键 -> 同桶竞争
#
★★

63. mapMulti 与 flatMap 在少量展开元素时如何减少临时 Stream 分配,回调使用有什么限制

mapMulti 与 flatMap 在少量展开元素时如何减少临时 Stream 分配?回调使用有什么限制?

  • mapMulti 减少临时 Stream
  • 回调限制
  • 少量展开

mapMulti 在少量展开元素时避免为每个元素创建临时 Stream(flatMap 每元素返回 Stream,产生临时对象),用回调(Consumer)直接产出结果,减少临时 Stream 分配与 GC。限制:回调使用限制:1)回调只应在 integrate 内同步使用(不能把 consumer 存到外部延迟调用,否则元素顺序/生命周期错误);2)回调产出的元素应"少量"(mapMulti 设计用于少量展开,大量展开用 flatMap);3)回调内不能改变流状态(纯产出)。mapMulti 适合"每元素展开 0-2 个"的场景,避免 flatMap 的临时 Stream 开销。

mapMulti 用回调直接产出避免临时 Stream,适合少量展开;回调须同步使用、不能延迟调用,大量展开用 flatMap。

// mapMulti:回调产出,避免临时 Stream
list.stream().mapMulti((x, consumer) -> {
    if (x != null) consumer.accept(x);   // 少量产出,同步使用
}).toList();
// 限制:consumer 不能存到外部延迟调用
#
★★

64. peek 受惰性求值和操作优化影响为何不适合作为业务副作用,调试时应如何使用

peek 受惰性求值和操作优化影响为何不适合作为业务副作用?调试时应如何使用?

  • peek 副作用
  • 惰性求值
  • 调试使用

peek 不适合作为业务副作用原因:1)惰性求值:peek 是中间操作,只有终止操作才执行,副产品时机不可控(元素可能根本不被处理);2)操作优化:JIT 与 Stream 实现可能优化掉 peek(如无副作用检测、短路跳过元素),peek 不保证对每个元素执行;3)违背纯函数。因此 peek 的副作用(如日志、统计)不可依赖,不适合业务。调试使用:peek 用于调试时观察中间元素(打印元素、检查值),配合测试;调试完成后移除(或保留在调试环境)。正确调试:用 peek(System.out::println) 观察中间过程,生产不依赖 peek 副作用。

peek 因惰性求值与操作优化不保证执行,副作用不可依赖,只用于调试观察中间元素,生产不依赖。

// 调试:观察中间元素
list.stream().map(x -> x * 2).peek(x -> System.out.println("after map: " + x)).filter(x -> x > 5).toList();
// 生产禁用:peek 副作用不保证执行
#
★★

65. takeWhile 在无序并行流上的语义为什么较弱,保留遇见顺序会带来什么性能代价

takeWhile 在无序并行流上的语义为什么较弱?保留遇见顺序会带来什么性能代价?

  • takeWhile 无序语义
  • 并行流
  • 顺序代价

takeWhile 在无序并行流上语义较弱:takeWhile 依赖"元素满足条件的前缀",要求序列有序;无序并行流中元素顺序不确定,takeWhile 无法确定"前缀"的边界(哪些元素满足、哪些不满足,且无法保证取到的是"前缀"),语义不明确(可能取到非前缀元素)。保留遇见顺序代价:若并行流有序且 takeWhile 需保留顺序,需缓冲/屏障(各线程先处理,再按顺序合并,遇到第一个不满足即停止),引入同步与顺序合并开销,降低并行度。因此 takeWhile 在无序并行流语义弱,有序并行需顺序屏障(性能代价)。

takeWhile 依赖前缀有序语义,无序并行流语义弱;有序并行取前缀需顺序合并屏障,降低并行度。

// 无序并行:takeWhile 语义弱
list.parallelStream().takeWhile(x -> x < 10).toList();   // 无序下前缀不确定
// 有序并行:需顺序屏障
list.stream().takeWhile(x -> x < 10).toList();   // 顺序
#
★★

66. 使用减法或浮点累加执行并行归约为何可能得到不同结果,应该怎样设计可结合的归约

使用减法或浮点累加执行并行归约为何可能得到不同结果?应该怎样设计可结合的归约?

  • 结合性
  • 并行归约
  • 浮点误差

并行归约要求归约函数是可结合的(associative)且 identity 恒等,否则结果不确定。减法:减法不可结合((a-b)-c != a-(b-c)),并行分块归约顺序不同导致结果不同。浮点累加:浮点加法虽满足结合律的数学定义,但浮点舍入误差使"结合顺序不同"结果不同(如 (1e20+1)+(-1e20) 与 1e20+(1+(-1e20)) 不同),并行把累加分块后合并顺序改变舍入误差。设计可结合归约:用可结合运算符(加法、乘法、最大、最小——但浮点注意舍入);避免减法/浮点链式累加做并行;用归约函数保证可结合性与恒等(identity)。求和用可结合方式,浮点高精度用 BigDecimal 或 Kahan 求和。

并行归约需可结合+恒等,减法不可结合、浮点累加舍入误差致结果随顺序变化,需用可结合归约并注意浮点。

// 减法不可结合(并行结果不同)
// (a-b)-c != a-(b-c)
// 浮点累加舍入误差
double s = list.parallelStream().reduce(0.0, Double::sum);   // 顺序影响舍入
// 可结合:求和用可结合方式,浮点用 BigDecimal/Kahan
#
★★

67. 分析 Stream 性能时如何区分数据源拆分、Lambda 分配、装箱和终止收集各自的成本

分析 Stream 性能时如何区分数据源拆分、Lambda 分配、装箱和终止收集各自的成本?

  • 数据源拆分成本
  • Lambda 分配
  • 装箱

分析 Stream 性能需区分各阶段成本:1)数据源拆分:Spliterator 的 trySplit 创建子块、估大小,集合 vs 流 vs 生成器拆分成本不同;2)Lambda 分配:Lambda 表达式(尤其捕获变量的)生成为对象,创建函数式接口实例有分配成本;3)装箱:原始类型流避免装箱,Stream 会装箱(Integer 对象分配);4)终止收集:collect 收集到集合/容器(List/Map 分配、容量增长复制)、reduce 归约。区分方法:用 JMH 基准分别测量(控制变量:用原始流 vs 装箱流测装箱、用不同数据源测拆分、用 collect vs forEach 测收集),用分配器(JFR/Allocation Profiler)测对象分配。定位瓶颈:先看数据源(拆分)、再看 Lambda(分配)、装箱(对象)、最后收集(容器)。

分析 Stream 性能用 JMH 控制变量分别测拆分/Lambda/装箱/收集,用分配器定位对象分配瓶颈。

// JMH 分别测量
@Benchmark public int rawSum() { return IntStream.range(0, N).sum(); }       // 无装箱
@Benchmark public int boxedSum() { return IntStream.range(0, N).boxed().reduce(0, Integer::sum); }  // 装箱
// 对比数据源:ArrayList vs 生成器
#
★★

68. 四大核心函数式接口(Function/Consumer/Supplier/Predicate)的典型签名与选择依据

四大核心函数式接口(Function/Consumer/Supplier/Predicate)的典型签名与选择依据是什么?

  • Function 签名
  • Consumer 签名
  • Supplier/Predicate

四大核心函数式接口:1)Function<T,R>:R apply(T t)——输入 T 输出 R(转换);2)Consumer :void accept(T t)——输入 T 无输出(消费);3)Supplier :T get()——无输入输出 T(供给);4)Predicate:boolean test(T t)——输入 T 输出 boolean(断言)。选择依据:需要"输入→输出"用 Function;"输入→无(副作用)"用 Consumer;"无输入→输出"用 Supplier;"输入→boolean"用 Predicate。标准库提供了 BiFunction(两参)、BiConsumer、BiPredicate 等变体。按"输入输出形态"选择四大接口之一。

Function 输入→输出、Consumer 消费、Supplier 供给、Predicate 断言,按输入输出形态选择。

Function<String, Integer> f = String::length;   // 输入→输出
Consumer<String> c = System.out::println;        // 消费
Supplier<String> s = () -> "x";                  // 供给
Predicate<String> p = x -> x.length() > 0;       // 断言
#
★★

69. 复用已经执行终止操作的 Stream 为什么抛 IllegalStateException,管道应如何重新创建

复用已经执行终止操作的 Stream 为什么抛 IllegalStateException?管道应如何重新创建?

  • Stream 一次性
  • IllegalStateException
  • 重建管道

Stream 是一次性的:执行终止操作(terminal)后,Stream 处于"已消费"状态,再次调用终止操作(或操作已关闭的流)会抛 IllegalStateException("stream has already been operated upon or closed")。原因:Stream 是惰性管道,终止操作消费了数据源,状态不可复用;Stream 设计为一次性,防止重复消费。需要重新处理时,应重新创建管道(从数据源新建 Stream,重新构建中间操作)。不能复用已终端的 Stream。示例:s.filter(...).toList() 后再 s.count() 抛 IllegalStateException,需重新 stream() 创建。

Stream 一次性,终止操作后再次操作抛 IllegalStateException,需从数据源重新创建管道。

Stream<String> s = list.stream().filter(x -> x.length() > 2);
s.count();           // 消费
s.count();           // 抛 IllegalStateException
// 重新创建
list.stream().filter(x -> x.length() > 2).count();   // 新管道
#
★★

70. 方法引用在实例方法与未绑定接收者形式下,函数式接口第一个参数分别如何参与调用

方法引用在实例方法与未绑定接收者形式下,函数式接口第一个参数分别如何参与调用?

  • 已绑定实例方法引用
  • 未绑定实例方法引用
  • 接收者参数

方法引用实例方法有两种形式:1)已绑定(bound)实例方法引用:obj::method,接收者已固定为 obj,函数式接口的参数即方法的参数(如 Supplier s = obj::getName,无参);2)未绑定(unbound)实例方法引用:Type::method,接收者作为函数式接口的第一个参数传入(如 Function<String, Integer> f = String::length,第一个参数是接收者 String,length 无参)。未绑定形式:函数式接口的第一个参数是接收者(实例),其余参数是方法参数。示例:BiFunction<String, Integer, Character> g = String::charAt,第一个参数 String 是接收者,第二个 Integer 是 charAt 的索引。

已绑定 obj::method 接收者固定,未绑定 Type::method 接收者作为函数式接口第一个参数传入。

// 已绑定:接收者固定 obj
Supplier<Integer> s = obj::getSize;   // 无参
// 未绑定:接收者作为第一个参数
Function<String, Integer> f = String::length;   // 参数 String 是接收者
BiFunction<String, Integer, Character> g = String::charAt;  // 参数1=接收者,参数2=索引
#
★★

71. 方法引用(Class::method)的四种形式与可读性

方法引用(Class::method)的四种形式与可读性是什么?

  • 四种方法引用
  • 可读性
  • 形式

方法引用四种形式:1)静态方法引用:Class::staticMethod(如 Integer::parseInt);2)实例方法引用(已绑定):obj::instanceMethod(如 obj::toString);3)实例方法引用(未绑定):Class::instanceMethod(如 String::length,接收者作第一参数);4)构造器引用:Class::new(如 ArrayList::new)。可读性:方法引用比 Lambda 更简洁、可读(用方法名表达意图),当 Lambda 只是调用一个方法时用方法引用(如 list.forEach(System.out::println) 代替 x -> System.out.println(x))。四种形式对应不同调用场景,选择能保持简洁与可读。

四种方法引用:静态、已绑定实例、未绑定实例、构造器,当 Lambda 只是调用方法时用方法引用更可读。

// 静态方法
Function<String, Integer> f = Integer::parseInt;
// 已绑定实例
Supplier<String> s = obj::toString;
// 未绑定实例(接收者第一参数)
Function<String, Integer> len = String::length;
// 构造器
Supplier<List<String>> list = ArrayList::new;
#
★★

72. 方法引用(Method Reference)与 Lambda 在字节码 invokedynamic 调用点的差异

方法引用(Method Reference)与 Lambda 在字节码 invokedynamic 调用点的差异是什么?

  • invokedynamic 调用点
  • 方法引用 vs Lambda
  • 字节码差异

方法引用与 Lambda 都被编译为 invokedynamic 调用点 + LambdaMetafactory,但略有差异:Lambda 体被包装为私有方法(lambda$...$0),invokedynamic 调用点用 LambdaMetafactory 生成函数式接口实例,其方法调用私有方法;方法引用直接引用目标方法(静态方法/实例方法/构造器),也走 invokedynamic + LambdaMetafactory,但引导方法直接绑定目标方法(无需包装私有方法)。差异:方法引用直接绑定被引用的方法,Lambda 需包装私有方法;两者都生成函数式接口实例,但方法引用少一层包装,可能更高效(直接调用目标方法)。字节码上都是 invokedynamic,但常量池/引导参数不同。

两者都编译为 invokedynamic + LambdaMetafactory,方法引用直接绑定目标方法(少一层私有方法包装),Lambda 需包装体。

// Lambda:包装为 lambda$main$0 私有方法
Runnable r = () -> System.out.println("hi");
// 方法引用:直接绑定目标方法
Runnable r2 = obj::run;   // 直接引用 run 方法
// 都走 invokedynamic + LambdaMetafactory
#
★★

73. 无限 Stream 使用 limit 能否保证终止,若前面放置有状态过滤或排序会发生什么

无限 Stream 使用 limit 能否保证终止?若前面放置有状态过滤或排序会发生什么?

  • 无限流 limit
  • 有状态操作
  • 终止

无限 Stream(Stream.generate/iterate)配合 limit 能保证终止:limit 短路,取够 N 个元素即停,即使源无限。但若在 limit 前放置有状态操作(sorted、distinct 需缓冲全部)或会阻止短路的操作,则可能不终止:1)sorted 需收集全部元素排序,无限流永远无法"全部"收集,limit 之前的 sorted 无法完成(无限缓冲,OOM 或永不终止);2)distinct 需缓冲已见元素,无限流下可能无限增长(若元素不断新增);3)limit 放在有状态操作之前才保证终止。因此无限流 + limit 保证终止,但 limit 前若有状态操作(sorted/distinct)会破坏终止(无限缓冲)。

无限流 + limit 短路可终止,但 limit 前有状态操作(sorted/distinct)需缓冲全部/无限增长,无法终止。

// 无限流 + limit:可终止
Stream.generate(() -> 1).limit(5).count();   // 5
// 无限流 + sorted 在 limit 前:不终止/OOM
// Stream.generate(() -> 1).sorted().limit(5).count();  // 无限缓冲
#
★★

74. 柯里化(Currying)与部分应用在 Java 中的实现

柯里化(Currying)与部分应用在 Java 中的实现是什么?

  • 柯里化
  • 部分应用
  • Java 实现

柯里化(Currying):把多参数函数转换为一系列单参数函数(f(x,y,z) -> f(x)(y)(z)),Java 用 Function 嵌套实现(Function<T, Function<U, R>>)。部分应用(Partial Application):固定部分参数,返回剩余参数的函数(如 f(x,y) 固定 x 后得到以 y 为参数的函数),Java 用闭包捕获固定参数实现。Java 实现示例:柯里化 Function<Integer, Function<Integer, Integer>> add = a -> b -> a + b;,部分应用 Function<Integer, Integer> add5 = add.apply(5);。柯里化与部分应用让函数可复用、惰性接收参数、组合。Java 中柯里化不常见(需手动嵌套),但函数式风格可利用。

柯里化用 Function 嵌套把多参转单参链,部分应用用闭包固定部分参数,Java 用手动嵌套实现。

// 柯里化
Function<Integer, Function<Integer, Integer>> add = a -> b -> a + b;
add.apply(1).apply(2);   // 3
// 部分应用
Function<Integer, Integer> add5 = add.apply(5);
add5.apply(3);   // 8
#
★★

75. 流式管道复用(多次 terminal)的失败原因

流式管道复用(多次 terminal)的失败原因是什么?

  • 多次 terminal
  • 一次性
  • 失败原因

流式管道复用(多次 terminal)失败:Stream 是一次性的,执行一次 terminal 后"已消费"(state 为已操作/已关闭),再次执行 terminal 抛 IllegalStateException。失败原因:Stream 是惰性管道,terminal 操作消费数据源(迭代元素),内部状态标记为已消费;Stream 设计为单次消费,防止重复迭代/副作用。要复用管道逻辑,应把中间操作封装为"重建函数"(Supplier 或方法),每次重新创建 Stream。不能复用已 terminal 的 Stream,需从数据源重新构建。

Stream 一次性,terminal 后已消费,再次操作抛 IllegalStateException,需把管道封装为重建函数每次新建。

// 失败:多次 terminal
Stream<String> s = list.stream().filter(x -> x.length() > 2);
s.count();   // 消费
s.findFirst();   // 抛 IllegalStateException
// 正确:封装为重建函数
Supplier<Stream<String>> sup = () -> list.stream().filter(x -> x.length() > 2);
sup.get().count();
sup.get().findFirst();
#
★★

76. distinct 的底层实现(HashSet/LinkedHashSet)在顺序流与并行流中的内存与性能特征

distinct 的底层实现(HashSet/LinkedHashSet)在顺序流与并行流中的内存与性能特征是什么?

  • distinct 底层
  • 顺序/并行
  • 内存与性能

distinct 的底层实现:顺序流用"有序去重"(LinkedHashSet 或按遇见顺序去重,保序),并行流用"并发去重"(ConcurrentHashMap 或合并去重,无序或保序需合并)。内存特征:distinct 需缓冲已见元素(去重集合),内存占用与"不同元素数"成正比(去重存储哈希),大数据下内存压力大。性能特征:顺序流 distinct 用 HashSet 去重 O(n)(哈希),保序用 LinkedHashSet;并行流 distinct 各线程先本地去重(O(n/p)),再合并去重(需合并,有开销),unordered 时并行更高效(省顺序合并)。distinct 是有状态操作,需缓冲+合并,内存与并行屏障开销。

distinct 用哈希去重(顺序 LinkedHashSet 保序、并行并发去重+合并),内存与不同元素数成正比,并行有合并开销。

// distinct 底层:去重集合(哈希)
list.stream().distinct().toList();   // 顺序保序(LinkedHashSet 语义)
list.parallelStream().distinct().toList();   // 并行并发去重+合并
#
★★

77. flatMap 实现嵌套循环与笛卡尔积时的中间 Stream 数量与资源开销控制

flatMap 实现嵌套循环与笛卡尔积时的中间 Stream 数量与资源开销控制是什么?

  • flatMap 嵌套
  • 笛卡尔积
  • 中间 Stream 开销

flatMap 实现嵌套循环(笛卡尔积)时,每个外层元素调用一次 flatMap 的映射函数,产生一个中间 Stream;笛卡尔积(外层 x 内层)会产生"外层元素数量"个中间 Stream,中间 Stream 对象分配与每次展开的开销大,尤其内层元素多时笛卡尔积元素数 = 外层 x 内层,资源开销(Stream 对象、元素)大。控制:1)用 mapMulti 替代(回调避免中间 Stream 对象);2)减少嵌套层数(笛卡尔积爆炸);3)用索引/循环生成而非 flatMap 嵌套;4)惰性避免提前展开。笛卡尔积元素数随维度增长,需控制数据规模与中间 Stream 数量。

flatMap 嵌套笛卡尔积产生大量中间 Stream(每外层元素一个),元素数随维度爆炸,用 mapMulti 或控制规模。

// 笛卡尔积:每外层元素产生中间 Stream
list1.stream().flatMap(a -> list2.stream().map(b -> a + "-" + b)).toList();   // N*M 元素
// 开销:N 个中间 Stream
// 控制:mapMulti 或限制规模
#

78. 自定义 Collector 的五要素(supplier/accumulator/combiner/finisher/characteristics)与 toList 实现

自定义 Collector 的五要素(supplier/accumulator/combiner/finisher/characteristics)是什么?toList 如何实现?

  • Collector 五要素
  • supplier/accumulator/combiner/finisher/characteristics
  • toList 实现

Collector 五要素:1)supplier:创建结果容器(工厂);2)accumulator:把元素累加到容器(BiConsumer);3)combiner:合并两个容器(并行归约,BinaryOperator);4)finisher:最终转换(容器→结果,Function);5)characteristics:特征(CONCURRENT、UNORDERED、IDENTITY_FINISH)。Collectors.toList() 实现:supplier 返回 ArrayList,accumulator 用 List.add,combiner 用 addAll 合并,finisher 是 identity(IDENTITY_FINISH,容器即结果),characteristics 含 IDENTITY_FINISH。自定义 Collector 需实现五要素,理解各要素职责(创建/累加/合并/转换/特征)。

Collector 五要素:supplier 建容器、accumulator 累加、combiner 合并、finisher 转换、characteristics 特征,toList 用 ArrayList + add + addAll + identity。

Collector<String, List<String>, List<String>> c = Collector.of(
    ArrayList::new,                 // supplier
    List::add,                      // accumulator
    (l1, l2) -> { l1.addAll(l2); return l1; },  // combiner
    Collector.Characteristics.IDENTITY_FINISH);  // characteristics
// 等价于 Collectors.toList()
#

79. 自定义 Gatherer 支持并行执行时,合并器必须满足哪些约束,状态不可合并时应如何声明

自定义 Gatherer 支持并行执行时,合并器必须满足哪些约束?状态不可合并时应如何声明?

  • Gatherer 合并器
  • 并行约束
  • 状态不可合并

自定义 Gatherer 支持并行时,合并器(combiner)必须满足:1)可结合(associative):合并顺序不影响结果((a合并b)合并c == a合并(b合并c));2)与初始状态一致:合并器与 supplier 的初始状态兼容;3)无副作用、确定性:合并操作不修改输入的流状态副作用。若状态不可合并(如全局唯一、顺序敏感的有状态处理),Gatherer 应声明"不支持并行"——通过不提供可结合的合并器,或使用 Gatherer 初始状态/特征告知框架。Gatherer 的 initializer 返回初始状态,若状态不可合并,框架会退化为顺序执行(或声明不支持 CONCURRENT)。正确声明:状态可合并时提供可结合 combiner 支持并行,不可合并时不声明并行/顺序处理。

并行 Gatherer 合并器需可结合、与初始状态一致、无副作用;状态不可合并时退化为顺序执行不声明并行。

// 可合并状态:提供可结合 combiner
Gatherer.of(ArrayList::new,
    (list, elem, downstream) -> { list.add(elem); downstream.push(list); return true; },
    (l1, l2) -> { l1.addAll(l2); return l1; });   // combiner 可结合
// 状态不可合并:不提供 combiner(顺序执行)
#

80. 自定义 Spliterator 错误声明 SIZED、SORTED 或 DISTINCT 特征,会使 Stream 得出哪些错误假设

自定义 Spliterator 错误声明 SIZED、SORTED 或 DISTINCT 特征,会使 Stream 得出哪些错误假设?

  • Spliterator 特征
  • 错误声明
  • Stream 错误假设

自定义 Spliterator 错误声明特征会导致 Stream 错误假设:1)错误声明 SIZED:Stream 假设元素数已知(estimateSize 精确),用于拆分/短路/缓冲区大小,实际元素数不符会错误(拆分不均衡、容量错);2)错误声明 SORTED:Stream 假设元素已排序,跳过 sorted 操作(或 takeWhile 等利用顺序),实际无序会得到错误结果;3)错误声明 DISTINCT:Stream 假设元素唯一,跳过 distinct 去重,实际有重复会得到错误结果。错误声明特征让 Stream 基于错误假设优化,导致并行结果错误或操作结果错误。自定义 Spliterator 应准确声明特征,否则产生错误。

错误声明 SIZED/SORTED/DISTINCT 让 Stream 假设元素数/排序/唯一,跳过相关操作或拆分错误,产生错误结果。

// 错误声明 SORTED:Stream 跳过 sorted,实际无序
// 错误声明 DISTINCT:Stream 跳过 distinct,实际有重复
// 错误声明 SIZED:estimateSize 错误,拆分/容量错误
// 自定义 Spliterator 需准确声明特征
#

81. 虚拟线程适合大量阻塞任务,而并行 Stream 适合数据并行,两者的选型依据是什么

虚拟线程适合大量阻塞任务,而并行 Stream 适合数据并行,两者的选型依据是什么?

  • 虚拟线程
  • 并行 Stream
  • 选型依据

虚拟线程与并行 Stream 选型依据:虚拟线程适合"大量阻塞任务"(I/O 密集、每个任务阻塞等待外部响应),虚拟线程轻量(大量线程)、阻塞时让出载体线程,不浪费资源;并行 Stream 适合"数据并行"(CPU 密集、对大量数据分块并行计算),利用多核并行处理数据元素。选型:任务级并发(每个任务独立、阻塞 I/O)用虚拟线程;数据级并行(对集合元素并行计算)用并行 Stream。若任务是"对每个元素做阻塞 I/O",用虚拟线程(每个元素一个虚拟线程)优于并行 Stream(阻塞占 commonPool 线程)。核心:阻塞/任务并发用虚拟线程,纯计算/数据分块用并行 Stream。

虚拟线程适合大量阻塞任务(I/O 密集、任务并发),并行 Stream 适合数据并行(CPU 密集、数据分块),按任务/数据形态选型。

// 阻塞任务:虚拟线程
try (var es = Executors.newVirtualThreadPerTaskExecutor()) {
    urls.forEach(u -> es.submit(() -> fetch(u)));
}
// 数据并行:CPU 密集
long sum = LongStream.range(0, 1_000_000).parallel().sum();
#

82. Stream reduce 的 identity、accumulator 与 combiner 需要满足哪些代数性质才能保证确定结果

Stream reduce 的 identity、accumulator 与 combiner 需要满足哪些代数性质才能保证确定结果?

  • reduce 性质
  • identity 恒等
  • accumulator/combiner 可结合

Stream reduce 的 identity、accumulator、combiner 需满足代数性质:1)identity 恒等:identity + element == element(accumulator 的恒等元),使空流结果为 identity;2)accumulator(BiFunction)可结合(associative):(a op b) op c == a op (b op c),保证归约顺序无关;3)combiner 与 accumulator 一致:combiner(并行)与 accumulator 语义一致且可结合;4)与 identity 兼容:combiner(identity, x) == x。满足这些性质(恒等 + 可结合 + 一致)才能保证并行归约结果确定(与顺序无关)。若违反(如减法、非恒等 identity),结果不确定。

reduce 需 identity 恒等、accumulator/combiner 可结合且与 identity 兼容,才能保证并行归约确定。

// 满足性质:identity 0,累加可结合
int sum = list.stream().reduce(0, Integer::sum);   // 0 恒等,加法可结合
// 违反:identity 非恒等(如 1 作加法 identity)或减法不可结合 -> 结果不确定
#

83. 并行流默认使用 ForkJoinPool.commonPool 时,阻塞 I/O 会怎样影响其他业务的并行任务

并行流默认使用 ForkJoinPool.commonPool 时,阻塞 I/O 会怎样影响其他业务的并行任务?

  • commonPool 共享
  • 阻塞 I/O
  • 影响其他任务

并行流默认用 ForkJoinPool.commonPool(共享公共池,线程 = CPU 核数 - 1)。若并行流中做阻塞 I/O,阻塞线程会占满 commonPool 的有限线程,导致:1)其他业务的并行任务(也用 commonPool)因线程被占而无法执行/饥饿;2)commonPool 线程数有限,阻塞占线程降低所有并行任务的并行度;3)长时间阻塞可能耗尽 commonPool 线程,其他并行任务等待。因此阻塞 I/O 不应放在并行流(commonPool),应使用虚拟线程、自定义 ForkJoinPool(隔离)、或异步。影响:阻塞 I/O 占 commonPool 线程,影响共享池的其他并行任务。

并行流用共享 commonPool,阻塞 I/O 占有限线程,导致其他并行任务饥饿/并行度下降,应避免阻塞或隔离池。

// 阻塞 I/O 占 commonPool 线程
list.parallelStream().forEach(x -> blockingIo(x));   // 影响其他并行任务
// 隔离:自定义 ForkJoinPool
ForkJoinPool pool = new ForkJoinPool(4);
pool.submit(() -> list.parallelStream().forEach(...)).join();
#

84. Gatherers.windowFixed 与 windowSliding 对尾部不足窗口的处理有何不同,内存开销如何估算

Gatherers.windowFixed 与 windowSliding 对尾部不足窗口的处理有何不同?内存开销如何估算?

  • windowFixed
  • windowSliding
  • 尾部处理与内存

Gatherers.windowFixed(n) 与 windowSliding(n)(JDK 22+)都是窗口化 Gatherer:windowFixed(n) 把元素分成固定大小 n 的窗口(非重叠),尾部不足 n 个元素的窗口会被丢弃(不输出);windowSliding(n) 生成滑动窗口(每个元素起一个窗口,窗口重叠),尾部不足 n 的窗口也会输出(因为滑动窗口覆盖每个位置)。差异:windowFixed 尾部不足丢弃(非重叠固定窗口),windowSliding 尾部不足也输出(重叠滑动)。内存开销:windowFixed 缓冲至多 n 个元素(窗口大小),windowSliding 缓冲 n 个元素(滑动窗口),内存 O(n);窗口大小 n 决定内存上界。估算:内存约 n 个元素(窗口容量)。

windowFixed 固定非重叠窗口(尾部不足丢弃),windowSliding 滑动重叠窗口(尾部不足也输出),内存 O(n)(窗口大小)。

List<Integer> list = List.of(1,2,3,4,5);
list.stream().gather(Gatherers.windowFixed(3)).toList();    // [[1,2,3],[4,5]? ] 尾部不足丢弃 -> [[1,2,3]]
list.stream().gather(Gatherers.windowSliding(3)).toList();  // [[1,2,3],[2,3,4],[3,4,5]]
#

85. JDK 25 Stream Gatherers 在 ETL 场景下的窗口化与状态化处理优势

JDK 25 Stream Gatherers 在 ETL 场景下的窗口化与状态化处理优势是什么?

  • Stream Gatherers
  • ETL 场景
  • 窗口化/状态化

Stream Gatherers(JEP 485,JDK 24 定稿)在 ETL(提取-转换-加载)场景的优势:1)窗口化:Gatherers.windowFixed/windowSliding 按窗口批处理数据(如批量写入、滑窗聚合),一次处理窗口批量;2)状态化:自定义 Gatherer 维护状态做序列化处理(去重、累积、状态机),替代复杂的有状态中间操作;3)组合:Gatherers 可组合(andThen)构建复杂转换管道;4)mapConcurrent 并发转换。ETL 场景优势:用 Gatherer 表达"窗口批处理 + 状态累积 + 并发转换",比 mapReduce 更灵活、可读、声明式,替代手写循环。适合数据清洗、批量加载、流式聚合。

Stream Gatherers 在 ETL 支持窗口批处理(windowFixed)、状态化累积(自定义 Gatherer)、组合与并发,声明式处理。

// ETL:窗口批量写入
rows.stream().gather(Gatherers.windowFixed(100)).forEach(batch -> batchInsert(batch));
// 状态化:去重/累积
data.stream().gather(Gatherers.scan(0, (acc, x) -> acc + x)).toList();
#

86. JDK 24 定稿的 JEP 485 Stream Gatherers 自定义中间操作的 API 设计要点是什么

JDK 24 定稿的 JEP 485 Stream Gatherers 自定义中间操作的 API 设计要点是什么?

  • JEP 485
  • Gatherer API
  • 设计要点

JEP 485(Stream Gatherers,JDK 24 定稿)的 API 设计要点:引入 Stream.gather(Gatherer) 方法,Gatherer 接口定义自定义中间操作的五要素:1)initializer(Supplier,可选):创建初始状态;2)integrator(Integrator,核心):逐元素处理,可发出 0/1/多个元素、请求短路(返回 false);3)combiner(可结合):合并行状态;4)finisher(可选):收尾转换。Gatherers 提供预置操作(windowFixed、windowSliding、mapConcurrent、scan、fold)和 Gatherer.of 工厂。设计要点:状态化、可并行(combiner)、短路、组合(andThen)、惰性(与 Stream 惰性一致)。Gatherer 是 Stream 中间操作的扩展点,其 API 关注状态、输出、并行、组合。

JEP 485 的 Gatherer API 含 initializer/integrator/combiner/finisher,支持状态化、并行、短路、组合,是自定义中间操作扩展点。

// Gatherer 接口核心
Gatherer<Integer, ?, Integer> g = Gatherer.of(
    () -> new ArrayList<>(),                       // initializer
    (state, elem, downstream) -> { ...; return true; },  // integrator
    (s1, s2) -> s1,                               // combiner
    (state, downstream) -> { ... });              // finisher
// 使用:stream.gather(g)
#

87. JEP 485 将 Stream Gatherers 定稿后,Gatherer 的初始化器、集成器、合并器和完成器各负责什么

JEP 485 将 Stream Gatherers 定稿后,Gatherer 的初始化器、集成器、合并器和完成器各负责什么?

  • Gatherer 四部分
  • initializer/integrator/combiner/finisher
  • 职责

JEP 485 定稿后 Gatherer 四部分职责:1)initializer(初始化器):创建初始状态(Supplier),为每个流/子任务提供初始状态;2)integrator(集成器):对每个元素处理,更新状态、通过 downstream 发出 0/1/多个元素、返回 boolean 请求短路(true 继续,false 停止上游);3)combiner(合并器):合并行子任务的状态(可结合),用于并行流;4)finisher(完成器):流结束后收尾处理(可把最终状态转换为输出、发出最终元素)。职责清晰:initializer 建状态、integrator 逐元素处理、combiner 并行合并、finisher 收尾。四个部分协作实现状态化自定义中间操作。

initializer 建状态、integrator 逐元素处理(可发元素/短路)、combiner 并行合并、finisher 收尾,协作实现状态化操作。

Gatherer.of(
    () -> 0,                       // initializer 初始状态
    (s, elem, down) -> { s += elem; down.push(s); return true; },  // integrator
    (a, b) -> a + b,               // combiner 合并
    (s, down) -> { System.out.println("done " + s); });  // finisher 收尾
#

88. 嵌套 parallelStream 为什么可能争用同一个公共池,如何通过基准确认过度并行问题

嵌套 parallelStream 为什么可能争用同一个公共池?如何通过基准确认过度并行问题?

  • 嵌套 parallelStream
  • commonPool 争用
  • 基准确认

嵌套 parallelStream 争用同一个公共池:所有 parallelStream 都使用 ForkJoinPool.commonPool(共享),嵌套的并行流(外层并行流内又用并行流)会"递归"提交任务到同一 commonPool,产生:1)线程争用:外层与内层任务抢 commonPool 的有限线程,内层并行度受限;2)过度并行:任务数超过线程数,任务排队/线程切换开销;3)可能死锁/饥饿:外层阻塞等待内层,内层等线程,commonPool 线程占满时死锁。基准确认过度并行:用 JMH 或并发监控测量:对比"单层并行"与"嵌套并行"的吞吐/延迟;观察 commonPool 活动线程数(ForkJoinPool.commonPool().getActiveThreadCount())与排队;用 JFR 看线程利用率。确认嵌套并行是否造成线程争用与性能下降。

嵌套 parallelStream 共用 commonPool,产生线程争用、过度并行、可能死锁,用 JMH/线程监控基准确认。

// 嵌套并行:共用 commonPool
list.parallelStream().forEach(x -> innerList.parallelStream().forEach(y -> compute(x, y)));
// 基准确认:对比单层/嵌套吞吐,观察 commonPool 活动线程
// ForkJoinPool.commonPool().getActiveThreadCount()
#

89. 如何将并行流提交到自定义 ForkJoinPool 而非 commonPool,及由此带来的收益与风险

如何将并行流提交到自定义 ForkJoinPool 而非 commonPool?由此带来的收益与风险是什么?

  • 自定义 ForkJoinPool
  • 并行流提交
  • 收益与风险

将并行流提交到自定义 ForkJoinPool:用 ForkJoinPool 包装,customPool.submit(() -> list.parallelStream().forEach(...)).join(),因为并行流使用"当前线程的 ForkJoinPool"(若在 ForkJoinPool 任务内执行则用该池),submit 到自定义池后并行任务用自定义池。收益:1)隔离:不占 commonPool,避免阻塞/竞争影响其他并行任务;2)控制并发度:自定义池大小决定并行度;3)避免死锁(阻塞任务不占 commonPool)。风险:1)自定义池管理开销(创建/关闭);2)submit 需正确 join,否则异常丢失;3)并行度匹配池大小,过大/过小影响性能;4)需关闭池(资源)。收益是隔离与可控,风险是资源管理与复杂度。

用 ForkJoinPool.submit 包裹并行流使其用自定义池,收益是隔离与并发度可控,风险是资源管理与 join 处理。

ForkJoinPool pool = new ForkJoinPool(4);
try {
    pool.submit(() -> list.parallelStream().forEach(x -> blockingIo(x))).join();
} finally {
    pool.shutdown();
}