问题 集合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<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]

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

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


2565
2017-08-17 20:18


起源

我不明白这个问题。它们都将以下元素设置为null。不一致的地方在哪里? - shmosel
@shmosel没有不一致,真正的问题是 为什么 会发生这样的事情,是否存在(可能会是)实际利用此代码的代码? - Eugene
我猜这个假设是你可能会覆盖目标数组中的数据,在这种情况下,null值可以告诉你上限。似乎并不十分有用,tbh。 - shmosel
@shmosel这个答案和评论怎么样? stackoverflow.com/a/51902457/1059372 这更有意义吗?我认为它回答了我的问题...... - Eugene
我还不完全确定你的问题是什么。如果你问如果有前面的空值,将下一个元素设置为null是有用的,那么,它不是。这正是文档在您的第二个引用中所说的内容。 - shmosel


答案:


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

首先,我们来看看 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);
}

啊!

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

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

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

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

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

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

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

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

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


9
2017-08-17 21:30



我们不再有办法告诉调用者从集合中复制到数组中的元素数量。 为什么不检查源集合的大小? - shmosel
在JDK 11中,有一个新的API允许用户使用数组构造函数引用。 甜。出于兼容性原因,Java 8中是否被拒绝了? - shmosel
@shmosel 为什么不检查源集合的大小? 调用者可以在调用toArray()之前或之后调用size()。但是,如果另一个线程修改了源集合,则size值可能不等于toArray()复制的实际元素数。只有toArray()知道复制的元素的确切数量。当然,如果您是单线程的,这不是问题,但一般情况是源是并发集合。 - Stuart Marks
@shmosel 出于兼容性原因,Java 8中是否被拒绝了? 我不这么认为。因为我们没时间了,所以8分掉了。在9或10个开发阶段讨论了这是否是最自然的API和性能 - 明显的覆盖实际上变得更慢!我们在11个开发过程中遇到了这些问题。 - Stuart Marks
找到了: mail.openjdk.java.net/pipermail/lambda-libs-spec-experts/... - shmosel


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

在上一个例子中,最后一个元素恰好是原始列表中最后一个元素之后的元素。知道最后一个场景不会真正有助于确定列表的大小,因为它 没有 允许空值。

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


3
2017-08-17 20:28



你有道理,所以你说如果那个特别的话 List/Set 不允许 nulls,你会打电话 toArray 与 array 尺寸大于 List,同时循环 array 一旦遇到第一个null - 你就会确定你已经完成了。这正是java-9中不可变集合的情况,它禁止空值。如果你要添加这部分,我会接受它 - Eugene
但这非常令人困惑 ArrayList (它接受空值)当你遇到第一个空时,你不知道你是否完成了。好吧,至少,文档中非常清楚。 - Eugene
@Eugene是的你是对的,所以它说“只要 如果调用者知道这个集合不包含任何null元素。“ - manouti
@Eugene没什么大不了的,Stuart的答案确实更好地涵盖了它,来自原始开发者。 - manouti


来自JDK 9的源代码 ArrayList

@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.ArrayListList 实现返回 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

有一个空列表, size 是 0 所以 a[0] 被设定为 null,并没有触及其他元素。

使用单身人士名单, size 是 1 所以 a[1] 被设定为 null,并没有触及其他元素。

如果列表的大小比数组的长度小1, a[size] 指的是数组的最后一个元素,因此它被设置为 null。在你的例子中,你有一个 null 在第二个位置(索引1),所以设置为 null 作为一个元素。如果有人在找 null 要计算元素,他们会停在这里而不是另一个 null, 哪一个是 null 将下一个元素设置为超出列表内容的结果 null。这些 nulls不能被分开。


2
2017-08-17 20:30



你是对的,这是另一个答案(明智的说法)更接近我的理解。谢谢! - Eugene


(例如)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。


0
2017-08-17 21:07