【问题标题】:Unintuitive behavior of removeAll method in sets [closed]集合中 removeAll 方法的不直观行为 [关闭]
【发布时间】:2013-08-08 10:04:30
【问题描述】:

我发现AbstractSetsremoveAll 方法在处理单个Comparators 时的这种奇怪行为。

根据比较集合的大小,使用不同的比较器。

它实际上记录在 API 中,但我仍然看不到它背后的原因。

代码如下:

import java.util.Comparator;
import java.util.Set;
import java.util.Stack;
import java.util.TreeSet;

public class Test {
    public static void main(String[] args) {
        // Any comparator. For this example, the length of a string is compared
        Set<String> set = new TreeSet<String>(new Comparator<String>() {
                @Override
                public int compare(String o1, String o2) {
                        return o1.length() - o2.length();
                }
        });

        set.add("a");
        set.add("aa");
        set.add("aaa");
        set.add("aaaa");
        System.out.println(set); // output: [a, aa, aaa, aaaa]

        Stack<String> stack = new Stack<String>();
        stack.push("b");
        stack.push("bb");
        stack.push("bbb");
        stack.push("bbbb");

        set.removeAll(stack); // NO ITEMS ARE REMOVED from the set
        System.out.println(set); // output: [a, aa, aaa, aaaa]

        // Now let's see what happens if I remove an object from the stack
        stack.pop();
        set.removeAll(stack); // ALL ITEMS from the stack are removed from the
                                                        // set
        System.out.println(set); // output: [aaaa]

        /* Reason for this strange behaviour: Depending on the size of the
         * passed Collection, TreeSet uses either the remove() function of
         * itself, or from the Collection object that was passed. While the
         * remove() method of the TreeSet uses the comparator to determine
         * equality, the remove() method of the passed usually determines
         * equality by calling equals() on its objects.
         */
    }
}

Here is the JavaDoc.

【问题讨论】:

标签: java set


【解决方案1】:

您基本上已经创建了未定义的行为,因为您的集合具有不同的平等标准。只有当它们具有相同的集合时,才能以任何方式组合集合。您基本上违反了A.equals(B) 必须产生与B.equals(A) 相同的结果的合同。

Comparable:强烈建议(尽管不是必需的)自然排序与equals一致。之所以如此,是因为没有显式比较器的排序集(和排序映射)在与自然顺序与等于不一致的元素(或键)一起使用时表现“奇怪”。特别是,这样的有序集合(或有序映射)违反了集合(或映射)的一般约定,它是根据 equals 方法定义的。

【讨论】:

    【解决方案2】:

    如果你问他们为什么选择以这种方式实施:

    这可能是出于性能原因。考虑有 2 个TreeSets,一个包含m 元素,另一个包含n 元素。现在考虑从具有m 元素的那一个中删除所有具有n 元素的元素。如果我们坚持遍历传入的集合并调用remove,如果mn 大得多,这将比遍历当前集合并检查它是否存在(O(m log n) &gt; O(n log m)) 慢得多。比较大小可以防止这种情况发生。

    这不是一个完美的系统 - 如果你将 Stack 传递给 TreeSet,迭代 TreeSet 总是渐近地迭代 Stack (O(m n) &gt; O(m log n)) 是一个更糟糕的主意,但它将遵循与上述相同的规则。尽管考虑所有允许类型的组合会有些麻烦。

    如果你问为什么代码会这样做:

    这是removeAll的代码:

    public boolean removeAll(Collection<?> c) {
        boolean modified = false;
    
        if (size() > c.size()) {
            for (Iterator<?> i = c.iterator(); i.hasNext(); )
                modified |= remove(i.next());
        } else {
            for (Iterator<?> i = iterator(); i.hasNext(); ) {
                if (c.contains(i.next())) {
                    i.remove();
                    modified = true;
                }
            }
        }
        return modified;
    }
    

    所以当Stack 的元素数量比TreeSet 多或相同时(在第一种情况下发生),removeAll 将遍历TreeSet 并删除Stack 中包含的每个元素.由于Stack 使用默认的String 比较,因此不会匹配任何字符串,也不会删除任何内容。

    Stack 的元素较少时(在第二种情况下发生),removeAll 将遍历Stack 并为每个使用您的Comparator 的元素调用TreeSet 上的remove,从而删除所有长度匹配的元素,只留下长度为4的元素,对应弹出的元素。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-05
      • 2013-04-28
      相关资源
      最近更新 更多