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


问题内容

假设我有以下代码:

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

这将打印[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));

以前面的示例为参考,我希望这个示例可以显示:

["a", "b", "c"]

但是,它对我来说有点出乎意料,它会打印:

[a, null, c]

而且,当然,我去了明确表明这是预期的文档:

如果此集合适合于具有剩余空间的指定数组(即,数组中的元素多于该集合),则紧接该集合结尾的数组中的元素将设置为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));

这将打印:

[z, null, z1, null]

因此,它清除了数组中的最后一个元素,但是为什么在第一个示例中不这样做呢?

有人可以在这里说些什么吗?


问题答案:

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

首先,让我们看一下toArray()。这将从集合中获取元素,并将其返回到中Object[]。也就是说,返回数组的组件类型始终为Object。这很有用,但不能满足其他几个用例:

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

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

处理情况(1)原来是一个相当微妙的API问题。调用方想重用一个数组,因此显然需要传递它。与no-arg toArray()方法不同,no-arg
方法返回正确大小的数组,如果调用方的数组被重用,我们需要一种方法返回复制的元素数。好的,让我们有一个看起来像这样的API:

int toArray(T[] a)

调用者传入一个数组,该数组被重用,返回值是复制到其中的元素数。不需要返回该数组,因为调用者已经拥有对该数组的引用。但是,如果数组太小怎么办?好吧,也许抛出一个例外。实际上,这就是Vector.copyInto所做的。

void copyInto​(Object[] anArray)

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

synchronized (vec) {
    Object[] a = new Object[vec.size()];
    vec.copyInto(a);
}

啊!

Collections.toArray(T[])如果目标数组太小,API会通过采取不同的行为来避免此问题。不会抛出类似Vector.copyInto()的异常,而是分配一个大小合适的

数组。这样就折衷了阵列重用的情况,以获得更可靠的操作。现在的问题是,调用者无法确定其数组是被重用还是分配了新的数组。因此,的返回值toArray(T[])需要返回一个数组:参数数组(如果足够大)或新分配的数组。

但是现在我们还有另一个问题。我们不再有办法告诉调用者从集合中复制到数组中的元素数量。如果目标数组是新分配的,或者数组恰好是正确的大小,则数组的长度就是复制的元素数。如果目标阵列比复制元件的数量较大,则该方法尝试进行通信,以复制的元素的数目的呼叫者,通过写null到阵列位置
一个超出
从集合中复制的最后一个元素。如果知道源集合没有空值,则调用者可以确定复制的元素数。调用之后,调用方可以搜索数组中的第一个空值。如果存在,则其位置确定要复制的元素数。如果数组中没有null,则知道复制的元素数等于数组的长度。

坦白说,这真是la脚。但是,考虑到当时对语言的限制,我承认我没有更好的选择。

我认为我从未见过任何重用数组或以这种方式检查null的代码。这可能是内存分配和垃圾回收昂贵的早期阶段的一个保留,因此人们希望尽可能多地重用内存。最近,使用此方法的惯用语已成为上述第二个用例,即按如下所述建立所需的数组组件类型:

MyType[] a = coll.toArray(new MyType[0]);

(为此目的分配一个零长度的数组似乎很浪费,但是事实证明,JIT编译器可以优化这种分配,并且明显的替代方案toArray(new MyType[coll.size()])实际上要慢一些。这是因为需要将数组初始化为,然后将其填充为集合的内容。有关此主题,请参见Alexey
Shipilev的文章 “古代智慧的数组” 。)

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

MyType[] a = coll.toArray(MyType[]::new);

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