集合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的事实。 这可能是说第一个元素在空列表中不存在,因此它设置为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 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] 

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

有人可以在这里说清楚吗?

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

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

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

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

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

 int toArray(T[] a) 

调用者传入一个重用的数组,返回值是复制到其中的元素数。 不需要返回该数组,因为调用者已经有了对它的引用。 但是如果arrays太小会怎么样? 好吧,也许抛出exception。 事实上,这就是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()那样抛出exception,而是分配一个大小合适的数组。 这样可以消除arrays重用情况,从而实现更可靠的操作。 现在的问题是,调用者无法判断其数组是否已被重用或是否已分配新数组。 因此, toArray(T[])的返回值需要返回一个数组:参数数组(如果足够大)或新分配的数组。

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

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

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

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

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

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

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

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

它只会清除原始列表中最后一个元素之后的索引中的元素,因此在第一个示例中,列表为空,因此它会使索引为零的元素(第一个元素为"1" )无效。

在上一个例子中,最后一个元素恰好是原始列表中最后一个元素之后的元素。 知道最后一个场景确实无法帮助确定列表的大小,因为它确实允许空值。

但是如果列表不允许null(例如Java 9中引入的不可变列表 ),那么这很有用,因为如果你循环遍历返回的数组, 你不会想要处理额外的元素 ,在这种情况下你可以停止第一个null元素的迭代器。

ArrayList的JDK 9源代码:

 @SuppressWarnings("unchecked") public  T[] toArray(T[] a) { if (a.length < size) // Make a new array of a's runtime type, but my contents: return (T[]) Arrays.copyOf(elementData, size, a.getClass()); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; } 

Arrays.ArrayListArrays.asList返回的List实现:

 @Override @SuppressWarnings("unchecked") public  T[] toArray(T[] a) { int size = size(); if (a.length < size) return Arrays.copyOf(this.a, size, (Class) a.getClass()); System.arraycopy(this.a, 0, a, 0, size); if (a.length > size) a[size] = null; return a; } 

如果要转换为数组的列表sizesize ,则它们都将a[size]设置为null

使用空列表时, size0因此a[0]设置为null ,并且不触摸其他元素。

使用单例列表时, size1因此a[1]设置为null ,并且不触及其他元素。

如果列表的大小比数组的长度小1,则a[size]引用数组的最后一个元素,因此将其设置为null 。 在您的示例中,您在第二个位置(索引1)中有一个null ,因此将其设置为null作为元素。 如果有人在寻找null来计算元素,那么它们将停在这里而不是另一个null ,这是因为将列表内容之外的下一个元素设置为null而产生的null 。 这些null不能分开讲述。

(例如)ArrayList的toArray(T [] a)代码非常清楚:

 public  T[] toArray(T[] a) { if (a.length < size) // Make a new array of a's runtime type, but my contents: return (T[]) Arrays.copyOf(elementData, size, a.getClass()); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; } 

如果输入数组的大小大于此列表(这意味着我们可以将所有列表的内容复制到此数组中,因为它的长度足够大),那么在所有列表内容复制之后,数组中的下一个元素引用(实际上索引等于列表的大小)将设置为null。