【java】彻底搞懂equals()和“==”的区别,以及重写equal()时为什么也必须重写hashCode()

场景

面试常问equals()和“==”的区别?去重时为什么要重写equals()和hashcode()方法,只重写一个可不可以?虽然能答出来,但总觉得是一知半解,没有彻底理解它。
先假设一个场景,如果你想查找一个集合中是否包含某个对象,那么程序应该怎么写呢?
通常的做法是逐一取出每个元素与要查找的对象一一比较,当发现两者进行equals比较结果相等时,则停止查找并返回true,否则,返回false。但是这个做法的一个缺点是当集合中的元素很多时,譬如有一万个元素,那么逐一的比较效率就特别差。于是有人发明了一种哈希算法来提高从该集合中查找元素的效率,这种方式将集合分成若干个存储区域(可以看成一个个桶),每个对象可以计算出一个哈希码,可以根据哈希码分组,每组分别对应某个存储区域,这样一个对象根据它的哈希码就可以分到不同的存储区域(不同的桶中)。如下图所示:
在这里插入图片描述

源码

在java中,一般一个对象默认使用父类object中的equals和hashcode方法,看下源码中的定义:
在这里插入图片描述
equals():如果不重写,默认比较的是两个对象的内存地址

在这里插入图片描述
hashcode():用C或C++代码写的,通过hash算法将该对象的内部地址换成一个整数值
通过源码解读,我总结以下几点:

  • 1.若重写了equals(Object obj)方法,则有必要重写hashCode()方法。
  • 2.若两个对象equals(Object obj)返回true,则hashCode()有必要也返回相同的int数。
  • 3.若两个对象equals(Object obj)返回false,则hashCode()不一定返回不同的int数。
  • 4.若两个对象hashCode()返回相同int数,则equals(Object obj)不一定返回true。
  • 5.若两个对象hashCode()返回不同int数,则equals(Object obj)一定返回false。
  • 6.同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。
    那到底什么时候需要重写equals和hashCode呢?
    一般来说涉及到对象之间的比较大小就需要重写equals和hashCode,实际上这只是一条规范,如果不这样做程序也可以执行,只不过会隐藏bug。一般一个类的对象如果会存储在HashTable,HashSet,HashMap等散列存储结构中,那么重写equals后最好也重写hashCode,否则会导致存储数据的不唯一性(存储了两个equals相等的数据)。而如果确定不会存储在这些散列结构中,则可以不重写hashCode。但是个人觉得还是重写比较好一点,谁能保证后期不会存储在这些结构中呢,况且重写了hashCode也不会降低性能,因为在线性结构(如ArrayList)中是不会调用hashCode,所以重写了也不要紧,也为后期的修改打了补丁。
    下面来看一张对象放入散列集合的流程图:
    在这里插入图片描述
    从上面的图中可以清晰地看到在存储一个对象时,先进行hashCode值的比较,然后进行equals的比较。可能现在你已经对上面的6点归纳有了一些认识。我们还可以通过JDK中得源码来认识一下具体hashCode和equals在代码中是如何调用的。
  • hashset

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
hashset的存值就是是根据hashmap的key值存放原理,不过hashset不能存null,

测试

  • 测试一:覆盖equals(Object obj)但不覆盖hashCode(),导致数据不唯一性
public class HashCodeTest {
	public static void main(String[] args) {
		Collection set = new HashSet();
		Point p1 = new Point(1, 1);
		Point p2 = new Point(1, 1);
 
		System.out.println(p1.equals(p2));
		set.add(p1);   //(1)
		set.add(p2);   //(2)
		set.add(p1);   //(3)
 
		Iterator iterator = set.iterator();
		while (iterator.hasNext()) {
			Object object = iterator.next();
			System.out.println(object);
		}
	}
}
 
class Point {
	private int x;
	private int y;
 
	public Point(int x, int y) {
		super();
		this.x = x;
		this.y = y;
	}
 
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Point other = (Point) obj;
		if (x != other.x)
			return false;
		if (y != other.y)
			return false;
		return true;
	}
 
	@Override
	public String toString() {
		return "x:" + x + ",y:" + y;
	}
}

  • 结果:

true
x:1,y:1
x:1,y:1
  • 原因分析:

(1)当执行set.add(p1)时(1),集合为空,直接存入集合;

(2)当执行set.add(p2)时(2),首先判断该对象(p2)的hashCode值所在的存储区域是否有相同的hashCode,因为没有覆盖hashCode方法,所以jdk使用默认Object的hashCode方法,返回内存地址转换后的整数,因为不同对象的地址值不同,所以这里不存在与p2相同hashCode值的对象,因此jdk默认不同hashCode值,equals一定返回false,所以直接存入集合。

