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

时间:2018-08-17 20:18:42

标签: java arraylist collections

假设我有以下代码:

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

这将打印[null, 2]。这种 是有意义的,因为我们有一个空列表,所以可以某种程度上应对我们传递一个更大的数组并将第一个元素设置为null的事实。这可能是说第一个元素在空列表中不存在,因此将其设置为null

但这仍然令人困惑,因为我们传递具有特定类型的数组只是为了帮助推断 returned 数组的类型;但是无论如何,这至少具有一定的逻辑。但是,如果我这样做:

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]

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

有人可以在这里照明吗?

4 个答案:

答案 0 :(得分:12)

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

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

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

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

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

gh!

如果目标数组太小,Collections.toArray(T[]) API通过具有不同的行为来避免此问题。不会抛出类似Vector.copyInto()的异常,而是分配一个大小正确的 new 数组。这样就折衷了阵列重用的情况,以获得更可靠的操作。现在的问题是,调用者无法分辨其数组是被重用还是分配了新的数组。因此,toArray(T[])的返回值需要返回一个数组:参数数组(如果足够大)或新分配的数组。

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

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

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

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

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

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

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

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

答案 1 :(得分:3)

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

在您的最后一个示例中,碰巧最后一个元素是原始列表中最后一个元素之后的那个。知道最后一种情况并不能真正帮助确定列表的大小,因为它 did 允许使用空值。

但是如果列表不允许为空(例如immutable lists introduced in Java 9),那么这很有用,因为如果要遍历返回的数组,您将不希望处理多余的元素,在这种情况下,您可以在第一个null元素处停止迭代器。

答案 2 :(得分:2)

来自ArrayList的JDK 9源代码:

@SuppressWarnings("unchecked")
public <T> 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.ArrayList中,由List返回的Arrays.asList实现:

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

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

在列表为空的情况下,size0,因此a[0]设置为null,并且其他元素均未触及。

对于单例列表,size1,因此a[1]设置为null,并且其他元素均未触及。

如果列表的大小比数组的长度小1,则a[size]指向数组的最后一个元素,因此将其设置为null。在您的示例中,您在第二个位置(索引1)有一个null,因此将其设置为null作为元素。如果某人正在寻找null来计数元素,那么他们将在这里停下来,而不是其他null,这是将列表内容之外的下一个元素设置为{{1 }}。这些null无法区分。

答案 3 :(得分:0)

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

public <T> 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。