Java中hashCode、equals、toString方法
这三个方法是Object类中的方法,换句话说是所有的类都会具有的方法。但是这三个方法对初学者来说一直有迷惑性,面试中也常被问到。现在整理一下,也写一写自己的理解。
Object中的这三个方法
在Jdk中,所有的类的共同根父类Object.class中,这三个方法是这样定义的:
1 | public class Object{ |
下面一一说明这三个方法:
1)toString:
toString()在Object中的默认实现其实是打印出这个类的名称和这个对象实例的hash值。
比如下面的结果:
java.lang.Object@15db9742
在一些其他的类中,往往会重写这个方法,比如ArrayList的toString就是打印内部存储的数组。
重写toString的方法往往是为了便于调试,即在debug模式下能够很方便的看到一个对象的内部属性情况,当然也可以用System.out.println()打印输出。
ArrayList的toString()方法由父类AbstractList的父类AbstractCollection实现。
AbstractCollection.toString():
1 | public String toString() { |
2)equals:
Object类中的默认equals()方法是对象之间使用“==”比较。默认的“==”是比较对象的内存引用。
所以默认的equals方法比较的是两个对象的内存引用是否相等。
但是,在开发或其他的类库中,比较对象的内存引用其实没有什么太大的意义,所以有很多类又会重写equals方法。
3)hashCode:
Object类中的hashCode的实现是由JVM实现的,这是一个native方法。
查阅相关资料和结合API文档可以得知:
hashCode方法可以这样理解:
首先JVM将对象存放的位置分为几块,或者叫几个“桶”,然后每一个新new出来的对象会先计算其在JVM内存中的“位置”,然后再根据这个“位置”放在合适的“桶”中。
为什么JVM要这样做呢?
如果形象一点理解的话,可以有这样的情景:
假如要查找一个学校的某一个学生,怎样查找更快呢?是拿着点名簿从上往下一个一个找,还是先获得该学生的学号(hash值),再根据学号判断是哪个班级,接着再根据其他信息能直接定位到具体的这个学生,不用再遍历学生的信息表。
比如我的学校的某个学生的学号为:208140123,可以判断他所在的年级是14届,班级是01班,班内序号是23,这样能很快的定位到学生。
像HashSet、HashMap等都是利用hash的典型类库。
equals详解
再来看看equals的方法:
1 | public boolean equals(Object obj) { |
其用法是obj1.equals(obj2),顾名思义是比较两个对象是否相等。
那么两个对象相等具体是什么意思,换句话说什么是相等的对象?
API中有这样一段话(已翻译):
1 | A 对称性(symmetric):如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。 |
也就是说除非特殊需求,判断两个对象是否相等需要遵循以上的规则。
Object中的equals只有“==”,但是却完全符合上面的那个规则,是不是感觉很神奇。
下面写一个Person类,来方便理解。绝大数代码可以直接由Eclipse或者Idea生成,而且是正确的。
Person.java:
1 | package com.chain.blog.test.day07; |
接下来再测试一下:
1 | private static void test3() { |
测试结果:
1 | true |
由上可以基本掌握equals的用法。
hashCode的详解
前面讲到hash的基本作用是为了便于快速查找。
那么hash还有其他作用吗?
Java中有一个很重要的集合类:HashSet。
Set具有确定性、互异性、无序性这三大特性。
HashSet的底层是由HashMap来实现。
先来看看HashSet.put()方法:
1 | public boolean add(E e) { |
API文档中描述的是如果添加的元素重复(已存在)那么将这个待添加的元素被丢弃并返回false。
那么如何判断要添加的元素是否重复(也可以理解为已存在)呢?
接着看看HashMap的put方法:
1 | public V put(K key, V value) { |
有源码可以看出,一个新增add元素的大致过程:
1)先计算对象的hash值
2)判断hash表中这个对象的hash值对应的位置是否由元素,
a) 如果没有元素,则添加上去,并返回;
b) 如果有元素,则接着判断equals方法,
i) 如果equals方法不想等,那么添加这个元素到这个hash所在位置链接的红黑树中;
ii) 如果equals方法相等,那么可以判断元素重复添加,不执行添加操作且返回已存在的那个值。
由此也可以看出hashCode和equals方法的大致关系,后面再讲。
那么hash在查找get元素上的作用,删除remove元素上的作用呢?
可以阅读源码发现和add的作用是类似的。
hashCode和equals
hashCode和equals可谓是难兄难弟,需要“捆绑”在一起使用和修改。
前面已经知道,像HashSet之类在添加和删除元素时都需要先判断hashCode,然后再判断equals。
先使用hash一是为了快速找到元素,防止遍历查找,二是为了能大致先预判判断元素是否相等。
如果两个对象连hashCode都不相等,那么肯定不是相等的对象,也就不用再进一步判断equals了。
换句话说,如果两个对象不想等,那么hashCode可能相等也可能不想等。
所以如果对象需要修改equals方法的话,也要修改hashCode方法,否则将起不到预期的作用。
比如Person中,如果不重写hashCode方法,只是重写了equals方法,那么如果将Person添加到HashSet集合中时将会在先调用hashCode时返回不相等。
这样就会出现peter和peteralso,乃至peteralso2都添加进了Set中。因为这三者都是不同的对象实例,所以hashCode都不一样的可能性很高。
所以应该谨慎对待hashCode和equals方法。
补充例子
下面再来一个例子,更好的说明hashCode和equals方法:
1 | private static void test4() { |
猜一猜结果是什么?
是不是这个:
1 | 4 |
其实正确的是这个:
1 | 4 |
这个是为什么呢?
因为修改了p2的name属性后,这个对象的hash和equals都已经发生了改变。
首先hash已经改变,因为hash计算中name参与其中。
而HashSet在数据添加时,对象的位置已经根据对象自己的hash值添加到合适的位置。
而调用了remove方法时,remove方法会先计算要删除元素o的hash值,然后再根据这个hash值去已经成型的hash表中查找。而元素已经改变,因而很有可能是找不到这个元素的,就算找到了也有可能会出现误删。因此要么删除失败,要么就是误删除。
这是一个大坑,所以当使用HashSet之类时,要注意添加进Set的元素的hashCode值不能再改变了。同理equals的结果也一样,不然会违反一致性的要求。
当然hashCode和equals还有其他注意事项,以后继续学习再补充吧。
越是简单的东西有时却越是复杂。