Java 为什么ConcurrentModificationException的设计标准仅被选择为结构修改

Java 为什么ConcurrentModificationException的设计标准仅被选择为结构修改,java,collections,concurrentmodification,design-decisions,Java,Collections,Concurrentmodification,Design Decisions,我知道,只要在创建迭代器后对基础集合进行结构修改,就会抛出ConcurrentModificationExcpetion。这似乎是一个设计决策 我想知道为什么CME不考虑通过set方法进行更新?最终,集合将得到更新,如果创建了迭代器,遍历仍可能导致不一致的结果 除非您真正参与了设计决策,否则不可能知道设计决策的确切原因 我推测原因可能是迭代器设计用于迭代集合,而不是迭代集合中的值 换句话说,迭代器就像一个指针,只需移动它,依次指向集合中的每个空间。通过在构造迭代器时了解集合的形状,您知道如何将指

我知道,只要在创建迭代器后对基础集合进行结构修改,就会抛出ConcurrentModificationExcpetion。这似乎是一个设计决策


我想知道为什么CME不考虑通过set方法进行更新?最终,集合将得到更新,如果创建了迭代器,遍历仍可能导致不一致的结果

除非您真正参与了设计决策,否则不可能知道设计决策的确切原因

我推测原因可能是迭代器设计用于迭代集合,而不是迭代集合中的值

换句话说,迭代器就像一个指针,只需移动它,依次指向集合中的每个空间。通过在构造迭代器时了解集合的形状,您知道如何将指针移动到集合中的下一个空间

如果存储在空间中的值发生更改,则这并不重要:该空间和集合中的所有其他空间保持不变


但是,如果通过结构修改更改集合的形状,则基本上会使关于指针指向何处的任何假设无效;除非你真的参与了设计决策,否则不可能知道设计决策的确切原因

我推测原因可能是迭代器设计用于迭代集合,而不是迭代集合中的值

换句话说,迭代器就像一个指针,只需移动它,依次指向集合中的每个空间。通过在构造迭代器时了解集合的形状,您知道如何将指针移动到集合中的下一个空间

如果存储在空间中的值发生更改,则这并不重要:该空间和集合中的所有其他空间保持不变

但是,如果通过结构修改更改集合的形状,则基本上会使关于指针指向何处的任何假设无效;除了做一些非常聪明的事情,你还不如放弃并抛出一个异常

最终,集合将得到更新,如果创建了迭代器,遍历仍可能导致不一致的结果

实际上,我认为原因是集合运算不会导致不一致的结果。迭代集合的代码将看到set调用修改的结果。。。或者不。。。以一种高度可预测的方式。并且代码保证仍然可以看到集合中的其他元素,即与您只设置一次的元素不同

相反,对典型的非并发集合进行结构修改可能会导致集合条目/元素四处移动。例如:

ArrayList中的插入或删除会导致支持数组中的元素改变位置

在HashMap或HashSet中的插入可能会导致调整大小,从而导致重新构建哈希链

在这两种情况下,结构修改都可能导致跳过图元或多次查看图元

最终,集合将得到更新,如果创建了迭代器,遍历仍可能导致不一致的结果

实际上,我认为原因是集合运算不会导致不一致的结果。迭代集合的代码将看到set调用修改的结果。。。或者不。。。以一种高度可预测的方式。并且代码保证仍然可以看到集合中的其他元素,即与您只设置一次的元素不同

相反,对典型的非并发集合进行结构修改可能会导致集合条目/元素四处移动。例如:

ArrayList中的插入或删除会导致支持数组中的元素改变位置

在HashMap或HashSet中的插入可能会导致调整大小,从而导致重新构建哈希链


在这两种情况下,结构修改都可能导致跳过元素,或多次看到元素。

请在下面找到我对您假设的评论

我知道,只要在创建迭代器后对基础集合进行结构修改,就会抛出ConcurrentModificationException

不完全是。ConcurrentModificationException实际上是在迭代器尝试获取下一个值并且基础集合中发生了结构更改时引发的。我想强调ConcurrentModificationException不是t hrown调用时,即在基础集合上添加或删除

那么,关于你最后的假设:

最终,集合将得到更新,如果创建了迭代器,遍历仍可能导致不一致的结果

我认为这是不对的。遍历如何仍然会导致不一致的结果?如果修改列表第i个位置的值,您将立即看到此更改。该列表实际上已被修改,但在结构上未被修改。只有第i个位置的值会更改,但列表的结构不会更改,即ArrayList的基础数组不会产生调整大小的风险,LinkedList不会分配新节点等

最后,关于这句话:

