对TreeSet的错误认识以及重新认识
背景
事情是这样的,一个朋友今天问我一个问题,说他今天发现了一个奇怪的bug,在TreeSet集合中竟然有两个一样的元素(这里的一样就是表面上一样的,对象内各个值都相等),然后他问我,TreeSet里面可不可能有两个一样的元素?
我信誓旦旦的就回答,Set集合嘛,肯定遵从接口规范呀,只要hashcode和equals方法一样的元素,那就不可能存在。然后…他笑了,截个IDE的debug的图给我,让我自己去看。于是我把打了一半的嘲讽的话收了回来,老老实实的先看看源码再说。
TreeSet
如果不看源码,以自己浅薄的认识,真的就会天真的以为它就是个元素不会重复且还有排序功能的集合,但是…
先看类声明:
public class TreeSet<E>
extends AbstractSet<E>
implements NavigableSet<E>,
Cloneable,
java.io.Serializable
我说的元素不会重复
其实就是说的最熟悉的HashSet
的不会重复的意思,也就是HashMap
中的判定方法,equal
方法和hashCode
方法都不相等,那么就代表元素不同,也就不会重复。那么同理,看到类声明的时候我都觉得自己是对的了与HashSet
一样,都继承了AbstractSet<E>
,不同的是,TreeSet
还实现了NavigableSet<E>
接口,这些方法跟这次的问题没有关系,先略过。
那么最重要的来了,要看能否有重复元素,只需要看Set
接口下的add
方法实现即可,很明显,TreeSet
重写了AbstractSet<E>
的add
方法:
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
方法看起来跟HashSet的没什么区别,但是,这里面的m
,其实是private transient NavigableMap<E,Object> m;
这样一个对象,默认是使用的TreeMap
实现类。那么现在问题就转移到看TreeMap
的put
方法是如何操作的了。
TreeMap
看看他的类声明:
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>,
Cloneable,
java.io.Serializable
因为他也是实现了Map
接口的,而且其put方法也是重新实现了的,那么就直接看put方法吧,其他的可以先不管了。
整个put方法的核心就这一段:
cmp = k.compareTo(t.key);
if (cmp < 0)
t= t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
可以很明显的看到,只要两个对象的java.lang.Comparable#compareTo
方法结果相同,那么就会被认为是同一个对象,进入最后一个else
,将原来的值替换掉。这其中并没有是用什么hashcode
和equals
方法!
答案
那么回到问题本身,TreeSet 里面能不能出现重复的对象?
答案是:不会能出现java.lang.Comparable#compareTo
结果相同的对象。
反思
- 使用TreeSet、TreeMap的时候,K必须是实现了
java.lang.Comparable#compareTo
接口的对象,要不然就会直接报错,这个问题还好一点,找起来没那么麻烦 - 实现
java.lang.Comparable#compareTo
接口的时候,一定要写清楚注释,要不然后人用起来只有想打人的冲动^_^