hashCode和equals的作用
hashCode和equals都是顶级类Object的方法,它们通常适用于对象之间判断相等使用,常用的业务场景如HashMap中的key是否存在判断就是先比较hashCode再比较equals。
对象值和引用的关系: 两个对象是否相等有两种判断模式,一种是引用地址是否相等,一种是对象值是否相等。
如:
String s1 = new String("abc");
String s2 = s1;
System.out.println(s1==s2);// true
上面代码就创建了一个对象,但是有两个引用指针指向了该对象,这种就叫做引用相等
String s1 = new String("abc");// 创建一个对象
String s2 = new String("abc");// 创建一个对象
System.out.println(s1==s2);// false
上面代码创建了两个对象,也就是在堆内存开辟了两个对象的内存空间,因此返回false,这种引用不相等,但是它们的值都是"abc"因此可以说这两个对象是值相等的
由以上结论可知:引用相等的对象也一定是值相等,因为它们都是同一个对象。但是值相等的对象,它们不一定引用相等。
hashCode:调用hashCode方法默认是根据对象引用内存地址计算出来的一个int类型的对象hash值,计算是本地方法计算,标识当前对象的引用地址。
equals:调用equals需要传递一个需要比较的对象,这种比较通常是值比较,跟引用没有关系。
什么时候需要重写hashCode和equals
当需要自定义对象相等逻辑的时候,就需要重写hashCode和equals,比如上面例子的两个String都为"abc"的对象,在使用过程中我们会认为这两个对象是相等的,但是在JVM内存层面来说,这又是两个不同的内存空间,因此它们不是相等的。即在遇到这种JVM内存地址不同,但是使用过程中又认为是相同的对象的时候,就需要重写hashCode和equals方法。
如上面的String,在源码中就是重写了hashCode和equals,同理Integer也是重写了hashCode和equals。
ps:Integer的hashCode返回就是当前的值,在equals比较的时候也是比较的两个Integer的int值。
java.lang.Integer#hashCode(int)
java.lang.Integer#equals
public static int hashCode(int value) { return value; }public boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false; }
在实际使用过程中HashMap中判断key是否重复就是基于hashCode和equals判断。
HashMap map = new HashMap();
map.put(new String("123"),"123");
map.put(new String("123"),"456");
System.out.println(map.size());// result : 1
以上即时是put了两个新对象key,最后在map中也只会存一个元素,就是因为HashMap内部调用String重写后的hashCode和equals后,判断这两个对象是相等对象,因此第二个put执行的是更新操作而不是添加操作。
HashMap判断key是否重复或包含
java.util.HashMap#getNode
first.hash == hash && // always check first node 先判断hashCode值是否相等
((k = first.key) == key || (key != null && key.equals(k))) // 在判断值或者引用是否相等
hashCode是否能够用随机值
不能:hashCode一定是跟对象自身属性息息相关,必须具备同一对象在任意次数调用hashCode生成的hash值都是相同的,如果不相同,如以上HashMap的key判断,就会出现即时是相同引用的对象,依旧会有不同的hashCode,从而造成了一个HashMap存了多个相同的key,违反了其设计原则。
重写hashCode和equals的规则
重写hashCode一定要重写equals,因为hashCode默认是根据对象地址使用hash算法计算出来的hash值,既然是通过hash算法计算出来的,因此也会出现hash算法的弊端,hash碰撞的问题,当两个对象值不相同,但是hashCode值相同的时候,就发生了hash碰撞。这时候单纯用一个hashCode来判断对象是否相等是无法满足的,因此这时候还需要借助对象自身的属性也就是值来判断。
同理重写equals也一定要重写hashCode,当一个对象equals为true的时候,并不代表这个对象对象的hashCode也相等,这时候会出现不同对象,但是属性值完全相同的情况,如果只重写equals不重写hashCode也会造成混乱,这种混乱在Map这种数据结构里面尤其严重。
扩展
hashCode值是存在对象头里面的吗?
对象创建的时候并不会将hash值存储在对象头,但是调用了hashCode后,会将hash值存在对象头中
我们通过JOL工具类,打印对象内存信息
Object obj = new Object();
System.out.println("调用hashCode前:");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
System.out.println("====================================");
System.out.println("调用hashCode后:"+obj.hashCode());
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
System.out.println("====================================");
结果:
调用hashCode前:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) (9)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) dd 01 00 f8 (11011101 00000001 00000000 11111000) (-134217251)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
====================================
调用hashCode后:1742920067
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 09 83 d9 e2 (00001001 10000011 11011001 11100010) (-489061623)
4 4 (object header) 67 00 00 00 (01100111 00000000 00000000 00000000) (103)
8 4 (object header) dd 01 00 f8 (11011101 00000001 00000000 11111000) (-134217251)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到上面打印结果:
首先明确每一行的意义,打印结果的OFFSET标识当前对象在内存地址的偏移量,SIZE标识从偏移量开始+SIZE的地址的具体值。
打印结果前两行,也就是前8个字节,代表了对象头的markword,我们通常说的hash值存储也在这里。
调用hashCode前:
0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) (9)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
调用hashCode后:1742920067
0 4 (object header) 09 83 d9 e2 (00001001 10000011 11011001 11100010) (-489061623)
4 4 (object header) 67 00 00 00 (01100111 00000000 00000000 00000000) (103)
在调用hashCode前,对象的markword和调用hashCode后的markword是不一样的,因此得出结论hashCode的hash值不会一开始存储在对象头中,但是调用了hashCode后会将hash值存于对象头中。
hashCode值 1742920067(十进制) = 67 E2 D9 83(十六进制),正好对应了调用后的对象头值。
在64位情况下并且无锁状态,使用了31个字节来存储hashCode,110 0111 1110 0010 1101 1001 1000 0011(二进制)= 1742920067(十进制) = 67 E2 D9 83(十六进制)。
参考:www.bilibili.com/video/BV1aJ…
结论
在对象创建的时候无锁状态下,对象hash值是不会存储在对象头中,如果调用了hashCode方法后,会将hash值存储在对象头中用31位保存。如果对象作为了锁使用并且升级后,则不会再保存hash值在对象头中,而是实时计算。
重写equals的规约
- 自反性:任何非null情况下x.equals(x)必须返回true,也就是自身对象与自身对象比较一定要相等
- 对称性:任何非null情况下,x.equals(y)要等于y.equals(x)
- 传递性:任何非null情况下,x.equals(y)=true,y.equals(z)=true,必须x.equals(z)=true
- 一致性:任何非null情况下,只要在对象的信息没有被修改,不管多少次比较x.equals(y)始终为true
- 任何对象存在的情况下,equals(null)都应该返回false