大家好,今天和大家分享一些日常使用,但是又颇为容易采坑的代码知识,这次要和大家分享的就是HashSet和TreeSet的等同性比较,闲话少说,开始正题。
首先我们知道Set接口使我们常用的集合接口之一,在对Set进行存储时,Set会自动将重复值去除,也就是如下:
Set<String> set = new HashSet<>();
set.add("111");
set.add("111");
在这段代码中set中只会有一个字符串”111”,而重复元素的个数不会随着添加而变多。
但是我们来看下面的代码:
HashSet<BigDecimal> hashSet = new HashSet<>();
TreeSet<BigDecimal> treeSet = new TreeSet<>();
//注意,这里传入的两个字符串
BigDecimal decimal1 = new BigDecimal("1.0");
BigDecimal decimal2 = new BigDecimal("1.00");
hashSet.add(decimal1);
hashSet.add(decimal2);
System.out.println(hashSet.size());
treeSet.add(decimal1);
treeSet.add(decimal2);
System.out.println(treeSet.size());
输出的结果是什么呢?经过输出后,hashSet的容量为2,但是TreeSet的容量为1。
我们知道,set在存值时,是依赖来等同性比较的,这么说吧,每个类都会有一个或者多个比较规则,然后set会根据自己选中的比较规则,来进行比较了,那么大家可能疑惑,我们平常的代码中并没有定义多余的比较规则啊?那么我们定义的类的默认比较规则是什么呢?答案就是equels()方法,因为Object是所有类的父类啊。
但是我们常常忽略了一种比较规则,就是实现compareable接口后需要实现的campareTo()方法,上面代码的问题就是出现在这里,简单说明一下,其实set的实现内部是依赖于Map的,HashSet的add()方法,实际上是调用了内部HashMap的put()方法,我们来看一下HashMap的put()方法:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
在这段代码中,我们注意到put()方法对key(由hashSet传入的add()方法中的参数)值进行了hash处理,然后进行比较,首先是比较内存地址,然后进行equels比较,在这个过程中,由于我们的代码传值为BigDecimal对象,所以地址比较肯定失败,所以开始进行equels比较。相同时,就会去掉旧值,设为新值,然后回传旧值。
我们再来看一下TreeSet中的add()方法,首先TreeSet默认内部依赖的Map为TreeMap:
public TreeSet() {
this(new TreeMap<E,Object>());
}
那么TreeMap的put()方法是怎么进行等同性比较的呢?
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
我们可以看到,首先是使用了内聚的caoparator的实现类进行compare比较,如果caoparator为空的话就将key转换为compareable接口的类型,进行比较,也就是说key必须实现compareable接口,所以在TreeSet中,对key的比较是依赖于camparable接口的。
通过以上分析,我们不难得到结论,如果一个类实现的equels方法和compare方法的等同性比较结果不一样,那么在两个Set中存储的内容自然也就不一样,而BigDecemal就是这样的一个情况,所以我们就看到了第一段代码的结果。
但是在《Effective JAVA》中提到:“强烈建议compareTo()方法和equels()方法实现同样的比较原则”,什么意思呢,就是说希望equels方法和compareTo方法在调用时,如果调用对象和比较对象不变的话,结果也是一样的,这样的话就可以避免在依赖于等同性比较的类中出现不同的结果,当然好处并不只有这一点,不过这也只是建议,而不是规则,如果说两者的比较结果不同,则需要明确进行说明。
好了,今天就和大家先分享这些,如有不足,请多多批评指正。