(3)当执行set.add(p1)时(3),时,因为p1已经存入集合,同一对象返回的hashCode值是一样的,继续判断equals是否返回true,因为是同一对象所以返回true。此时jdk认为该对象已经存在于集合中,所以舍弃。

  • 测试二:覆盖hashCode方法,但不覆盖equals方法,仍然会导致数据的不唯一性
  • 修改Point类:
public class HashCodeTest {

    public static void main(String[] args) {
        Collection set = new HashSet();
        Point p1 = new Point(1, 1);
        Point p2 = new Point(1, 1);

        System.out.println(p1.equals(p2));
        set.add(p1);   //(1)
        set.add(p2);   //(2)
        set.add(p1);   //(3)

        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object object = iterator.next();
            System.out.println(object);
        }
    }
}
class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        super();
        this.x = x;
        this.y = y;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + x;
        result = prime * result + y;
        return result;
    }

    @Override
    public String toString() {
        return "x:" + x + ",y:" + y;
    }

}

输出结果:

false
x:1,y:1
x:1,y:1
  • 原因分析:

(1)当执行set.add(p1)时(1),集合为空,直接存入集合;

(2)当执行set.add(p2)时(2),首先判断该对象(p2)的hashCode值所在的存储区域是否有相同的hashCode,这里覆盖了hashCode方法,p1和p2的hashCode相等,所以继续判断equals是否相等,因为这里没有覆盖equals,默认使用’=='来判断,所以这里equals返回false,jdk认为是不同的对象,所以将p2存入集合。

(3)当执行set.add(p1)时(3),时,因为p1已经存入集合,同一对象返回的hashCode值是一样的,并且equals返回true。此时jdk认为该对象已经存在于集合中,所以舍弃。

- 综合上述两个测试,要想保证元素的唯一性,必须同时覆盖hashCode和equals才行。
(注意:在HashSet中插入同一个元素(hashCode和equals均相等)时,会被舍弃,而在HashMap中插入同一个Key(Value 不同)时,原来的元素会被覆盖。)

  • 测试三:在内存泄露问题
public class HashCodeTest {
	public static void main(String[] args) {
		Collection set = new HashSet();
		Point p1 = new Point(1, 1);
		Point p2 = new Point(1, 2);
 
		set.add(p1);
		set.add(p2);
		
		p2.setX(10);
		p2.setY(10);
		
		set.remove(p2);
 
		Iterator iterator = set.iterator();
		while (iterator.hasNext()) {
			Object object = iterator.next();
			System.out.println(object);
		}
	}
}
 
class Point {
	private int x;
	private int y;
 
	public Point(int x, int y) {
		super();
		this.x = x;
		this.y = y;
	}
 
 
	public int getX() {
		return x;
	}
 
 
	public void setX(int x) {
		this.x = x;
	}
 
 
	public int getY() {
		return y;
	}
 
 
	public void setY(int y) {
		this.y = y;
	}
 
 
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + x;
		result = prime * result + y;
		return result;
	}
 
 
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Point other = (Point) obj;
		if (x != other.x)
			return false;
		if (y != other.y)
			return false;
		return true;
	}
 
 
	@Override
	public String toString() {
		return "x:" + x + ",y:" + y;
	}
}

结果:

x:1,y:1
x:10,y:10
  • 原因分析:
    假设p1的hashCode为1,p2的hashCode为2,在存储时p1被分配在1号桶中,p2被分配在2号筒中。这时修改了p2中与计算hashCode有关的信息(x和y),当调用remove(Object obj)时,首先会查找该hashCode值得对象是否在集合中。假设修改后的hashCode值为10(仍存在2号桶中),这时查找结果空,jdk认为该对象不在集合中,所以不会进行删除操作。然而用户以为该对象已经被删除,导致该对象长时间不能被释放,造成内存泄露。解决该问题的办法是不要在执行期间修改与hashCode值有关的对象信息,如果非要修改,则必须先从集合中删除,更新信息后再加入集合中。

总结

  • 1.hashCode是为了提高在散列结构存储中查找的效率,在线性表中没有作用。
  • 2.equals和hashCode需要同时覆盖。
  • 3.若两个对象equals返回true,则hashCode有必要也返回相同的int数。
  • 4.若两个对象equals返回false,则hashCode不一定返回不同的int数,但为不相等的对象生成不同hashCode值可以提高 哈希表的性能。
  • 5.若两个对象hashCode返回相同int数,则equals不一定返回true。
  • 6.若两个对象hashCode返回不同int数,则equals一定返回false。
  • 7.同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值