例如在 JDK17 中阅读 ArrayList 类的 OpenJDK 代码时
(https://github.com/openjdk/jdk17/blob/master/src/java.base/share/classes/java/util/ArrayList.java)
我偶然发现了以下构造函数:
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
区分
c.getClass()
是否是ArrayList.class
的原因是什么?这种情况有必要分拆吗?
(我只是想了解 Java 代码,它是 ArrayList 类的 OpenJDK 发行版的一部分。)
免责声明:我没有参与编写这段代码,也不知道有谁参与过。
您正在查看的构造函数是“复制构造函数”;它的目的是创建给定集合的浅表副本
c
。这意味着 c
的元素必须复制到新集合中,但新集合的“结构”(ArrayList
中的数组)必须不同。否则,对 c
的结构修改也将通过新集合可见,反之亦然。
这部分:
if (c.getClass() == ArrayList.class) { elementData = a; } else { elementData = Arrays.copyOf(a, size, Object[].class); }
确保新的
ArrayList
和 c
不共享同一个数组。但是 ArrayList
knows 它自己的 toArray()
实现返回一个不同的数组。所以,当c
是ArrayList
时,它避免了复制数组,从而避免了不必要的工作。否则,如果 c
是任何其他集合类型的实例,包括 ArrayList
的子类型(可以覆盖 toArray()
),那么就不能再依赖 toArray()
的知识,因此防御性的已制作数组的副本。
Collection#toArray()
的合同规定:
返回的数组将是“安全的”,因为该集合不维护对其的引用。 (换句话说,即使此集合由数组支持,此方法也必须分配一个新数组)。因此,调用者可以自由修改返回的数组。
因此,复制数组似乎是没有必要的,使得上面的代码显得多余。但我可以想到应该复制数组的两个原因:
另一种优化。
Collection#toArray()
的文档中没有提及任何有关返回数组的 length 的内容。这意味着返回的数组的容量可能大于元素的数量。上述代码进行的复制可确保数组的容量等于 c
的大小,从而可能节省内存。
Collection
可能会破坏
toArray()
的契约并保留对数组的引用,从而允许参与者意外地修改新的 ArrayList
。由于 ArrayList
是一个使用非常广泛的类,因此防范此类事情非常重要。
ArrayList
仍然
知道,它自己的
toArray()
实现返回一个容量等于元素数量的不同数组,因此在c.getClass() == ArrayList.class
时仍然可以避免复制数组。