集合emptyList/singleton/singletonList/List/Set toArray

Eug*_*ene 12 java collections arraylist

假设我有这个代码:

String[] left = { "1", "2" };
String[] leftNew = Collections.emptyList().toArray(left);
System.out.println(Arrays.toString(leftNew));
Run Code Online (Sandbox Code Playgroud)

这将打印[null, 2].这个排序是有道理的,因为我们有一个空列表它是某种假设来处理那些我们传递一个数组,它是更大的,并设置为空的第一个元素的事实.这可能是说第一个元素在空列表中不存在,因此设置为null.

但这仍然令人困惑,因为我们传递一个具有某种类型的数组只是为了帮助推断返回数组的类型; 但无论如何,这是至少具有某种逻辑的东西.但是,如果我这样做:

String[] right = { "nonA", "b", "c" };
// or Collections.singletonList("a");
// or a plain List or Set; does not matter
String[] rightNew = Collections.singleton("a").toArray(right);
System.out.println(Arrays.toString(rightNew));
Run Code Online (Sandbox Code Playgroud)

以上一个例子为参考,我希望这个例子能够显示:

["a", "b", "c"]
Run Code Online (Sandbox Code Playgroud)

但是,对我来说有点意外,它打印:

[a, null, c]
Run Code Online (Sandbox Code Playgroud)

当然,我会转到明确说明这是预期的文档:

如果此集合适合指定的数组,并且有空余空间(即,数组的元素多于此集合),则紧跟集合结尾的数组中的元素将设置为null.

好的,好的,至少记录在案.但它后来说:

仅当调用者知道此集合不包含任何null元素时,这在确定此集合的长度时非常有用.

这是文档中最让我困惑的部分:|

一个更有趣的例子对我来说没什么意义:

String[] middle = { "nonZ", "y", "u", "m" };
List<String> list = new ArrayList<>();
list.add("z");
list.add(null);
list.add("z1");
System.out.println(list.size()); // 3

String[] middleNew = list.toArray(middle);
System.out.println(Arrays.toString(middleNew));
Run Code Online (Sandbox Code Playgroud)

这将打印:

[z, null, z1, null]
Run Code Online (Sandbox Code Playgroud)

所以它清除了数组中的最后一个元素,但为什么它不会在第一个例子中那样做呢?

有人能在这里说清楚吗?

Stu*_*rks 12

<T> T[] toArray(T[] a)关于Collection 的方法很奇怪,因为它试图同时实现两个目的.

首先,我们来看看toArray().这将从集合中获取元素并将其返回到Object[].也就是说,返回数组的组件类型总是如此Object.这很有用,但它不满足其他一些用例:

1)如果可能,调用者想要重用现有的数组; 和

2)调用者想要指定返回数组的组件类型.

处理案例(1)证明是一个相当微妙的API问题.调用者想要重用一个数组,所以它显然需要传入.与no-arg toArray()方法不同,它返回一个正确大小的数组,如果调用者的数组被重用,我们需要一个方法来返回复制的元素数.好的,我们有一个看起来像这样的API:

int toArray(T[] a)
Run Code Online (Sandbox Code Playgroud)

调用者传入一个重用的数组,返回值是复制到其中的元素数.不需要返回该数组,因为调用者已经有了对它的引用.但是如果阵列太小会怎么样?好吧,也许抛出异常.事实上,这就是Vector.copyInto的作用.

void copyInto?(Object[] anArray)
Run Code Online (Sandbox Code Playgroud)

这是一个糟糕的API.它不仅不返回复制的元素数量,而且IndexOutOfBoundsException如果目标数组太短也会抛出.由于Vector是并发集合,因此调用之前的大小可能会随时更改,因此调用方无法保证目标数组的大小足够大,也无法确定复制的元素数.调用者唯一能做的就是围绕整个序列锁定Vector:

synchronized (vec) {
    Object[] a = new Object[vec.size()];
    vec.copyInto(a);
}
Run Code Online (Sandbox Code Playgroud)

啊!

Collections.toArray(T[])如果目标数组太小,API会通过具有不同的行为来避免此问题.它不是像Vector.copyInto()那样抛出异常,而是分配一个大小合适的数组.这将换掉阵列重用案例,以实现更可靠的操作.现在的问题是,调用者无法判断其数组是否已被重用或是否已分配新数组.因此,返回值toArray(T[])需要返回一个数组:参数数组,如果它足够大,或者新分配的数组.

但现在我们还有另一个问题.我们不再有办法告诉调用者从集合中复制到数组中的元素数量.如果目标数组是新分配的,或者数组恰好是正确的大小,则数组的长度是复制的元素数.如果目标数组大于复制的元素数,则该方法尝试通过null向数组位置写入超出从集合中复制的最后一个元素的数量来向调用方传递复制的元素数.如果已知源集合没有空值,则可以使调用者确定复制的元素数.调用之后,调用者可以搜索数组中的第一个空值.如果有,则其位置确定复制的元素数.如果数组中没有null,则它知道复制的元素数等于数组的长度.

坦率地说,这非常蹩脚.但是,考虑到当时语言的限制,我承认我没有更好的选择.

我不认为我曾经见过任何重用数组或以这种方式检查空值的代码.这可能是从内存分配和垃圾收集昂贵的早期开始的延续,因此人们希望尽可能多地重用内存.最近,使用该方法的公认惯用法是上述第二个用例,即如下建立数组的所需组件类型:

MyType[] a = coll.toArray(new MyType[0]);
Run Code Online (Sandbox Code Playgroud)

(为此目的分配零长度数组似乎很浪费,但事实证明,这种分配可以通过JIT编译器进行优化,显而易见的替代方案toArray(new MyType[coll.size()])实际上更慢.这是因为需要将数组初始化为nulls,然后用集合的内容填充它.参见Alexey Shipilev关于这个主题的文章,Ancients的智慧阵列.)

但是,很多人发现零长度阵列违反直觉.在JDK 11中,有一个新的API允许用户使用数组构造函数引用:

MyType[] a = coll.toArray(MyType[]::new);
Run Code Online (Sandbox Code Playgroud)

这使调用者可以指定数组的组件类型,但它允许集合提供大小信息.