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

📅 发布时间:2026/9/9 13:34:05
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等于bb等于c那么a必须等于c。这条在继承场景下最容易踩坑。一致性对象没被修改过多次调用equals结果必须相同。千万别在equals里放一个随机数或者可变状态。非空性任何对象调用equals(null)必须返回false而不是抛NullPointerException。日常开发里最常见的坑是判断类型用instanceof还是getClass()。如果你用instanceof那么子类对象和父类对象之间也可能equals成功这符合对称性吗不一定。比如父类Person和子类Studentstudent.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.User3f0ee7cb这种。这串东西对调试的用处其实不大你看到的是一堆类名加地址根本不知道对象内部字段是什么值。所以强烈建议在实体类、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哪怕引用类型是ObjectgetClass拿到的依然是实际创建的User类。这个特性在写框架、做JSON序列化、ORM映射时是核心入口因为拿到Class对象你就能通过反射拿到它的字段、方法、注解、父类和接口几乎什么事都能干。getClass和instanceof的区别也是面试常客。instanceof判断的是“是不是这个类或它的子类实例”getClass判断的是“运行时类型是不是严格等于这个类”。举个例子Dog继承Animaldog instanceof Animal是true但dog.getClass() Animal.class是false。什么时候用哪个如果你只关心对象能不能当某种类型用用instanceof如果你要求精确匹配类型用getClass。4. clone 与 finalize拷贝和清理的冷知识4.1 clone 的浅拷贝原理与坑clone方法的本意是创建一个对象的副本它是一个protected native方法。为什么是protected而不是public这是JDK的刻意设计Object并不知道你的对象里有哪些字段它只能做“逐位复制”也就是浅拷贝——基本类型字段直接拷贝值引用类型字段拷贝的是引用地址。这意味着复制出来的对象和原对象会共享同一个引用类型的子对象。要正确使用clone必须做两件事实现Cloneable接口。这个接口是个空标记它不定义任何方法只是告诉JVM“这个类允许被clone”。不实现的话调用super.clone()会抛CloneNotSupportedException。重写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开始被标记为废弃deprecatedJava 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 LinkedListString 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 就别手动 waitJava 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不一样再一看代码只重写了equalshashCode还是默认的。修复方式很简单用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对象模型、哈希容器、线程协作、垃圾回收这四大块知识串成了一条线。我准备了这张对照表给你随时查阅熟悉到能默写出来应付面试和日常开发都会从容得多。