集合中removeAll方法的不直观行为

时间:2013-08-08 10:04:31

标签: java set

在处理个人removeAll时,我发现了AbstractSets 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

2 个答案:

答案 0 :(得分:0)

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

  

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

答案 1 :(得分:0)

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

可能出于性能原因。考虑使用2 TreeSet个,其中一个包含m个元素,另一个包含n个元素。现在考虑从具有m元素的n元素中移除所有元素。如果我们坚持迭代传入的集合并调用remove,如果m远大于n,这将比迭代当前集并检查它是否存在要慢得多({{1 }})。比较尺寸可以防止这种情况发生。

这不是一个完美无瑕的系统 - 如果你将O(m log n) > O(n log m)传递给Stack,那么迭代TreeSet渐渐地总是更糟糕的想法来迭代TreeSetStack),但它将遵循与上述相同的规则。虽然考虑所有允许类型的组合会有点麻烦。

如果你问为什么代码会做它的作用:

以下是O(m n) > 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上为每个元素调用删除,使用你的TreeSet,从而删除所有匹配长度的元素,只留下长度为4的元素,对应于弹出的元素。