TreeSet显示错误的输出
当我在树上工作时,发现了非常奇怪的行为。 根据我的理解,这个程序应该打印两个相同的行:
public class TestSet { static void test(String... args) { Set<String> s = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER); s.addAll(Arrays.asList("a", "b")); s.removeAll(Arrays.asList(args)); System.out.println(s); } public static void main(String[] args) { test("A"); test("A", "C"); } }
但奇怪的是它打印:
[b] [a, b]
为什么树的行为是这样的?
发生这种情况是因为SortedSet的比较器用于sorting,但removeAll依赖于每个元素的equals
方法。 从SortedSet文档 :
请注意,如果有序集合要正确实现
Set
接口,则由有序集合(不论是否提供显式比较器)维护的sorting必须与equals一致 。 (请参阅Comparable
接口或Comparator
接口以获得与equals一致的精确定义) 。这是因为Set
接口是根据equals
操作定义的,但是有序集使用compareTo
(或compare
)方法执行所有元素比较,所以这个方法被认为是相等的两个元素从sorting集的angular度来看是相等的。 即使sorting与等号不一致,sorting集合的行为也是明确的。 它只是不服从Set
接口的总体合同。
在“可比较”文档中定义了“与equals一致”的解释:
当且仅当
e1.compareTo(e2) == 0
与e1.equals(e2)
具有相同的布尔值e1.equals(e2)
对于C
类的每个e1
和e2
e1.equals(e2)
时,才说C
类的自然顺序与equals相一致 。 请注意,null
不是任何类的实例,即使e.equals(null)
返回false
,e.compareTo(null)
应抛出NullPointerException
。强烈build议(尽pipe不要求)自然sorting与平等一致。 这是因为sorting集(和sorting映射)没有明确的比较器,当它们与自然sorting与equals不一致的元素(或键)一起使用时,performance“奇怪”。 特别是,这样一个有序的集合(或有序的映射)违反了集合(或映射)的一般契约,这是根据
equals
方法定义的。
总之,你的Set的比较器的行为与元素的equals
方法不同,导致不寻常的(虽然是可预测的)行为。
那么,这让我感到惊讶,我不知道我是否正确,但看看AbstractSet
中的这个实现:
public boolean removeAll(Collection<?> c) { Objects.requireNonNull(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; }
基本上在你的例子中,set的大小等于你想移除的参数的大小,所以else条件被调用。 在这种情况下,检查你的要移除的参数集合是否contains
迭代器的当前元素,并且该检查是区分大小写的,因此它检查c.contains("a")
是否返回false,因为c
包含"A"
,而不是"a"
,所以元素不会被删除。 请注意,当你添加一个元素到你的设置s.addAll(Arrays.asList("a", "b", "d"));
它工作正常,因为size() > c.size()
现在是true,因此没有contains
检查了。
要添加一些关于为什么remove
TreeSet
实际上在您的示例中删除大小写的信息(并提供您按照if (size() > c.size())
的回答中解释的if (size() > c.size())
path:
这是TreeSet
的remove
方法:
public boolean remove(Object o) { return m.remove(o)==PRESENT; }
它调用从其内部的TreeMap
remove
:
public V remove(Object key) { Entry<K,V> p = getEntry(key); if (p == null) return null; V oldValue = p.value; deleteEntry(p); return oldValue; }
它调用getEntry
final Entry<K,V> getEntry(Object key) { // Offload comparator-based version for sake of performance if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; Entry<K,V> p = root; while (p != null) { int cmp = k.compareTo(p.key); if (cmp < 0) p = p.left; else if (cmp > 0) p = p.right; else return p; } return null; }
如果有一个Comparator
(如你的例子),这个条目是根据这个Comparator
(这是由getEntryUsingComparator
完成的)来search的,这就是为什么它实际上被find(然后被移除),尽pipe有不同的情况。
这很有趣,所以这里有一些输出testing:
static void test(String... args) { Set<String> s =new TreeSet<String>(String.CASE_INSENSITIVE_ORDER); s.addAll(Arrays.asList( "a","b","c")); s.removeAll(Arrays.asList(args)); System.out.println(s); } public static void main(String[] args) { test("C"); output: [a, b] test("C", "A"); output: [b] test("C", "A","B"); output: [a, b, c] test("B","C","A"); output: [a, b, c] test("K","C"); output: [a, b] test("C","K","M"); output: [a, b, c] !! test("C","K","A"); output: [a, b, c] !! }
现在没有比较器,它就像一个sorting的HashSet<String>()
:
static void test(String... args) { Set<String> s = new TreeSet<String>();// s.addAll(Arrays.asList( "a","b","c")); s.removeAll(Arrays.asList(args)); System.out.println(s); } public static void main(String[] args) { test("c"); output: [a, b] test("c", "a"); output: [b] test("c", "a","b"); output: [] test("b","c","a"); output: [] test("k","c"); output: [a, b] test("c","k","m"); output: [a, b] test("c","k","m"); output: [a, b] }
现在从文档:
public boolean removeAll(Collection c)
从该集合中移除指定集合中包含的所有元素(可选操作)。 如果指定集合也是集合,则此操作将有效地修改此集合,使其值为两个集合的非对称集合差异。
这个实现通过调用每一个的size方法来确定这个集合和指定的集合中哪一个更小。 如果这个集合有更less的元素,那么这个实现迭代这个集合,依次检查迭代器返回的每个元素,看看它是否包含在指定的集合中。 如果包含它,则使用迭代器的remove方法将其从此集中移除。 如果指定的集合有更less的元素,那么实现迭代指定的集合,从这个集合中删除迭代器返回的每个元素,使用这个集合的remove方法。
资源