equals与hashCode的剖析
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了equals与hashCode的剖析相关的知识,希望对你有一定的参考价值。
Java中有两条众所周知的规定
1)对象相等必须有相等的hashCode
2)两个对象不相等,hashCode可能相同
但是为什么有这两个规定呢,不可能凭空产生,总是有原因的,下面我们就来分析两条规定的由来
1. 哈希码
首先这两条规定和哈希码密不可分,甚至可以说这两条规定就是为了对象的hashCode()实现。
hashCode()方法返回的就是该对象的哈希码,是一个整数,通过该哈希码我们可以确定该对象在散列存储结构中的地址(比如Set,HashMap的key),快速索引该对象,查找效率极高。
这是别人帖子中看到的,比较易懂的解释,整理了下
1) 假设内存有以下地址,每个地址可以看作一个内存块,常规只存放一个对象,hashCode相同的特殊情况下可存放多个对象 0 1 2 3 4 5 6 7 我们这里有个类,这个类有个字段ID,我们要把类的实例放在这些位置上,如果不用hashCode,那么就要一个个位置挨个找,判断哪些地址可用,或者用二分法之类的算法
2) 假设我们定义hashCode为ID%8,然后把对象放在取得余数的那个位置,比如9%8余数为1,就把对象放在1的位置,那么索引就可以通过ID%8取余数直接找到对象位置了
3) 那如果hashCode相同怎么办,那么这时候就需要equals了,先通过hashCode判断对象在某个内存块,然后通过equals找到我们的对象
4) 因此重写equals,最好重写hashCode,想想看,我们要查找对象,如果确定对象内存块,再查找对象是不是效率的多,而不是从头到尾一个个去比较
2. 规定来源
通过上面哈希码的分析,我们粗略了解散列存储结构的内存模型。
以Set集合为例,每一个元素位置都是一个内存块,绝大多数情况下只存放一个元素,但有些特殊情况下存放多个元素。
这个元素位置就是指hashCode,也就是说hashCode确定是否是同一个内存块,而不是确定是否是同一个对象。
因此如果两个对象相等,那么所在的内存块一定相等,即hashCode必须相等,而如果两个对象不相等,hashCode也可能相等(同一个内存块中的不同对象)。
3. 实例分析[1]
对象内存地址隐式判断
情景:将同一个对象连续两次放入Set集合中,让hashCode返回值相同,而此时并不会调用equals
//测试类 public class Test { public static void main(String[] args) throws Exception { Set<Student> set = new HashSet<Student>(); Student s1 = new Student("aaaa"); set.add(s1); System.out.println(set); s1.stuName = "cccc"; // 修改属性比对 set.add(s1); System.out.println(set); } } // Student类 class Student { public String stuName; private static int a = 0; public Student(String stuName) { this.stuName = stuName; } // hashCode总是返回同一个值 @Override public int hashCode() { System.out.println("hashCode-----------"); return a; } @Override public boolean equals(Object obj) { System.out.println("equals-----------"); return false; } @Override public String toString() { return stuName; } }
输出结果:
hashCode-----------
[aaaa]
hashCode-----------
[cccc]
输出结果分析:第一次放入s1的时候获取一个hashCode,在set中不存在则直接放入,第二次获取hashCode,但此时在set中存在相同的hashCode,但我们并没有看到调用equals,因此这里就可能隐式的对两个对象的内存地址做了个判断,发现相同,则直接替换先前的元素,不进行equals比较,和下面的例子[2]做对比。
4. 实例分析[2]
情景:不同对象放入Set集合中,hashCode返回值相同
//测试类 public class Test { public static void main(String[] args) throws Exception { Set<Student> set = new HashSet<Student>(); Student s1 = new Student("aaaa"); Student s2 = new Student("cccc"); set.add(s1); set.add(s2); System.out.println(set); } } // Student类 class Student { public String stuName; private static int a = 0; public Student(String stuName) { this.stuName = stuName; } @Override public int hashCode() { System.out.println("hashCode-----------"); return a; } @Override public boolean equals(Object obj) { System.out.println("equals-----------"); return false; } @Override public String toString() { return stuName; } }
输出结果:
hashCode-----------
hashCode-----------
equals-----------
[aaaa, cccc]
输出结果分析:存放s1的时候获取一个hashCode,在set中不存在直接放入,存放s2的时候获取hashCode,发现hashCode存在,但s1和s2内存地址不同,然后调用equals来判断,确定不是同一个对象,放入s2。
注:如果equals方法返回改为true,则最终set的输出结果为[cccc],此时认为s1和s2两个对象相等。
5. 实例分析[3]
情景:不同对象放入Set集合中,hashCode返回值不同
//测试类 public class Test { public static void main(String[] args) throws Exception { Set<Student> set = new HashSet<Student>(); Student s1 = new Student("aaaa"); Student s2 = new Student("cccc"); set.add(s1); set.add(s2); System.out.println(set); } } // Student类 class Student { public String stuName; private static int a = 0; public Student(String stuName) { this.stuName = stuName; } @Override public int hashCode() { System.out.println("hashCode-----------"); a++; // 让每次返回的hashCode都不同 return a; } @Override public boolean equals(Object obj) { System.out.println("equals-----------"); return false; } @Override public String toString() { return stuName; } }
输出结果:
hashCode-----------
hashCode-----------
[aaaa, cccc]
输出结果分析:存放s1的时候获取一个hashCode,在set中不存在直接放入,存放s2时获取hashCode发现不同,直接确定两个对象不想等,放入s2,不再调用equals来判断。
6. 实例分析[4]
情景:同一对象放入Set集合中,hashCode返回值不同
//测试类 public class Test { public static void main(String[] args) throws Exception { Set<Student> set = new HashSet<Student>(); Student s1 = new Student("aaaa"); set.add(s1); set.add(s1); System.out.println(set); } } // Student类 class Student { public String stuName; private static int a = 0; public Student(String stuName) { this.stuName = stuName; } @Override public int hashCode() { System.out.println("hashCode-----------"); a++; return a; } @Override public boolean equals(Object obj) { System.out.println("equals-----------"); return false; } @Override public String toString() { return stuName; } }
输出结果:
hashCode-----------
hashCode-----------
[aaaa, aaaa]
输出结果分析:存放s1的时候获取一个hashCode,在set中不存在直接放入,再存放s1的时候发现hashCode不同,则直接放入,也不再判断内存地址是否相同。
7. 总结
equals和hashCode可能会困扰一部分人,尤其是新人,而且在重写equals问题上即便老手也可能会使用不当,因此理解equals和hashCode是十分必要的,下面我用一张图表示
以上是关于equals与hashCode的剖析的主要内容,如果未能解决你的问题,请参考以下文章
Java equals 方法与hashcode 方法的深入解析