这似乎是一个设计决策

当然。这是一个100%的设计决策,虽然我没有参与其中,但我确信它的目的是避免在遍历底层集合时出现不一致。与其以意外或不一致的数据结束,不如尽早抛出异常并让用户知道

然而,文档中还提到,快速失效迭代器会尽最大努力抛出ConcurrentModificationException

例如,考虑下面的代码:

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));

Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    try {
        Integer next = iterator.next();
        System.out.println("iterator.next() = " + next);

        System.out.println("BEFORE remove: " + list);
        list.remove(0);
        System.out.println("AFTER remove: " + list);

    } catch (ConcurrentModificationException e) {

        System.out.println("CME thrown, ending abnormally...");
        System.exit(1);
    }
}
这是预期的行为。当迭代器第二次尝试从列表中获取元素时,将引发ConcurrentModificationException。这是因为实现检测到自迭代器创建以来已进行了结构更改。因此,iterator.next抛出一个CME

然而,由于快速失效迭代器不能保证总是抛出ConcurrentModificationException,但要尽最大努力才能抛出,有一种方法可以欺骗实现。考虑下面的代码,它是前一个代码段的修改版本,如果执行它,请警告它完成之前需要一些时间:

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));

int structuralChanges = 0;

Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    try {
        Integer next = iterator.next();
        System.out.println("iterator.next() = " + next);

        System.out.println("BEFORE remove: " + list);
        list.remove(0);
        System.out.println("AFTER remove: " + list);
        structuralChanges++;

        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            list.add(i);
            structuralChanges++;
            list.remove(list.size() - 1);
            structuralChanges++;
        }
        list.add(0);
        structuralChanges++;

        System.out.println("Structural changes so far = " + structuralChanges);

    } catch (ConcurrentModificationException e) {

        System.out.println("CME NEVER thrown, so you won't see this message");
        System.exit(1);
    }
}

System.out.println("AFTER everything: " + list);
System.out.println("Program ending normally");
如输出所示,没有抛出ConcurrentModificationException

此外,在进行所有结构修改后,structuralChanges变量的值最终为0。这是因为它是int类型的变量,在由于我们的人工for循环和随后的增量而递增Integer.MAX_值*2+2倍Integer.MAX_值*2+1倍,再加上由于原始代码的list.remove0调用而递增的1之后,它会溢出并再次达到0


然后,在所有这些Integer.MAX_VALUE*2+2结构修改之后,当我们调用iterator.next时,在while循环的后续迭代中,永远不会抛出ConcurrentModificationException。我们已经欺骗了实现,所以现在可以看到数据在内部发生了什么。在内部,实现通过保持int计数来跟踪结构修改,就像我对structuralChanges变量所做的那样。

请在下面找到我对您的假设的评论

我知道,只要在创建迭代器后对基础集合进行结构修改,就会抛出ConcurrentModificationException

不完全是。ConcurrentModificationException实际上是在迭代器尝试获取下一个值并且基础集合中发生了结构更改时引发的。我想强调的是,在调用基础集合(即添加或删除)时不会抛出ConcurrentModificationException

那么,关于你最后的假设:

最终,集合将得到更新,如果创建了迭代器,遍历仍可能导致不一致的结果

我认为这是不对的。遍历如何仍然会导致不一致的结果?如果修改列表第i个位置的值,您将立即看到此更改。该列表实际上已被修改,但在结构上未被修改。只有第i个位置的值会更改,但列表的结构不会更改,即ArrayList的基础数组不会产生调整大小的风险,LinkedList不会分配新节点等

最后,关于这句话:

这似乎是一个设计决策

当然。这是一个100%的设计决策,虽然我没有参与其中,但我确信它的目的是避免在遍历底层集合时出现不一致。与其以意外或不一致的数据结束,不如尽早抛出异常并让用户知道

然而,文档中还提到,快速失效迭代器会尽最大努力抛出ConcurrentModificationException

例如,考虑下面的代码:

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));

Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    try {
        Integer next = iterator.next();
        System.out.println("iterator.next() = " + next);

        System.out.println("BEFORE remove: " + list);
        list.remove(0);
        System.out.println("AFTER remove: " + list);

    } catch (ConcurrentModificationException e) {

        System.out.println("CME thrown, ending abnormally...");
        System.exit(1);
    }
}
这是预期的行为。当迭代器第二次尝试从列表中获取元素时,将引发ConcurrentModificationException。这是因为实现检测到自迭代器创建以来已进行了结构更改。因此,iterator.next抛出一个CME

