news 2026/9/9 13:34:00

Java Object类11个方法详解:从源码原理到实战应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Object类11个方法详解:从源码原理到实战应用

做Java开发这些年,我面试过不少候选人,也被人问过很多次“Object类有哪些方法”。这个问题看似基础,但它就像一面镜子,能照出一个人对Java语言底层设计到底理解到什么程度。毕竟Object是所有类的父类,Java里一切对象行为的基础都从这里来。它的11个核心方法——equals、hashCode、toString、getClass、clone、finalize、wait、notify、notifyAll,以及wait(long)、wait(long, int)这两个重载——几乎决定了对象的比较、哈希、字符串表示、拷贝、生命周期终结和线程协作方式。这篇文章我会把这11个方法逐个讲透,从源码原理到实战用法,再结合我实际踩过的坑和面试中反复出现的考点,一次性给你说明白。不管你是刚学Java的新手,还是准备跳槽的老兵,这份梳理都值得收藏。

1. Object 类到底是什么:Java 世界的根基

1.1 为什么说“万物皆对象”

先聊聊基础。Java是单根继承的语言,所有类,不管是JDK自带的String、ArrayList,还是你自己写的业务类,最终都直接或间接继承自java.lang.Object。这个设计不是拍脑袋定的,它带来一个巨大的好处:你可以用Object类型引用任何实例,把所有对象当成一个统一的类型来处理。比如写一个通用的日志打印方法,接收Object参数,就能打印任何对象的信息,这就是多态的底层支撑。

也正因如此,Object里定义的方法就成了所有Java对象天生具备的行为。你可以对任何对象调用equals比较是否相等,调用hashCode拿哈希值,调用toString转成字符串。理解Object类,其实就是在理解Java对象模型的基础规则。这些方法虽然看着简单,但每一条背后都有JVM层面的设计和约定,不是随便重写一下就完事的。

1.2 11个方法按照职责来分类

Object类的方法数量,不同资料说法略有差异,常见说法是11个公开的实例方法。如果算上私有静态的registerNatives(),那就是12个,但那个方法只是用来注册本地方法实现的,平时开发根本碰不到。真正需要我们掌握的,是下面这11个:

方法签名类型主要用途
boolean equals(Object obj)实例方法比较两个对象是否相等
int hashCode()实例方法返回对象的哈希码
String toString()实例方法返回对象的字符串表示
Class<?> getClass()实例方法获取对象运行时的Class对象
Object clone()受保护实例方法创建对象的拷贝
void finalize()受保护实例方法垃圾回收前的清理(已废弃)
void wait()实例方法让当前线程进入等待状态
void wait(long timeout)实例方法带超时时间的等待
void wait(long timeout, int nanos)实例方法带毫秒和纳秒精度的等待
void notify()实例方法唤醒一个等待线程
void notifyAll()实例方法唤醒所有等待线程

你会发现,这些方法天然分成两拨:一拨是通用对象方法,负责对象自身的比较、描述、拷贝和生命周期;另一拨是线程协作方法,负责让线程之间配合干活。后一拨是Object类最特别的地方——为什么线程同步相关的方法不放Thread里,而是放在Object里?这个问题我后面会专门讲,先卖个关子。

2. equals 与 hashCode:面试高频区,也是bug重灾区

2.1 equals 默认实现的原理

不重写equals时,Object.equals使用的是引用比较,通俗说就是“看是不是同一个对象”。源码就这么简单:

public boolean equals(Object obj) { return (this == obj); }

这行代码的意思是:只有两个引用指向同一块堆内存时,才返回true。对于String、Integer这些类为什么能用equals比较内容?因为它们重写了equals。所以记住一个关键点:默认的equals是引用比较,而绝大多数业务场景下你需要的是“逻辑相等”——两个对象内容一样就算相等,这时候就必须自己重写。

2.2 重写 equals 必须遵守的5条铁律

Java官方文档里给equals定了几条约定,违反了会出现非常诡异的bug,尤其是往HashSet、HashMap里放对象的时候。我用大白话给你翻译一遍:

  • 自反性:对象无论如何都要等于自己。a.equals(a)必须为true。
  • 对称性:a.equals(b)为true,那么b.equals(a)也必须为true。很多人重写equals时忘了这点,结果前向判断和反向判断结果不一致。
  • 传递性:a等于b,b等于c,那么a必须等于c。这条在继承场景下最容易踩坑。
  • 一致性:对象没被修改过,多次调用equals结果必须相同。千万别在equals里放一个随机数或者可变状态。
  • 非空性:任何对象调用equals(null)必须返回false,而不是抛NullPointerException。

