tostring()默认返回类名@哈希码,是调试兜底方案;必须重写以提供可读、安全、稳定的状态描述,否则日志和调试失效。

Object 类是 Java 一切对象的根,面试官问它,不是想听你背方法列表,而是考察你对面向对象底层契约、JVM行为、并发模型和集合机制的理解深度。答得好,能直接拉开技术段位差距。
紧扣“为什么设计成这样”讲清楚核心方法
别只说“toString 返回字符串”,要解释设计意图和实际影响:
-
toString():默认返回
ClassName@hashCode,本质是调试友好性兜底方案;重写时必须保证可读、稳定、不含敏感信息(比如密码字段不能打出来);Spring Boot 的 Actuator 端点、日志框架(如 Logback)都依赖它——没重写,日志里全是Person@1a2b3c,排查问题效率归零。 - equals() 和 ==:== 是内存地址比较,equals 默认也是;但一旦涉及业务逻辑(比如两个 User 对象 id 相同就应视为相等),就必须重写;重写必须满足五条契约(自反、对称、传递、一致、非空),否则在 Collections.sort 或 TreeSet 中会出诡异 bug。
-
hashCode():不是“随便返回个数”,它是哈希表(HashMap/HashSet)性能的命脉;只要 equals 返回 true,hashCode 就必须相同;反过来不强制,但若大量不同对象返回相同 hashCode,就会退化成链表遍历,O(1) 变 O(n)——这就是为什么用
Objects.hash(name, age)比手写name.hashCode() * 31 + age更安全(自动处理 null)。
讲透 wait/notify 为什么在 Object 而不在 Thread
这是高级岗高频陷阱题,答案不能停留在“每个对象都能锁”,得说到语言设计哲学:
Java JDK 25 来自 OpenJDK 官方归档,版本为 JDK 25,本条下载地址已指向官方 Windows x64 zip 安装包直链,适合调试旧项目或兼容旧版 Java 运行环境。
- wait/notify 是基于监视器(monitor)的线程通信机制,而 monitor 绑定在对象头(Object Header)上,不是线程本身;JVM 通过对象的 mark word 实现锁升级(无锁→偏向→轻量→重量),所以通信必须依附于对象。
- 如果放在 Thread 类里,就变成“线程 A 主动通知线程 B”,这违背了 Java 的协作式等待模型:线程只能等待“某个对象”的条件,不能指定通知谁——由 JVM 在等待队列中选一个唤醒,避免竞态和死锁。
- 必须强调:
wait()会释放锁,sleep()不会;wait()必须在 synchronized 块内调用,否则抛IllegalMonitorStateException——这不是语法限制,而是语义保障:只有持有锁的线程,才有资格决定“我等什么条件、什么时候被唤醒”。
clone 和 finalize 已淘汰,但得知道为什么
面试官可能故意问这两个方法,测试你是否关注 Java 演进:
-
clone():浅拷贝语义模糊,深拷贝实现复杂且易错;现代替代方案是构造函数传参、Builder 模式或序列化(如 Jackson 的
copy());JDK 17+ 中Cloneable接口已被标记为@Deprecated(forRemoval = true)。 -
finalize():不可靠、性能差、无法保证调用时机;JDK 9 开始被弃用,JDK 18 彻底移除;资源清理必须用
try-with-resources或Cleaner(基于虚引用的确定性回收)。 - 补充一点:
getClass()是 final 方法,不能重写——因为运行时类信息是对象身份的一部分,篡改会导致反射、序列化、安全检查全线崩溃。
结合真实场景说明“不重写会怎样”
光讲理论不够,用后果倒推必要性:
- 没重写
equals/hashCode→ 放进HashSet的两个相同用户会被当成不同对象,导致重复注册;放进HashMap后用 key 查不到值,接口返回 null,前端报 500。 - 没重写
toString()→ 单元测试断言失败时只看到Person@4f2a3e,debug 要一层层点开属性;线上log.info("user: {}", user)打印一堆无意义字符,SRE 查问题多花 20 分钟。 - 误用
wait()不加循环检查条件 → 虚假唤醒(spurious wakeup)导致业务逻辑跳过校验,比如库存扣减没判断剩余量就执行,超卖。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