然而,由于快速失效迭代器不能保证总是抛出ConcurrentModificationException,但要尽最大努力才能抛出,有一种方法可以欺骗实现。考虑下面的代码,它是前一个代码段的修改版本,如果执行它,请警告它完成之前需要一些时间:

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));

int structuralChanges = 0;

Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
    try {
        Integer next = iterator.next();
        System.out.println("iterator.next() = " + next);

        System.out.println("BEFORE remove: " + list);
        list.remove(0);
        System.out.println("AFTER remove: " + list);
        structuralChanges++;

        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            list.add(i);
            structuralChanges++;
            list.remove(list.size() - 1);
            structuralChanges++;
        }
        list.add(0);
        structuralChanges++;

        System.out.println("Structural changes so far = " + structuralChanges);

    } catch (ConcurrentModificationException e) {

        System.out.println("CME NEVER thrown, so you won't see this message");
        System.exit(1);
    }
}

System.out.println("AFTER everything: " + list);
System.out.println("Program ending normally");
如输出所示,没有抛出ConcurrentModificationException

此外,在进行所有结构修改后,structuralChanges变量的值最终为0。这是因为它是int类型的变量,在由于我们的人工for循环和随后的增量而递增Integer.MAX_值*2+2倍Integer.MAX_值*2+1倍,再加上由于原始代码的list.remove0调用而递增的1之后,它会溢出并再次达到0


然后,在所有这些Integer.MAX_VALUE*2+2结构修改之后,当我们调用iterator.next时,在while循环的后续迭代中,永远不会抛出ConcurrentModificationException。我们已经欺骗了实现,所以现在可以看到数据在内部发生了什么。在内部,实现通过保持int计数来跟踪结构修改,就像我对structuralChanges变量所做的那样。

通过添加和删除操作,可以意外跳过或遍历元素,尽管这些元素已经消失或出现两次,但这肯定是不正确和不可预测的。对于无法发生的集合,您确实可以看到最新的值,并且从迭代的角度来看,没有任何东西是固有的中断添加/删除元素会更改集合的大小,而更新不会更改集合的大小。若您在下一次调用中使用迭代器进行遍历,那个么它也会在同一个抽象列表上调用checkformodification;您的意思是,如果列表中的某个元素被更新,列表的迭代器不会抛出CME?但什么是更新?如果调用list.get0.equals1和list.set1,1,是否应引发CME?字符串相等,但可能不相同!=。平等性是决定元素是否已更新的标准吗?通过将CME仅用于结构更改,迭代器不关心这个问题。这可能是一个原因。通过添加和删除操作,您可能会意外跳过或遍历元素,尽管这些元素已经消失或出现两次,但这肯定是不正确和不可预测的。对于无法发生的集合,您确实可以看到最新的值,并且从迭代的角度来看,没有任何东西是固有的中断添加/删除元素会更改集合的大小,而更新不会更改集合的大小。若您在下一次调用中使用迭代器进行遍历,那个么它也会在同一个抽象列表上调用checkformodification;您的意思是,如果列表中的某个元素被更新,列表的迭代器不会抛出CME?但什么是更新?如果调用list.get0.equals1和list.set1,1,是否应引发CME?字符串相等,但可能不相同!=。平等性是决定元素是否已更新的标准吗?通过将CME仅用于结构更改,迭代器不关心这个问题。这可能是一个原因。我一直在想为什么我想不出这样的答案。我想这不是我的强项。我喜欢编写Java程序来欺骗实现。设计决策?谁在乎呢?“我们仍然可以欺骗它。”Sara欺骗这个特殊的设计决定很有趣。但我认为这不太实际。在我的笔记本电脑上运行了一段时间。我写它只是为了说明您的代码永远不应该依赖于抛出ConcurrentModificationException。我知道你没有问过,但我认为这是相关的。我认为输出表明迭代器的行为有些出乎意料。第一次返回1,这是正确的,第二次返回3,但我希望它返回2,尽管列表的第一个元素已被删除。第三次3而不是0我一直在想为什么我想不出这样的答案。我想这不是我的强项。我喜欢编写Java程序来欺骗实现。设计决策?谁在乎呢?“我们仍然可以欺骗它。”Sara欺骗这个特殊的设计决定很有趣。但我认为这不太实际。在我的笔记本电脑上运行了一段时间。我写它只是为了说明您的代码永远不应该依赖于抛出ConcurrentModificationException。我知道你没有问过,但我认为这是相关的。我认为输出表明迭代器的行为有些出乎意料。第一次返回1,这是正确的,第二次返回3,但我希望它返回2,尽管列表的第一个元素已被删除。第三次是3而不是0