日常开发里最常见的坑是:判断类型用instanceof还是getClass()。如果你用instanceof,那么子类对象和父类对象之间也可能equals成功,这符合对称性吗?不一定。比如父类Person和子类Student,student.equals(person)在instanceof判断下可能返回true,但person.equals(student)返回false,这就破坏了对称性。所以如果你要求两个对象必须类型完全一致才相等,用getClass()更安全;如果允许子类参与比较,则需要小心设计字段比较规则。我习惯是:严格模式下用getClass(),继承设计比较深时干脆不重写equals,或者用组合代替继承。

2.3 hashCode 与 equals 的爱恨纠葛

hashCode的约定最核心的就一条:两个对象equals相等,那么它们的hashCode必须相等。反过来不成立:hashCode相等,equals不一定相等,因为哈希碰撞是允许的。

这个约定的本质原因在于HashMap、HashSet这些哈希容器的查找逻辑:先通过hashCode定位到桶,再在桶里用equals精确匹配。如果你重写了equals却没重写hashCode,会出现一个非常经典的bug:两个内容完全一样的对象,因为hashCode不同被放进HashMap的不同桶里,导致get方法取不到值,contains判断失效。我排查过的最诡异的一个线上问题就是这个:业务方重写了equals但没重写hashCode,导致Set去重完全失效,同一个用户被插入了好几条记录。

实战代码示范一个标准写法:

public class User { private final String name; private final int age; public User(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return age == user.age && Objects.equals(name, user.name); } @Override public int hashCode() { return Objects.hash(name, age); } }

这里用了Objects.hash,它内部会调用Arrays.hashCode,把多个字段合成一个整数。合成时用31作为乘法因子是有讲究的,31是奇素数,乘法可以用移位和减法优化,而且能有效降低碰撞概率。很多老手写成31 * name.hashCode() + age,就是这个道理。

3. toString 与 getClass:调试时最不能忽略的两个方法

3.1 toString 默认输出到底是什么

默认的toString长这样:

public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }

输出大概就是com.example.User@3f0ee7cb这种。这串东西对调试的用处其实不大,你看到的是一堆类名加地址,根本不知道对象内部字段是什么值。所以强烈建议在实体类、DTO、配置类上都重写toString,方便打日志和排查问题。

我自己写toString的经验是:

  • 不要用字符串拼接,用String.format或者更现代的写法,可读性更好。
  • 不要把敏感信息放进去,比如密码、token,日志会打印出来。
  • 字段比较多的时候,可以只打关键业务字段,避免把整个对象打到日志里刷屏。
@Override public String toString() { return String.format("User{name='%s', age=%d, createdTime=%s}", name, age, createdTime); }

如果你在用IDE,大部分IDE都有“Generate toString()”的快捷键,生成完按自己习惯微调就行。生产环境排查问题时,一行清晰可读的toString能帮你省下大量猜时间。

3.2 getClass 与反射入口

getClass返回的是这个对象运行时的Class对象,它和User.class这种编译期常量不完全是一回事。虽然大多数情况下obj.getClass() == User.class都是true,但遇到继承时就有区别了:

Object obj = new User("张三", 18); System.out.println(obj.getClass()); // class com.example.User

哪怕引用类型是Object,getClass拿到的依然是实际创建的User类。这个特性在写框架、做JSON序列化、ORM映射时是核心入口,因为拿到Class对象你就能通过反射拿到它的字段、方法、注解、父类和接口,几乎什么事都能干。

getClass和instanceof的区别也是面试常客。instanceof判断的是“是不是这个类或它的子类实例”,getClass判断的是“运行时类型是不是严格等于这个类”。举个例子,Dog继承Animal,dog instanceof Animal是true,但dog.getClass() == Animal.class是false。什么时候用哪个?如果你只关心对象能不能当某种类型用,用instanceof;如果你要求精确匹配类型,用getClass。

4. clone 与 finalize:拷贝和清理的冷知识

4.1 clone 的浅拷贝原理与坑

