关于equals的用法,在另一篇文章《Java中==和equals的使用区别》中已经详细介绍过了,本文主要讲解equals和hashcode在一起使用时的注意事项。
一、我可以不重写hashcode吗?
我们在重写对象的equals方法的时候,并没有要求重写hashcode方法,那如果我们不去管hashcode方法,能正常进行对象的equals比较吗?
答案是肯定的。
public class Fruit {
private String name;
private Integer weight;
public Fruit(){}
public Fruit(String name, Integer weight){
this.name = name;
this.weight = weight;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Fruit fruit = (Fruit) o;
return Objects.equals(name, fruit.name) &&
Objects.equals(weight, fruit.weight);
}
// getters and setters...
public static void main(String[] args) throws Exception {
Fruit apple1 = new Fruit("apple",12);
Fruit apple2 = new Fruit("apple",12);
// print true
System.out.println(apple1.equals(apple2));
}
在如上两个对象的equals比较中,我们没有用到hashcode,也没有重写它,照样可以正常使用。
二、什么情况下不重写hashcode会出现问题?
但是,如果我们在散列表这种数据结构中不重写hashcode的话,会发生意料之外的情况,比如下面这个例子。
Java中的HashSet是被设计为不能添加重复的相同对象的,即hashcode相同且内容也相同的对象,如果hashcode是不同的,那么即使对象的内容相同,hashset也认为它们是不同的。
// 执行hashset.add方法时的部分源码
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
我们还是使用上面的Fruit类来做演示,其中只是重写了equals方法,并没有重写hashcode方法。
public static void main(String[] args) throws Exception {
Fruit apple1 = new Fruit("apple",12);
Fruit apple2 = new Fruit("apple",12);
// apple1 hashcode-603742814
System.out.println("apple1 hashcode-" + apple1.hashCode());
// apple2 hashcode-1067040082
System.out.println("apple2 hashcode-" + apple2.hashCode());
Set<Fruit> fruitSet = new HashSet<>(16);
fruitSet.add(apple1);
fruitSet.add(apple2);
// print 2
System.out.println(fruitSet.size());
}
由于apple1和apple2的hashcode不同,因此,虽然它们的内容是相同的,但是仍然会被重复添加到hashset中。
如果我们在Fruit中重写了hashcode,使得内容相同的对象具有相同的hashcode值,那么这个问题就能得到解决:
// 在Fruit类中添加如下代码
@Override
public int hashCode() {
return Objects.hash(name, weight);
}
public static void main(String[] args) throws Exception {
Fruit apple1 = new Fruit("apple",12);
Fruit apple2 = new Fruit("apple",12);
// apple1 hashcode--1411060813
System.out.println("apple1 hashcode-" + apple1.hashCode());
// apple2 hashcode--1411060813
System.out.println("apple2 hashcode-" + apple2.hashCode());
Set<Fruit> fruitSet = new HashSet<>(16);
fruitSet.add(apple1);
fruitSet.add(apple2);
// print 1
System.out.println(fruitSet.size());
}
以上例子只是使用散列表中的hashset作为案例进行说明,其它的散列表也是有相同的问题的,比如hashtable、hashmap等。
所以,在使用散列表类型的数据结构增删查改对象的时候,该对象一定要重写equals方法和hashcode方法。
三、为什么要存在hashcode?
有人就会问,散列表为什么要这么设计,不是让我们开发变得很不方便了么?
其实,散列表这么设计恰恰是为了提高效率,至少是为了提高查找的效率。当一个对象需要存储到散列表中的时候,存储的地址是由该对象的哈希值计算出来的。
区别于链表,散列表中对象的存储并不是线性的,而是根据对象的哈希值计算得到一个散列表空间的地址,也许是在散列表空间的头部,也许是在中间或者尾部。
当我们需要在散列表中查找是否存在某个对象的时候,就根据对象计算哈希值,再得到地址,去散列表中看看是否该地址已经有对象了,如果没有,那么就认为还不存在这个对象;如果已经有对象了,也不代表两个对象就一定相同,还需要对它们的值进行比对。
所以,我们看出,使用散列表进行查找是效率非常高的,瞬间就能定位到高概率存在的地址上进行进一步的比对,复杂度为O(1),而链表则慢得多,查找复杂度为O(n)。
四、关于hashcode使用的一些规则
关于hashcode的详细过程比较复杂,本文不作展开,以后会单独写文章予以介绍,这里只是列出几个使用规则。有兴趣的朋友也可以参考《数据结构》进行学习。
- 相同内容的对象它们的hashcode一定相同;
- hashcode相同的对象它们的内容不一定是相同的;
五、我们该如何重写hashcode
首先,判断你创建的对象是否会存储到散列表性质的数据结构中,如果确定不会,那就不用重写hashcode,它只对散列表性质的数据结构有效。
其次,如果要用到散列表性质的数据结构,那么重写hashcode的时候要遵循上述两个hashcode的使用规则。即,在产生hashcode的时候,仅根据对象的内容来生成hashcode。如此,只要两个对象内容相同,势必产生一样的hashcode。
比如下面是用IDE自动生成的hashcode重写方法:
@Override
public int hashCode() {
return Objects.hash(name, weight);
}
如果两个不同的对象产生相同的hashcode怎么办?这个就不用我们操心了,散列表自己会有一套散列碰撞处理机制,它总是能帮助我们找到正确的对象的。详细处理机制可以参考《Java中的哈希表》。
网友评论