AbstractSet.removeAll() 方法无法正常工作

AbstractSet.removeAll() method not working properly

下面显示的代码确实输出:

[b]

[a, b]

但是我希望它在输出中打印两行相同的行。

import java.util.*;

public class Test{
    static void test(String... abc) {
        Set<String> s = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
        s.addAll(Arrays.asList("a", "b"));
        s.removeAll(Arrays.asList(abc));
        System.out.println(s);
    }

    public static void main(String[] args) {
        test("A");
        test("A", "C");
    }
}

规范明确指出 removeAll

"Removes all this collection's elements that are also contained in the specified collection."

所以根据我的理解,当前的行为是不可预测的。请帮助我理解这一点

您只阅读了部分文档。您忘记了 TreeSet 中的一个重要段落:

Note that the ordering maintained by a set (whether or not an explicit comparator is provided) must be consistent with equals if it is to correctly implement the Set interface. (See Comparable or Comparator for a precise definition of consistent with equals.) This is so because the Set interface is defined in terms of the equals operation, but a TreeSet instance performs all element comparisons using its compareTo (or compare) method, so two elements that are deemed equal by this method are, from the standpoint of the set, equal. The behavior of a set is well-defined even if its ordering is inconsistent with equals; it just fails to obey the general contract of the Set interface.

现在 removeAll 实现来自 AbstractSet 并利用 equals 方法。根据您的代码,您将拥有 "a".equals("A") 不是 true,因此即使您提供了一个在 TreeSet 本身中使用时管理它们的比较器,元素也不被视为相等。如果您尝试使用包装器,那么问题就会消失:

import java.util.*;
import java.lang.*;

class Test
{
    static class StringWrapper implements Comparable<StringWrapper>
    {
      public final String string;

      public StringWrapper(String string)
      {
        this.string = string;
      }

      @Override public boolean equals(Object o)
      { 
        return o instanceof StringWrapper && 
            ((StringWrapper)o).string.compareToIgnoreCase(string) == 0; 
      }

      @Override public int compareTo(StringWrapper other) { 
        return string.compareToIgnoreCase(other.string); 
      }

      @Override public String toString() { return string; }
    }

    static void test(StringWrapper... abc) 
    {
        Set<StringWrapper> s = new TreeSet<>();
        s.addAll(Arrays.asList(new StringWrapper("a"), new StringWrapper("b")));
        s.removeAll(Arrays.asList(abc));
        System.out.println(s);
    }

    public static void main(String[] args)
    {
        test(new StringWrapper("A"));
        test(new StringWrapper("A"), new StringWrapper("C"));
    }
}

这是因为您现在在对象的 equalscompareTo 之间提供了一致的实现,因此您永远不会在对象如何添加到排序集中以及所有抽象对象如何之间出现不一致的行为集合的行为使用它们。

这在一般情况下是正确的,Java 代码的一种三规则:如果你实施 compareToequalshashCode 你应该始终实施所有避免标准集合出现问题(即使 hashCode 不太重要,除非您在任何散列集合中使用这些对象)。这在 java 文档中多次指定。

这是 TreeSet<E> 实现中的一个不一致,接近错误。当您传递给 removeAll 的集合中的项目数大于或等于集合中的项目数时,代码将忽略自定义比较器。

不一致是由一个小的优化引起的:如果你看removeAllimplementation,继承自AbstractSet,优化如下:

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;
}

您可以看到,当 c 的项目少于此集合(顶部分支)与它具有更多或更多项目(底部分支)时的行为是不同的。

顶部分支使用与此集合关联的比较器,而底部分支使用 equals 进行比较 c.contains(i.next()) - 全部采用相同的方法!

您可以通过向原始树集添加一些额外的元素来演示此行为:

s.addAll(Arrays.asList("x", "z", "a", "b"));

现在两个测试用例的输出变得相同,因为remove(i.next())利用了集合的比较器。

原因是因为您使用的比较器String.CASE_INSENSITIVE_ORDER与equals不一致

TreeSet所述:

Note that the ordering maintained by a set (whether or not an explicit comparator is provided) must be consistent with equals if it is to correctly implement the Set interface.

Comparable:

所述的一致

The natural ordering for a class C is said to be consistent with equals if and only if e1.compareTo(e2) == 0 has the same boolean value as e1.equals(e2) for every e1 and e2 of class C.

作为您使用的不区分大小写的比较器的示例:

"a".compareTo("A") == 0 => true

"a".equals("A") => false