【问题标题】:Why is toArray implemented like this in java?为什么toArray在java中是这样实现的?
【发布时间】:2016-01-07 15:02:58
【问题描述】:

正如我看到的: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. 当集合的大小改变时我会得到什么?我想知道它是否可以正常工作。

【问题讨论】:

标签: java collections toarray


【解决方案1】:

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

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

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

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

如果集合在迭代期间被修改,其他集合实现有不同的策略。有些是fail-fast,这意味着他们会抛出ConcurrentModificationException。其他是弱一致,这意味着修改可能对迭代器可见,也可能不可见。

这适用于您的第二个问题。如果集合大小在迭代期间发生变化,并且该集合的迭代器支持这一点(即,它不是快速失败的),则此处的代码将处理来自迭代器的元素数量与 size() 最初报告的数量不同。

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

【讨论】:

    【解决方案2】:

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

    Collections.synchronizedCollection(myCollection);
    

    或者你可以看看:

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

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

    【讨论】:

      【解决方案3】:

      当集合的大小改变时我会得到什么?

      • 如果集合的大小小于预期,则数组将使用 return Arrays.copyOf(r, i)toArray() 方法中“减少”,如注释所示。
      • 如果集合的大小超出预期,it.hasNext() ? finishToArray(r, it) : r 调用会处理这种情况。 finishToArray 方法继续向数组中添加元素并在需要时“扩展”其大小:计算新容量 (newCap = cap + (cap &gt;&gt; 1) + 1) 并“扩展”数组 (r = Arrays.copyOf(r, newCap))。

      【讨论】:

      • 你确定不会有ConcurrentModificationException吗?
      • 我猜你是对的,如果迭代开始后大小发生变化,你会得到一个异常。是这样吗?我对答案很感兴趣:)
      • @Marvin 我也想过这个问题,但是ConcurrentModificationException 可能不会在所有情况下都被抛出(并且根本不保证会被抛出)。乍一看,代码似乎试图应对这一事实,但我必须进一步消化它才能做出更深刻的答案。
      【解决方案4】:

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

      如果在迭代 Collection 时对其进行了修改,则在大多数实现中,会抛出 ConcurrentModificationExceptionIterators 这样做被称为快速失败迭代器。

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

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-10-31
        • 2011-09-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-08-29
        相关资源
        最近更新 更多