clone方法的本意是创建一个对象的副本,它是一个protected native方法。为什么是protected而不是public?这是JDK的刻意设计:Object并不知道你的对象里有哪些字段,它只能做“逐位复制”,也就是浅拷贝——基本类型字段直接拷贝值,引用类型字段拷贝的是引用地址。这意味着复制出来的对象和原对象会共享同一个引用类型的子对象。

要正确使用clone,必须做两件事:

  1. 实现Cloneable接口。这个接口是个空标记,它不定义任何方法,只是告诉JVM“这个类允许被clone”。不实现的话调用super.clone()会抛CloneNotSupportedException。
  2. 重写clone方法,并把返回类型收窄为自己的类型,同时把protected改成public,方便外部调用。

一个典型的浅拷贝写法:

public class User implements Cloneable { private String name; private int age; private Address address; @Override public User clone() throws CloneNotSupportedException { return (User) super.clone(); } }

这里address是引用类型,浅拷贝后两个User的address指向同一个Address对象。如果你改了其中一个的address属性,另一个也会变。这在很多业务场景下是致命的。所以需要深拷贝时,我的做法是:

  • 手动new一个Address,把字段逐个复制进去;
  • 或者用序列化反序列化的方式,实现Serializable接口后,通过对象流复制;
  • 更现代的做法是用Jackson/Gson做JSON序列化再反序列化,简单粗暴但性能稍差;
  • 如果对象结构很复杂,可以考虑第三方库,比如Apache Commons Lang的SerializationUtils。

实战中我其实很少用clone。它的设计被不少人诟病,接口是空的、方法自带checked exception、浅拷贝误导性强,很多现代Java代码规范里甚至明确禁止在业务代码里使用clone。我更推荐直接用构造器、Builder模式或者工厂方法做拷贝,语义清晰得多。但如果是面试,clone的原理还是得答得上来。

4.2 finalize 为什么被官方标记废弃

finalize是JVM垃圾回收在回收对象之前会调用的一个钩子方法,设计意图是让你在对象被回收前释放一些非Java资源,比如关闭文件句柄、释放本地内存。听起来很贴心,但实际用起来坑非常多:

  • 调用时机不确定。GC什么时候执行、对象会不会被回收,都不由你说了算,所以不能依赖它做关键清理。
  • 可能造成对象“复活”。如果在finalize里给对象重新赋了一个强引用,GC就会把它标记为不可回收,这等于把垃圾回收流程搅乱了。
  • 性能开销大。一个重写了finalize的对象,进入回收队列时需要额外的处理流程,会比普通对象慢很多。
  • 它根本不能保证执行。JVM退出时没被回收的对象,finalize可能根本不会被调用。

正因为这些原因,finalize从Java 9开始被标记为废弃(deprecated),Java 18里虽然没有移除,但官方明确建议不要使用。现在释放资源的正确姿势是:实现AutoCloseable接口,配合try-with-resources语句使用;或者用Java 9引入的Cleaner机制做更底层的资源清理。一句话总结:看到finalize,直接绕道走就对了。

5. wait、notify、notifyAll:线程协作的三剑客

5.1 为什么线程等待方法要放在 Object 里

这可能是Object类里最反常识的地方了。线程等待和唤醒,为什么不放到Thread类里?答案是:wait/notify操作的并不是线程本身,而是某一个对象的监视器锁(monitor)。Java里每个对象都内置了一把锁,线程要调用wait,必须先持有该对象的锁,调用之后会释放这把锁并进入等待状态;其他线程拿到同一把锁后调用notify,才能唤醒等待在这个对象上的线程。

如果把这些方法放在Thread类里,就没有办法把“线程和对象锁”绑定在一起了。所以JDK把wait/notify/notifyAll设计成Object的方法,让它们天然和对象锁关联。理解了这个设计,你就能明白为什么这些方法必须在synchronized块里调用——不持有监视器锁就没有资格操作等待队列,直接调用会抛IllegalMonitorStateException。

5.2 wait 和 sleep 到底差在哪

面试常考的一道题:wait和sleep有什么区别?我从四个维度给你拆开:

  • 锁的释放:wait会释放持有的对象锁,让其他线程有机会进入同步块;sleep不会释放任何锁,一直占着锁睡。
  • 使用位置:wait必须在synchronized块或方法里调用;sleep可以用在任何地方。
  • 唤醒方式:wait需要其他线程调用notify/notifyAll才能唤醒,也可以设置超时时间自动醒;sleep到时间自然醒。
  • 异常处理:两者都会抛InterruptedException,但语义不同,wait被中断通常意味着条件变了或者外部要求停止等待,sleep被中断意味着你不想让它睡了。

