为何Stream.toList需复制Arrays.asList结果?不做防御拷贝有何问题?
你看到的Stream.toList实现是:
Collections.unmodifiableList(new ArrayList<>(Arrays.asList(this.toArray())))
你疑惑为什么要把Arrays.asList的结果拷贝到新ArrayList中,直接用下面的代码行不行,以及不做防御拷贝会有什么问题:
Collections.unmodifiableList(Arrays.asList(this.toArray()))
不做防御拷贝的核心问题
要搞清楚这个问题,得先明白Arrays.asList()的本质:
Arrays.asList()返回的并不是我们常用的java.util.ArrayList,而是Arrays类内部的私有静态内部类ArrayList。这个内部类直接持有传入数组的引用,和原数组共享底层数据。- 这个内部类不支持
add、remove等修改列表结构的操作,调用会直接抛出UnsupportedOperationException,但它的元素内容会随着原数组的修改而变化。
如果跳过防御拷贝,直接返回Collections.unmodifiableList(Arrays.asList(this.toArray())),会带来两个关键问题:
1. 列表内容可能被外部意外修改
Stream.toList()的设计初衷是返回流元素的固定快照,也就是一个内容不可变的列表。但如果用Arrays.asList()包装数组,只要有人能拿到该数组的引用并修改元素,返回的“不可变列表”内容就会跟着变:
// 模拟这种场景的示例 String[] streamArray = {"foo", "bar"}; List<String> badList = Collections.unmodifiableList(Arrays.asList(streamArray)); // 修改原数组 streamArray[0] = "baz"; // 此时badList的内容也变成了["baz", "bar"] System.out.println(badList);
这完全违背了用户对“不可变列表”的预期——用户以为拿到的是固定结果,却可能被外部操作悄悄改变。
2. 违背不可变集合的语义约定
Collections.unmodifiableList()的作用是阻止外部通过列表本身的方法修改结构或内容,但它无法阻止底层数据源的修改。如果底层是Arrays$ArrayList,它和数组绑定的特性会让unmodifiableList的“不可变”变成伪命题,因为数据的控制权不在列表手里。
而通过new ArrayList<>(Arrays.asList(...))做防御拷贝,会把数组中的元素全部复制到独立的java.util.ArrayList实例中,这个新列表和原数组彻底解绑。之后再用Collections.unmodifiableList()包装,才真正实现了内容和结构都不可变的列表,完全符合Stream.toList()的设计目标。
内容的提问来源于stack exchange,提问作者k314159

