【问题标题】:TreeMap iterator.remove() modifies the last EntryTreeMap iterator.remove() 修改最后一个Entry
【发布时间】:2017-08-24 13:53:10
【问题描述】:

我有一张地图。

Map<Integer,String> map = ...

地图有 n 个元素(我们以这 9 个元素为例)

  map.put(1,"one");
  map.put(2,"two");
  map.put(3,"three");
  map.put(4,"four");
  map.put(5,"five");
  map.put(6,"six");
  map.put(7,"seven");
  map.put(8,"eigth");
  map.put(9,"nine");

现在我想迭代这张地图,并使用迭代器删除第 n 个元素。

private void remove(int num, final Map<Integer, String> map) {

  Iterator<Map.Entry<Integer,String>> it = map.entrySet().iterator();
  Map.Entry<Integer,String> entry;
  while(it.hasNext()){

     entry = it.next();

     if(Integer.valueOf(num).equals(entry.getKey())){
        it.remove();
        System.out.println(entry.getValue());
        // vs
        // System.out.println(entry.getValue());
        // it.remove();
     }
  }
}

从 javadoc 中,我假设删除的语义定义明确。

但是根据映射的实现——即 HashMap 与 TreeMap,it.remove() 是在 before 还是 after entry.getValue() 完成时有所不同。

对于 HashMaps map = new HashMap&lt;&gt;(),行为是

...
remove(4, map); //output: four
//or
remove(5, map); //output: five

对于 TreeMap map = new TreeMap&lt;&gt;(),行为是相同的,当我从迭代器中删除当前条目之后我访问了它:

System.out.println(entry.getValue());
it.remove();

结果

remove(4, map); //output: four
//or
remove(5, map); //output: five

到目前为止一切都很好,但是如果我删除元素 之前 我会访问该条目:

it.remove();
System.out.println(entry.getValue());

输出意外

remove(4, map); //output: five !!!
//or
remove(5, map); //output: five ok

显然,TreeMapit.remove() 修改了Entries,因为TreeMap 是由Entries 组成的,并且迭代器实际上返回了映射的实际元素。并且根据树中的当前位置,Entry 指向下一个或当前(已删除)元素的内部引用。

但我不确定这是错误还是故意的。如果是后者,不知道背后的原理是什么?

编辑: TreeMap iterator.remove()的源码

【问题讨论】:

  • 我没有得到您在任一地图中描述的行为...您使用什么 Java 版本?如果在 remove() 之前和之后都输出条目的值会发生什么?
  • jdk1.8.0_121 ...它可以用只有 3 个条目 (1,2,3) 的 TreeMap 重现,删除中间元素 (2)。但它没有出现在 HashMap 中
  • 在“二,三”前后打印出来
  • 我现在也明白了。我无法想象这是故意的。执行此操作的代码有注释“如果严格内部,则将后继元素复制到 p,然后使 p 指向后继”......这并不是真正严格内部。
  • 我们刚刚也遇到了这个问题。我在 Oracle 上为它做了一个错误报告——当它没有被审查时,我会为它添加一个链接。

标签: java iterator


【解决方案1】:

来自Map.Entry Javadoc

如果在迭代器返回条目后修改了支持映射,则映射条目的行为未定义,除非通过对映射条目的 setValue 操作

来自Map.Entry.getValue Javadoc

如果映射已从支持映射中删除(通过迭代器的删除操作),则此调用的结果未定义。

禁止在it.remove() 之后调用entry.getValue()。 Java 不承诺如果您尝试它会发生什么。您应该在删除条目之前检索该值。

【讨论】:

    【解决方案2】:

    这是一个部分答案,但希望能提示其他人如何给出更准确的答案。

    假设您对Iterator#next() 的调用会返回位于底层地图顶部的视图,

    那么,我们可能会在尝试访问值从映射中删除底层映射条目时得到一些未定义的行为,这可能并不奇怪。

    while (it.hasNext()) {
        entry = it.next();
    
        if (Integer.valueOf(num).equals(entry.getKey())) {
            it.remove();
            // the object which 'entry' points to is already removed
            // what is this pointing to?
            System.out.println(entry.getValue());
       }
    }
    

    根据您自己的观察,该行为似乎是特定于 Map 实现的,这意味着特定地图的实现涉及观察到的行为。

    【讨论】:

    • 如果您觉得这里没有任何价值,我可以删除此答案。顺便说一句,如果你能找到Iterator 实现的源代码,你可能会有答案。
    • 是的,我知道为什么会发生这种情况,但不知道为什么会以这种方式实现以及是否是故意的(IMO 它违反了接口合同)。
    • 合同在哪里说删除条目后该条目仍然有效?另外,没有看到地图迭代器的源代码,你怎么知道它为什么会发生?
    • 合约只提到 1) Map 返回的 entrySet 由 Map 支持,2) iterator.remove() 修改底层集合(因此修改了支持映射)。但没有提及对条目本身的影响。调用 remove 并通过修改条目(它仍然是同一个对象!)是一个相当大的副作用 IMO,我想知道这是否是故意的? ...而且我确实查看了源代码,因此我知道内部发生了什么:)
    • @GeraldMücke 请在您的问题中提供一些指向Iterator 实现源代码的链接。仅此一项对于将来阅读您的问题的其他人来说就是非常有用的信息。
    猜你喜欢
    • 2014-02-23
    • 2017-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-24
    • 1970-01-01
    相关资源
    最近更新 更多