5.3 生产者和消费者实战:一个完整的例子

用一个最简单的阻塞队列把wait/notifyAll串起来:

class SimpleQueue { private final LinkedList<String> items = new LinkedList<>(); private static final int MAX_SIZE = 10; public synchronized void put(String item) throws InterruptedException { while (items.size() >= MAX_SIZE) { wait(); } items.addLast(item); notifyAll(); } public synchronized String take() throws InterruptedException { while (items.isEmpty()) { wait(); } String item = items.removeFirst(); notifyAll(); return item; } }

这里有几个细节必须注意:

  • 条件判断用while不用if。这是wait最常见的坑。如果用if,线程被唤醒后不会再重新检查条件,如果多个线程同时被唤醒,就可能出现“虚假唤醒”或者条件已经被其他线程改变的情况。官方文档明确推荐在循环里调用wait。
  • 唤醒用notifyAll而不是notify。notify只会随机唤醒一个线程,如果唤醒的是同类型的生产者或消费者,可能造成死等。我只在明确知道只有两个线程且角色互斥的场景下才用notify,否则一律notifyAll。
  • put和take方法都加了synchronized,确保操作的是同一个监视器锁。wait/notifyAll必须和synchronized配合使用,这一点我在代码里用注释强调了很多次。

5.4 现代实践:能用 Condition 就别手动 wait

Java 5之后有了java.util.concurrent包,里面的Lock和Condition是更高级的替代方案。Condition提供了await、signal、signalAll方法,语义和wait/notify对应,但功能更强:你可以为同一个锁创建多个Condition对象,分别管理不同的等待队列。比如在生产者消费者里,一个Condition管理队列满的等待,另一个管理队列空的等待,这样生产者只需要唤醒消费者,消费者只需要唤醒生产者,效率更高,代码也更清晰。

我的建议是:老代码里看到wait/notify不急着改,能看懂就行;但新写的代码优先用BlockingQueue、Condition这些并发工具。它们把底层的等待唤醒逻辑封装好了,能让你的bug少很多。不过面试题考察wait/notify原理依然很常见,原理必须懂。

6. 高频问题排查与面试真题速查

6.1 我遇到的 Object 相关线上事故复盘

第一个事故是equals和hashCode没一起重写导致的。当时有个业务在做用户信息去重,往HashSet里塞用户对象,结果同一个用户ID的对象因为hashCode不同,被当成了两个不同的人,数据库里插入了重复数据。排查时我先打印了对象的hashCode,发现相同的userId但hashCode不一样,再一看代码,只重写了equals,hashCode还是默认的。修复方式很简单:用userId字段重写hashCode,问题立刻消失。

第二个事故是toString里打印了敏感信息。有一次排查日志,发现用户手机号和身份证号被日志完整打印了出来,就是因为某个DTO重写了toString把所有字段都打了出来。后来我加了脱敏工具类,凡是跟安全相关的字段在toString里一律打星号。

第三件事跟clone有关,不是事故,但很典型。当时有个对象要被用于多线程处理,为了不互相影响,直接调用了clone想复制一份。结果两个线程改同一个List,数据全乱了。原因就是浅拷贝共享了List引用。后来改成构造函数里new一个新的ArrayList,问题解决。这种问题非常隐蔽,遇到对象共享数据异常时,可以先检查是不是浅拷贝导致的。

6.2 面试中常见的 10 个问题

面试题关键回答方向
Object类有哪些方法?列出11个方法,说明用途分类
equals和==有什么区别?==比引用,equals默认比引用,重写后比内容
为什么重写equals必须重写hashCode?哈希容器的存储和查找依赖hashCode定位,相等就必须同哈希
hashCode相同但equals不同可以吗?可以,这是哈希碰撞
HashMap为什么用hashCode?先用hashCode定位桶,再用equals处理碰撞
clone为什么是protected?防止任意对象被外部克隆,需主动实现Cloneable并重写
浅拷贝和深拷贝的区别?浅拷贝共享引用,深拷贝复制引用指向的对象
wait和sleep区别?锁、位置、唤醒方式、异常四方面展开
为什么wait/notify必须在synchronized里?等待和唤醒基于对象监视器锁,不持锁操作会抛异常
为什么用while而不是if判断wait条件?防止虚假唤醒和条件变化后继续执行

