[google/guava]使用 Lists.partition 和 remove 时出现 NoSuchElementException

2024-07-16 884 views
4

你好,

Lists.partition 的实现似乎存在错误。问题如下;假设我对一个整数列表进行了分区,如果您对分区列表执行循环,并且对每个子列表执行 removeAll() 函数,则会收到错误 (NoSuchElementException)

代码如下:

图像

似乎每当子列表被清空时,空的子列表就会从分区列表中删除,从而导致上面提到的异常,该异常是从 AbstractList.java 中的 next() 方法抛出的

回答

4

这是一个有趣的失败模式?最终我怀疑文档中关于突变的警告适用:

内部列表...遵守有关修改的所有常见警告,如 [ List.sublist()] 中所述。

话虽如此,这绝对是奇怪的行为。考虑一下:

jshell> var partitioned = Lists.partition(Lists.newArrayList(1), 2)
partitioned ==> [[1]]

jshell> var iter = partitioned.iterator()
iter ==> java.util.AbstractList$Itr@28c4711c

jshell> iter.hasNext()
$5 ==> true

jshell> iter.next().removeAll(Lists.newArrayList(1))
$6 ==> true

jshell> iter.hasNext()
$7 ==> true

jshell> iter.next()
|  Exception java.util.NoSuchElementException
|        at AbstractList$Itr.next (AbstractList.java:377)
|        at (#8:1)

partitioned仅以一个元素开始!为什么第二次调用时会iter.hasNext()返回?true

我们可以用一个更简单、不涉及的例子来触发相同的行为Lists.partition()

jshell> var ls = Lists.newArrayList(1)
ls ==> [1]

jshell> var iter = ls.iterator()
iter ==> java.util.ArrayList$Itr@534df152

jshell> iter.hasNext()
$11 ==> true

jshell> iter.next()
$12 ==> 1

jshell> ls.remove(0)
$13 ==> 1

jshell> iter.hasNext()
$14 ==> true

不对!调用.next()同样会失败,但会提供更多信息ConcurrentModificationException。无论如何,这直接违反了 [Iterator规范]( https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/Iterator.html#hasNext ()),其中规定:

如果[ ]返回元素而不是抛出异常,则hasNext()返回 true 。next()

好像AbstractList.Itr.hasNext()有一个错误:

public boolean hasNext() {
  return cursor != size();
}

true因为如果cursor大于,这将返回size()

我想知道是否有一个好的理由来!=代替<

2

JavadocArrayList指出,如果在迭代过程中修改了列表,迭代器将失败(抛出ConcurrentModificationException),除非通过迭代器自己的remove()add()方法(即通过调用)进行remove(0)修改。它还指出,具体失败时间尚未指定。

基本上,如果您在迭代时修改非线程安全的列表,您应该预期 CME,但不可靠。

Lists.partition()返回一个列表列表;每个内部列表都是原始列表子列表的动态视图,并且“遵守该 API 中解释的有关修改的所有常见警告”。

文档中hasNext()说,如果返回truenext()不会抛出,实际上是指next()不会抛出NoSuchElementException,而文档中记录的是如果hasNext()返回则会抛出false。(我承认,文档中说“而不是抛出异常”应该澄清为“而不是抛出” NoSuchElementException。)

1

我不确定我是否理解了你的最后一段。在我上面的例子中,hasNext()返回true然后next()抛出NoSuchElementException。即使对 上的文档Iterator.hasNext()进行了调整以专门引用,它仍然似乎在断言如果返回NoSuchElementException,则不应抛出此异常。hasNext()true

5

好吧,我没有仔细阅读你的例子。这不是很好,但ArrayList确实有效地表明,如果在迭代过程中修改了列表,则所有赌注都会失效。

如果我们想改变迭代器,我会说我们应该hasNext()调用checkForComodification()

但是,除了 JDK 之外,我发现它Partition没有使用AbstractList使其抛出 CME 的功能。Partition它本身不允许修改,但如果源列表被修改,它的迭代器可能会做出那样的反应。

7

@netdpb @dimo414 你好,看了你们的讨论后,我有以下一些想法。

根据讨论,你对iter.hasNext()为何为真感到困惑。实际上,这是因为Partition类使用了超类AbstractList中的Iterator。而AbstractList类中的hasNext()函数并不完善。它们已在子类ArrayList中进行了优化。请在下面查看

这是AbstractList类中的hasNext。它调用size()函数

public boolean hasNext() {
    return cursor != size();
} 

这是ArrayList类中的hasNext。它使用值 size。文档说它是AbstractList.Itr 的优化版本。我在 JDK1.8 中看到了这个

public boolean hasNext() {
    return cursor != size;
}

当使用函数size()时,每次程序调用hasNext()hasNext()会调用size(),然后size 的值会被重新计算。回到 #3570 的代码,当程序执行removeall()时,重新计算的 size 会是 0,但是游标是 1。所以hasNext()返回 true。

for(List<Integer> l : ll){
   l.removeAll(elementExclude);
}
8

@netdpb @dimo414 有一个拼写错误,是 3790,而不是 3570。

我还测试了下面的代码。我使用 ArrayList 作为容器,然后执行相同的 removeAll。没有抛出任何异常。所以,我认为分区的行为应该相同

        List<Integer> list = new ArrayList();
        list.add(1);
        List<Integer> elementExclude = new ArrayList();
        elementExclude.add(1);

        List<List<Integer>> ll = new ArrayList();

        ll.add(list);
        for(List<Integer> l : ll){

            l.removeAll(elementExclude);
        }
3

根据讨论,您对为什么 iter.hasNext() 为真感到困惑。

我认为你误解了;考虑到当前的实现,返回 true 的原因很清楚hasNext(),但不清楚这种行为是否真正符合规范(我不认为是)。

3

@dimo414 好的,我想知道预期的行为是什么?在我看来,我认为预期的行为应该与 ArrayList 相同。不应抛出任何异常。

为了更清楚,我认为下面两个代码应该具有相同的行为

以分区作为容器的示例

        List<Integer> list = new ArrayList();
        list.add(1);
        List<Integer> elementExclude = new ArrayList();
        elementExclude.add(1);

        List<List<Integer>> ll =Lists.partition(list, 5);

        ll.add(list);
        for(List<Integer> l : ll){

            l.removeAll(elementExclude);
        }

以ArrayList为容器的示例

        List<Integer> list = new ArrayList();
        list.add(1);
        List<Integer> elementExclude = new ArrayList();
        elementExclude.add(1);

        List<List<Integer>> ll = new ArrayList();

        ll.add(list);
        for(List<Integer> l : ll){

            l.removeAll(elementExclude);
        }