前面学完抽象类、接口、内部类之后,接下来非常值得继续掌握的一个知识点,就是Object类中的几个核心方法:
equals()hashCode()toString()
这三个方法在 Java 开发中出现频率非常高。
可以这么说:
- 不理解
equals(),你就容易在“对象比较”上出错; - 不理解
hashCode(),你就很难真正弄明白HashMap、HashSet; - 不理解
toString(),你打印对象时看到的内容就总是一串看不懂的地址值风格字符串。
所以这部分内容,不只是面试常考,在真实开发里也非常常用。
一、Object 类是什么?
Object是 Java 中所有类的根类。
也就是说,任何一个类,如果没有明确继承其他类,那么默认都会继承Object。
classStudent{}上面这个类,其实等价于:
classStudentextendsObject{}因此,任何 Java 对象,天然都可以使用Object类中的方法,比如:
equals()hashCode()toString()getClass()wait()notify()
其中最常用、最基础的,就是今天要讲的这三个。
二、equals() 方法详解
1. equals() 是干什么的?
equals()用来比较两个对象是否“相等”。
但是这里的“相等”,到底比较什么,要分情况看。
先看代码:
Strings1=newString("abc");Strings2=newString("abc");System.out.println(s1==s2);// falseSystem.out.println(s1.equals(s2));// true这里就说明:
==比较的是两个变量保存的地址是否相同equals()比较的是两个对象的内容是否相同(前提是类重写了 equals 方法)
2. == 和 equals() 的区别
这是初学阶段最容易混淆的地方。
(1)对于基本数据类型
==比较的是值。
inta=10;intb=10;System.out.println(a==b);// true(2)对于引用数据类型
==比较的是地址值。
Strings1=newString("hello");Strings2=newString("hello");System.out.println(s1==s2);// false因为s1和s2指向的是两个不同对象。
(3)equals() 默认也是比较地址
很多同学以为equals()天生就是比较内容,其实不是。
Object类中默认的equals()实现,本质上还是比较地址。
你可以简单理解为:
publicbooleanequals(Objectobj){return(this==obj);}也就是说,如果一个类没有重写equals(),那么它的效果和==差不多。
3. 为什么很多类的 equals() 比较内容?
因为像String、Integer这些类,已经重写了equals()方法。
例如String重写后,比较的是字符串内容。
Strings1=newString("java");Strings2=newString("java");System.out.println(s1.equals(s2));// true这就是因为String类把“两个字符串内容相同”定义为了相等。
4. 自定义类为什么要重写 equals()?
来看一个例子:
classStudent{Stringname;intage;publicStudent(Stringname,intage){this.name=name;this.age=age;}}publicclassTest{publicstaticvoidmain(String[]args){Students1=newStudent("张三",18);Students2=newStudent("张三",18);System.out.println(s1.equals(s2));}}输出结果是:
false为什么?
因为Student没有重写equals(),所以调用的是Object的实现,本质比较的是地址。
但从业务角度看:
- 姓名相同
- 年龄相同
我们通常会认为这两个学生对象“内容相等”。
这时就应该重写equals()。
5. equals() 的重写示例
importjava.util.Objects;classStudent{privateStringname;privateintage;publicStudent(Stringname,intage){this.name=name;this.age=age;}@Overridepublicbooleanequals(Objectobj){if(this==obj){returntrue;}if(obj==null||getClass()!=obj.getClass()){returnfalse;}Studentstudent=(Student)obj;returnage==student.age&&Objects.equals(name,student.name);}}测试:
publicclassTest{publicstaticvoidmain(String[]args){Students1=newStudent("张三",18);Students2=newStudent("张三",18);System.out.println(s1.equals(s2));// true}}6. equals() 重写时常见写法解释
this == obj
表示如果两个引用本来就指向同一个对象,那肯定相等,直接返回true。
obj == null
如果传进来的对象是null,那一定不相等。
getClass() != obj.getClass()
表示只有类型完全相同,才继续比较。
强制类型转换
Studentstudent=(Student)obj;因为传入参数类型是Object,需要转成当前类后才能访问属性。
比较成员变量
returnage==student.age&&Objects.equals(name,student.name);这一步才是真正的“内容比较”。
7. equals() 使用时的注意事项
(1)重写 equals() 时,通常也要重写 hashCode()
这是一个非常重要的规则,后面会详细讲。
(2)尽量使用Objects.equals()比较引用类型
因为这样可以避免空指针异常。
Objects.equals(name,student.name)比下面这种更安全:
name.equals(student.name)因为如果name是null,后者会报错。
三、hashCode() 方法详解
1. hashCode() 是什么?
hashCode()方法返回的是对象的哈希值,也可以理解为一个整数“编号”。
这个值不是对象地址本身,但通常和对象的存储、查找效率有关。
Objectobj=newObject();System.out.println(obj.hashCode());每个对象都可以调用这个方法。
2. hashCode() 有什么用?
它最主要的用途是:提高哈希结构中的查找效率。
比如:
HashMapHashSetHashtable
这些集合在存储对象时,并不是一个一个慢慢比较,而是先根据hashCode()找位置,再进一步比较。
也就是说:
- 先看哈希值
- 再决定放到哪个位置
- 如果哈希冲突,再调用
equals()进一步比较
所以:
hashCode()主要服务于哈希类集合。
3. equals() 和 hashCode() 的关系
这是最核心的内容之一。
Java 规范要求:
规则 1
如果两个对象通过equals()比较结果为true,那么它们的hashCode()必须相同。
规则 2
如果两个对象的hashCode()相同,equals()不一定为true。
也就是说:
- 相等对象,哈希值必须相等
- 哈希值相等,对象不一定相等
4. 为什么重写 equals() 后还要重写 hashCode()?
因为如果你只重写了equals(),没有重写hashCode(),那么在哈希集合中可能会出现逻辑错误。
看例子:
importjava.util.HashSet;importjava.util.Objects;classStudent{privateStringname;privateintage;publicStudent(Stringname,intage){this.name=name;this.age=age;}@Overridepublicbooleanequals(Objectobj){if(this==obj){returntrue;}if(obj==null||getClass()!=obj.getClass()){returnfalse;}Studentstudent=(Student)obj;returnage==student.age&&Objects.equals(name,student.name);}}publicclassTest{publicstaticvoidmain(String[]args){HashSet<Student>set=newHashSet<>();set.add(newStudent("张三",18));set.add(newStudent("张三",18));System.out.println(set.size());}}很多人以为结果会是1,但实际上可能是2。
原因就在于:
equals()认为这两个对象相等- 但它们没有相同的哈希值规则
HashSet可能先把它们分到不同位置- 于是去重失败
5. hashCode() 的正确重写方式
Java 中通常直接使用Objects.hash(...)。
importjava.util.Objects;classStudent{privateStringname;privateintage;publicStudent(Stringname,intage){this.name=name;this.age=age;}@Overridepublicbooleanequals(Objectobj){if(this==obj){returntrue;}if(obj==null||getClass()!=obj.getClass()){returnfalse;}Studentstudent=(Student)obj;returnage==student.age&&Objects.equals(name,student.name);}@OverridepublicinthashCode(){returnObjects.hash(name,age);}}这样写后:
- 只要
name和age相同 equals()返回truehashCode()也会一致
这就满足规范要求了。
6. 一句话理解 hashCode()
你可以把hashCode()理解成:
给对象生成一个“分类编号”,方便哈希集合快速定位。
而equals()则是:
当两个对象被分到同一区域后,再进一步精确比较它们是否真的相等。
所以两者经常配合使用。
四、toString() 方法详解
1. toString() 是干什么的?
toString()方法用于返回对象的字符串表示形式。
例如:
Objectobj=newObject();System.out.println(obj.toString());System.out.println(obj);上面两句输出效果本质是一样的,因为直接打印对象时,Java 会自动调用对象的toString()方法。
2. Object 默认的 toString() 长什么样?
默认情况下,Object类中的toString()返回结果格式大致如下:
类名@十六进制哈希值比如:
Student@1b6d3586这个结果对于程序运行没问题,但对人阅读不友好。
所以在开发中,我们通常会重写toString(),让对象打印时直接显示关键属性。
3. 为什么要重写 toString()?
看下面例子:
classStudent{Stringname;intage;publicStudent(Stringname,intage){this.name=name;this.age=age;}}publicclassTest{publicstaticvoidmain(String[]args){Students=newStudent("李四",20);System.out.println(s);}}输出类似:
Student@6d06d69c这对调试和日志输出帮助不大。
如果重写toString():
classStudent{Stringname;intage;publicStudent(Stringname,intage){this.name=name;this.age=age;}@OverridepublicStringtoString(){return"Student{name='"+name+"', age="+age+"}";}}再打印:
Student{name='李四',age=20}这样就直观多了。
4. toString() 的典型使用场景
(1)打印对象
System.out.println(student);(2)调试程序
当你想快速看对象内部数据时,重写toString()非常方便。
(3)日志输出
在记录日志时,输出对象信息会更加清晰。
五、三个方法放在一起理解
我们用一个完整例子,把equals()、hashCode()、toString()串起来。
importjava.util.Objects;classStudent{privateStringname;privateintage;publicStudent(Stringname,intage){this.name=name;this.age=age;}@Overridepublicbooleanequals(Objectobj){if(this==obj){returntrue;}if(obj==null||getClass()!=obj.getClass()){returnfalse;}Studentstudent=(Student)obj;returnage==student.age&&Objects.equals(name,student.name);}@OverridepublicinthashCode(){returnObjects.hash(name,age);}@OverridepublicStringtoString(){return"Student{name='"+name+"', age="+age+"}";}}publicclassTest{publicstaticvoidmain(String[]args){Students1=newStudent("王五",19);Students2=newStudent("王五",19);System.out.println(s1.equals(s2));System.out.println(s1.hashCode());System.out.println(s2.hashCode());System.out.println(s1);}}输出可能类似:
true1234567812345678Student{name='王五',age=19}这说明:
equals():比较内容hashCode():生成一致的哈希值toString():打印更友好的对象信息
六、初学者最容易踩的坑
1. 把==和equals()混为一谈
记住:
==比较地址(引用类型)equals()比较内容(前提是重写了)
2. 只重写 equals(),不重写 hashCode()
这会导致哈希集合中行为异常。
3. 以为 toString() 不重要
实际上它对调试、打印日志、排查问题都很有帮助。
4. equals() 中直接调用属性的 equals(),可能空指针
比如:
name.equals(other.name)如果name为null,会报错。更推荐:
Objects.equals(name,other.name)七、面试/考试常见问题总结
1. Object 类中最常用的方法有哪些?
答:equals()、hashCode()、toString()。
2. equals() 和==有什么区别?
答:
==对基本类型比较值,对引用类型比较地址equals()默认比较地址,重写后一般比较内容
3. 为什么重写 equals() 时通常要重写 hashCode()?
答:
因为 Java 规定相等对象必须有相同哈希值,否则在HashSet、HashMap等集合中会出现逻辑问题。
4. 为什么要重写 toString()?
答:
为了让对象打印时显示更有意义的内容,方便调试和日志查看。
八、最后总结
学完Object类中的这三个方法后,你要真正记住下面这三句话:
- equals():判断两个对象内容是否相等
- hashCode():为对象提供哈希值,方便哈希集合快速定位
- toString():返回对象的字符串表示,便于输出和调试
再进一步总结就是:
equals()决定“逻辑上是否相等”hashCode()决定“哈希结构里怎么快速找”toString()决定“打印出来给人看是什么样子”
如果这三个方法你真正理解了,后面学习集合框架,尤其是HashSet、HashMap时,会顺畅很多。
九、练习题
练习 1
定义一个Book类,包含title和price两个属性,重写equals()和toString()。
练习 2
继续完善Book类,再重写hashCode(),并放入HashSet中测试是否能正确去重。
练习 3
分别使用==和equals()比较两个内容相同的字符串对象,观察输出结果并解释原因。