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的剖析的主要内容,如果未能解决你的问题,请参考以下文章

一次性搞清楚equals和hashCode

Java实战equals()与hashCode()

Java ==,equals() 和hashCode

Java equals 方法与hashcode 方法的深入解析

代码安全 | 第十七期:对象只定义了Equals和Hashcode方法之一的漏洞

hashCode()与 equals() 之间的关系