【java基础】重写hashCode和equals方法
在使用HashMap和HashSet保存数据时,需要根据键的哈希值来进行散列,先来看如果不重写这两个方法会怎么样?
public class Main {
public static void main(String[] args) {
//HashSet hs=new HashSet();
HashMap<People,Integer> map=new HashMap<People,Integer>();
People p1=new People("蜀","刘备");
People p2=new People("蜀","张飞");
People p3=new People("魏","曹操");
People p4=new People("蜀","刘备");
map.put(p1, 1);
map.put(p2, 2);
map.put(p3, 3);
System.out.println(map.get(p4));
}
}
class People{
String country="";
String name="";
public People(String country,String name){
this.country=country;
this.name=name;
}
}
输出的结果为 null
因为HashMap是使用hash算法进行散列到数组中的。
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;
}
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
首先调用函数hash(key),获取哈希值,然后在调用方法index(hash,table.length)获取散列表的索引位置。
获取索引位置后,就获取这个索引位置的Entry<K,V>对象,然后遍历此链表,在此过程中通过判断key是否是同一个对象,或者调用key的equals()方法,来判断是否和已存在此链表上的Entry<K,V>是否相等。如果相等就覆盖旧值,返回旧值,否则就讲此对象存放在该数组位置,该对象的后继节点就是原来存放在此数组位置的那个对象。
所以经过上面分析,我们知道HashMap会先使用key的hashCode函数来获取散列表的索引位置,获取索引位置后,就通过key的equals()方法对此索引位置上的对象链进行遍历判断是否相等。相等就覆盖,否则就存在对象链的头位置。
由此可以看出,重写equals()判断对象相等的话,那么hashCode()返回的哈希值也要相等,只要hashCode相等才能在散列表中搜索到这个索引位置进行对象相等判断。
现在我们对开头的示例进行重写:
public class Main {
public static void main(String[] args) {
//HashSet hs=new HashSet();
HashMap<People,Integer> map=new HashMap<People,Integer>();
People p1=new People("蜀","刘备");
People p2=new People("蜀","张飞");
People p3=new People("魏","曹操");
People p4=new People("蜀","刘备");
map.put(p1, 1);
map.put(p2, 2);
map.put(p3, 3);
System.out.println(map.get(p4));
}
}
class People{
String country="";
String name="";
public People(String country,String name){
this.country=country;
this.name=name;
}
public boolean equals(Object obj){
if(obj instanceof People){
People p=(People)obj;
if(country.equals(p.country)&&name.equals(p.name))
return true;
else
return false;
}else{
return false;
}
}
public int hashCode(){
return country.hashCode();
}
}
输出结果是 1
下面给出重写hashCode的规则:
Google首席Java架构师Joshua Bloch在他的著作《Effective Java》中提出了一种简单通用的hashCode算法
1. 初始化一个整形变量,为此变量赋予一个非零的常数值,比如int result = 17;
2. 选取equals方法中用于比较的所有域,然后针对每个域的属性进行计算:
(1) 如果是boolean值,则计算f ? 1:0
(2) 如果是byte\char\short\int,则计算(int)f
(3) 如果是long值,则计算(int)(f ^ (f >>> 32))
(4) 如果是float值,则计算Float.floatToIntBits(f)
(5) 如果是double值,则计算Double.doubleToLongBits(f),然后返回的结果是long,再用规则(3)去处理long,得到int
(6) 如果是对象应用,如果equals方法中采取递归调用的比较方式,那么hashCode中同样采取递归调用hashCode的方式。 否则需要为这个域计算一个范式,比如当这个域的值为null的时候,那么hashCode 值为0
(7) 如果是数组,那么需要为每个元素当做单独的域来处理。如果你使用的是1.5及以上版本的JDK,那么没必要自己去 重新遍历一遍数组,java.util.Arrays.hashCode方法包含了8种基本类型数组和引用数组的hashCode计算,算法同上,
public static int hashCode(long a[]) {
if (a == null)
return 0;
int result = 1;
for (long element : a) {
int elementHash = (int)(element ^ (element >>> 32));
result = 31 * result + elementHash;
}
return result;
}
Arrays.hashCode(...)只会计算一维数组元素的hashCOde,如果是多维数组,那么需要递归进行hashCode的计算,那么就需要使用Arrays.deepHashCode(Object[])方法。
3. 最后,要如同上面的代码,把每个域的散列码合并到result当中:result = 31 * result + elementHash;
4. 测试,hashCode方法是否符合文章开头说的基本原则,这些基本原则虽然不能保证性能,但是可以保证不出错。
2. 为什么每次需要使用乘法去操作result? 主要是为了使散列值依赖于域的顺序,还是上面的那个例子,Test t = new Test(1, 0)跟Test t2 = new Test(0, 1), t和t2的最终hashCode返回值是不一样的。
3. 为什么是31? 31是个神奇的数字,因为任何数n * 31就可以被JVM优化为 (n << 5) -n,移位和减法的操作效率要比乘法的操作效率高的多。
上一篇: Iterable和Iterator的关系
下一篇: MyBatis学习笔记
推荐阅读
-
Java中如何正确重写equals方法
-
浅谈java 重写equals方法的种种坑
-
Java学习总结之方法重载和方法重写
-
java中hashCode和equals什么关系,hashCode到底怎么用的
-
详解java中的深拷贝和浅拷贝(clone()方法的重写、使用序列化实现真正的深拷贝)
-
Java连载57-equals重写、finalize方法、hashCode方法
-
【java基础】为什么重写toString()方法?
-
jdk基础之Object类hashCode()和equal()方法
-
关于重写equals()和hashCode()的思考
-
一文搞懂hashCode()和equals()方法的原理