为什么toArray在java中是这样实现的?

Why is toArray implemented like this in java?

我看到源码:java.util.AbstractCollection.toArray(),是这样实现的:

 public Object[] toArray() {
    // Estimate size of array; be prepared to see more or fewer elements
    Object[] r = new Object[size()];
    Iterator<E> it = iterator();
    for (int i = 0; i < r.length; i++) {
        if (! it.hasNext()) // fewer elements than expected
            return Arrays.copyOf(r, i);
        r[i] = it.next();
    }
    return it.hasNext() ? finishToArray(r, it) : r;
}

private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
    int i = r.length;
    while (it.hasNext()) {
        int cap = r.length;
        if (i == cap) {
            int newCap = cap + (cap >> 1) + 1;
            // overflow-conscious code
            if (newCap - MAX_ARRAY_SIZE > 0)
                newCap = hugeCapacity(cap + 1);
            r = Arrays.copyOf(r, newCap);
        }
        r[i++] = (T)it.next();
    }
    // trim if overallocated
    return (i == r.length) ? r : Arrays.copyOf(r, i);
}

如你所见,实现不是那么容易理解,我的问题是:

  1. 当集合的元素在迭代过程中发生变化(大小不变)时,我会得到什么?我猜迭代器可能是某种快照。
  2. 当集合的大小改变时我会得到什么?不知道能不能正常使用

What will I get when the collection's size changed?

  • 如果集合的大小小于预期,则数组为 "reduced",在 toArray() 方法中使用 return Arrays.copyOf(r, i),如注释所示。
  • 如果集合的大小超过预期,it.hasNext() ? finishToArray(r, it) : r 调用会处理这种情况。 finishToArray 方法继续向数组中添加元素,如果需要 "expand" 其大小:计算新容量 (newCap = cap + (cap >> 1) + 1) 并且数组为 "expanded" (r = Arrays.copyOf(r, newCap)) .

我认为并非所有 Collection 实现都是线程安全的,不用担心,您可以使用以下方法使您的 Collection 同步:

Collections.synchronizedCollection(myCollection);

或者你可以看看:

https://docs.oracle.com/javase/tutorial/essential/concurrency/collections.html

编辑: Here 我找到了一个很好的解释

您只能确定迭代的结果是未定义的(除非您知道正在使用的集合的确切实现)。通常会抛出 ConcurrentModificationException,但您不能依赖该假设。

如果 Collection 在遍历它时被修改,在大多数实现中,会抛出 ConcurrentModificationExceptionIterators 这样做被称为 fail-fast 迭代器。

但这取决于每个实现,尽管 JRE 提供的所有通用集合实现都这样做,但并非所有 Iterators 都是 fail-fast。还要注意 fail-fast 行为无法得到保证,因为一般来说,在存在非同步并发修改的情况下不可能做出任何硬性保证。

Why is toArray implemented like this in java?

因为此实现假定集合的大小可以随时更改,因为迭代器可能不会抛出任何异常。 因此,此方法检查迭代器提供的元素可能多于或少于初始估计大小。

As you see,the implementation is not so easy to understand, my question is :

  1. What will I get when the collection's elements change (size not changed) during iteration? I guess the iterator may be some kind of snapshot.
  2. What will I get when the collection's size is changed? I wonder if it can work correctly.

实现是这样的,因为它旨在处理迭代器 returns 与 size() 不同数量的元素的情况。如果集合的大小在迭代过程中发生变化,就会发生这种情况。目标数组是根据 size() 分配的,在大小不变的乐观情况下,它非常简单。代码的复杂性在于迭代器返回的实际元素数与 size() 返回的初始值不同。如果元素的实际数量较少,则将元素复制到大小合适的较小数组中。如果实际数字更大,则将元素复制到更大的数组中,然后迭代更多元素。如果数组填满,则会重复重新分配更大的数组,直到迭代完成。

对于您的第一个问题,迭代器不一定对元素进行快照。这取决于实际的集合实现。一些集合(比如 CopyOnWriteArrayList)确实有快照语义,所以如果集合被修改,修改对迭代器是不可见的。在这种情况下,迭代器报告的元素数将匹配 size(),因此不需要重新分配数组。

其他集合实现对于在迭代期间修改集合时会发生什么有不同的策略。有些是 fail-fast,这意味着他们会抛出 ConcurrentModificationException。其他的弱一致,这意味着迭代器可能会或可能不会看到修改。

这适用于你的第二个问题。如果集合大小在迭代过程中发生变化,并且该集合的迭代器支持此功能(即,它不是 fail-fast),则此处的代码将处理迭代器中出现的元素数量与 [=10 最初报告的数量不同=].

一个可能发生这种情况的例子是 ConcurrentSkipListSet。这个class的迭代器是弱一致的,它继承了AbstractCollectiontoArray()方法。因此,当 toArray() 迭代集合以将元素收集到目标数组中时,另一个线程修改集合(可能会更改其大小)是完全合法的。这显然会导致迭代器报告与 size() 返回的初始值不同的元素数,这将导致执行 toArray() 中的数组重新分配代码。