6.3 日常写代码的三个实用习惯

最后分享三个我个人的习惯,都是踩过坑之后总结出来的:

第一,实体对象一律重写toString。哪怕你觉得日志用不上,等出了问题你就知道有一行完整可读的对象信息是多么幸运。给自己约法三章:不打敏感字段,不打超长字段,用格式化的方式输出。

第二,类中如果出现了“判断两个对象字段是否相等”的逻辑,直接使用IDE生成的equals和hashCode,然后检查是否需要调整类型判断方式。手写这两个方法又慢又容易漏字段,IDE生成至少保证约定齐全。

第三,做对象拷贝时先问一句:这个对象被拷贝之后会和其他线程共享可变状态吗?如果有,浅拷贝直接出局。宁可多写几行构造函数、Builder或者工厂方法,也别为了省事用clone。在很多团队的代码规范里,clone已经被列进禁用清单了。

Object类这11个方法看起来零零碎碎,其实是把Java对象模型、哈希容器、线程协作、垃圾回收这四大块知识串成了一条线。我准备了这张对照表给你随时查阅,熟悉到能默写出来,应付面试和日常开发都会从容得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 13:32:50

深入解析MyBatis分页插件原理:PageHelper与MyBatis Plus实战

1. 从手写分页到插件接管&#xff0c;先聊清楚分页这件事 做Java后端的人&#xff0c;只要接触过数据库&#xff0c;基本都逃不过分页查询。早期用JDBC的时候&#xff0c;分页是纯手工活&#xff0c;MySQL写 LIMIT offset, size &#xff0c;Oracle玩 ROWNUM &#xff0c;S…

作者头像 李华
网站建设 2026/9/9 13:32:16

级联H桥SVG/STATCOM三相不平衡补偿的三层控制策略与仿真实践

1. 项目概述与整体设计思路 搞电力电子的朋友应该都有体会&#xff0c;SVG&#xff08;静止无功发生器&#xff09;和STATCOM&#xff08;静止同步补偿器&#xff09;在行业内基本被当成同一个东西用&#xff0c;只是叫法不同&#xff0c;一个侧重低压配电&#xff0c;一个侧重…

作者头像 李华
网站建设 2026/9/9 13:32:12

Seelen-UI 插件系统实战:4 类内置模块,从 0 到能用的桌面定制

Seelen-UI 插件系统实战&#xff1a;4 类内置模块&#xff0c;从 0 到能用的桌面定制 【免费下载链接】Seelen-UI The Fully Customizable Desktop Environment for Windows 10/11. 项目地址: https://gitcode.com/GitHub_Trending/se/Seelen-UI 一句话讲清楚它是什么 …

作者头像 李华
网站建设 2026/9/9 13:31:36

从被AI气晕到理性共处:大模型能力边界、偏见与落地实践

1. 从"被AI气晕"到"重新认识AI"&#xff1a;我的认知转变1.1 早期我对AI的理解&#xff1a;死板的规则引擎大概十年前&#xff0c;我对人工智能的看法还停留在一个非常朴素的层面&#xff1a;某个程序能不能"听懂人话"&#xff0c;本质上就是一堆…

作者头像 李华
网站建设 2026/9/9 13:29:26

Ant Design表单焦点错乱:同名Field注册冲突的成因与解法

把时间拨回两周前&#xff0c;我正对着一个Bug工单发愁。同事的描述很简短&#xff1a;“弹窗里的输入框&#xff0c;鼠标点一下&#xff0c;光标却跑到页面顶部的搜索框里去了&#xff1b;弹出的字符没有进弹窗&#xff0c;反而把搜索框填满了。” 我第一反应是“怎么可能”&a…

作者头像 李华
网站建设 2026/9/9 13:29:25

TongWeb版本怎么选?场景化选购指南与部署避坑实践

TongWeb这个牌子&#xff0c;圈里搞信创和国产中间件的人应该都不陌生。但我在社群和后台经常看到一类提问&#xff0c;上来就是“我项目该用哪个版本”&#xff0c;或者“下了一个TongWeb怎么部署还报错”。说句实在话&#xff0c;TongWeb的版本选购真不是看哪个新就选哪个&am…

作者头像 李华