尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Java Object 类全解:equals/hashCode 契约、clone 的坑、finalize 为何被废?面试与实战一次讲透

Java Object 类全解:equals/hashCode 契约、clone 的坑、finalize 为何被废?面试与实战一次讲透 文章收录专栏Java 核心原理全解源码・并发・面试实战前言如果说String是 Java 里最常用的类那Object就是最 “默默无闻” 的根类 ——所有类都继承它。但正是这个几乎没什么字段的类藏着面试官最爱问的契约、最容易踩的坑equals和hashCode到底什么关系为什么重写一个就必须重写另一个clone为什么被大家嫌弃finalize又为什么被官方亲手送走这篇文章从源码契约到生产实战一次性讲透Object类的核心方法与约定最后再盘点 10 个高频生产坑。建议收藏面试前过一遍。一、Object 是什么java.lang.Object是所有类的根父类数组类型也直接继承它。它本身不声明任何实例字段只是提供一组普适方法。其中有几个方法标记为native如getClass、hashCode、clone、wait/notify说明真正的实现在 JVM 层面Java 代码只是入口。常用方法全景方法用途是否常被重写equals(Object)逻辑相等判断✅ 是hashCode()哈希值✅ 是toString()字符串表示✅ 是clone()对象拷贝较少finalize()回收前清理已废弃否getClass()运行时类否wait()/wait(long)/wait(long,int)线程等待否notify()/notifyAll()唤醒线程否一句话记忆真正需要你重写的就三个 ——equals、hashCode、toString其他的要么不用动要么别乱动。二、equals 与 hashCode一对必须 “成双出现” 的契约2.1 equals 的默认行为与重写目的Object中equals的默认实现等价于也就是比较两个引用是否指向同一对象地址比较。只有当我们想基于对象内容判断逻辑相等时才需要重写equals。比如两个Person只要name和age相同就算同一个人。2.2 重写 equals 必须遵守的 5 条约定这 5 条来自Object的 Javadoc是硬性契约任何一条被破坏equals 的实现就是错的性质要求通俗理解自反性x.equals(x)必须为true自己当然等于自己对称性x.equals(y)为true⇒y.equals(x)必须为true比较结果不能 “偏心”传递性x.equals(y)且y.equals(z)⇒x.equals(z)必须为trueAB、BC 则 AC一致性对象未修改时多次调用结果一致结果要稳定非空性x.equals(null)必须为false任何人都不等于 null2.3 标准实现模板public class Person { private String name; private int age; Override public boolean equals(Object obj) { // 1. 引用相同直接 true快速路径 if (this obj) return true; // 2. 类型检查含 null 判断 if (obj null || getClass() ! obj.getClass()) return false; // 3. 强转后逐个比较关键字段 Person other (Person) obj; return age other.age Objects.equals(name, other.name); } }实现要点清单先if (this obj) return true;做快速路径类型检查记得先判 null避免 NPE浮点字段用Double.compare/Float.compare不能用原因见后文坑点数组字段用Arrays.equals多维用deepEquals可能为null的字段用Objects.equals。2.4 ⚠️ 高频考点getClass()还是instanceof这是面试里最容易展开追问的点。方式语义适用场景getClass() ! obj.getClass()精确类型一致才相等子类与父类不相等要求严格类型相等obj instanceof Person子类实例也可以与父类比较相等希望支持多态比较经典陷阱 —— 对称性被破坏假设父类Point用instanceof重写 equals子类ColorPoint增加了color字段point.equals(colorPoint) // true父类只比较坐标不看颜色 colorPoint.equals(point) // false子类要求连颜色一起比较对称性被破坏了—— 两个对象互相比较结果竟然不一样解决方案用getClass()做精确类型判断子类对象与父类永不相等用 Lombok 的canEqual机制最推荐组合优先于继承不要在 equals 上做继承扩展能省掉 90% 的麻烦。2.5 hashCode 的契约与实现默认行为基于对象内存地址计算一个整数不同 JVM 实现可能不同。铁律必须遵守两个对象equals相等 ⇒ 两者的hashCode必须相等两个对象不相等 ⇒ hashCode 不要求不同但不同能提升哈希表性能减少冲突对象作为哈希表 key 期间参与 equals 的字段没变 ⇒ 多次 hashCode 结果一致。实现要点参与equals比较的所有字段都必须参与hashCode计算推荐Objects.hash(...)一行搞定或者手写经典的 31 素数算法Override public int hashCode() { int result 17; // 任意非零初始值 result 31 * result (name ! null ? name.hashCode() : 0); result 31 * result age; return result; }为什么用 3131 是奇素数做乘法时不容易在溢出后丢失太多信息冲突更少JVM 会把31 * i优化成(i 5) - i移位 减法比乘法更快。容易犯的错不要把hashCode做成随机值不要用会发生变化的字段参与计算比如随时会变的toString拼接结果。2.6 契约破坏的后果面试必答只重写equals而不重写hashCode会发生什么两个equals相等的对象哈希码不同 → 在HashMap中落入不同桶→get找不到在HashSet中去重失效重复元素被重复加入。这是集合正确性的基石问题也是生产环境最常见、最难排查的低级 bug 之一。三、toString日志与调试的好帮手默认行为Person6d06d69c—— 类名 十六进制哈希码可读性极差。建议重写它返回包含类名和关键字段的简洁字符串日志、异常、调试信息里都会用到。格式建议Person{name张三, age30}IDEA 自动生成的风格。Override public String toString() { return Person{name name , age age }; }实现要点用 IDE 生成别手写容易漏禁止输出敏感信息—— 密码、密钥、token 打印到日志里就是事故字段太多时只输出必要字段集合类ArrayList、HashMap已经重写了 toString数组没有要用Arrays.toString一维/Arrays.deepToString多维。四、clone一个设计糟糕的 “官方” 拷贝方式4.1 基本机制Object.clone()是protected native方法必须实现Cloneable标记接口否则调用时抛CloneNotSupportedException默认实现是浅拷贝基本类型拷贝值引用类型只复制引用新旧对象共享同一个引用字段。4.2 重写方式浅拷贝示例public class Person implements Cloneable { private String name; private int age; Override public Person clone() { try { return (Person) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); // 已实现 Cloneable正常不会触发 } } }深拷贝示例可变引用字段需要手动处理public class Department implements Cloneable { private String name; private ListPerson employees; Override public Department clone() { try { Department cloned (Department) super.clone(); // 外层深拷贝 cloned.employees new ArrayList(employees); // 若 Person 可变还需逐个克隆元素 // cloned.employees.replaceAll(Person::clone); return cloned; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } }4.3 clone 的 5 大缺陷异常契约反人类Cloneable只是标记接口不实现就抛受检异常但只有运行时才知道错绕过构造器clone不调用构造器无法保证不可变约束和防御性拷贝被正确执行深拷贝太麻烦需要手写递归多层嵌套很容易漏final字段无法处理final字段在clone中不能重新赋值编译直接报错接口无法声明clone继承体系中子类容易漏重写埋下隐患。4.4 生产上用什么替代方式说明拷贝构造器public Person(Person other) { this.name other.name; ... }静态工厂public static Person copyOf(Person other) { ... }record拷贝new Person(p.name(), p.age())序列化反序列化需实现Serializable注意性能和安全第三方库Apache CommonsSerializationUtils.clone结论绝大多数团队的规范是 ——禁止使用 clone一律用拷贝构造器或静态工厂。五、finalize从 “回收前清理” 到 “被官方亲手废弃”5.1 原设计finalize的设计初衷是在对象被 GC 回收前调用用来释放非内存资源文件句柄、网络连接等。理想很丰满现实很骨感。5.2 废弃状态JDK 9起标记DeprecatedJDK 18JEP 421进一步标记为Deprecate for Removal即将移除。5.3 为什么要废弃执行时机不确定不保证及时执行甚至可能完全不执行程序直接退出、没有 GC 压力时增加 GC 开销对象要经过至少两次回收且要进入 F-Queue 等待执行 finalize对象复活resurrection在 finalize 里重新建立该对象的引用会让 “已判定可回收” 的对象复活破坏 GC 逻辑异常被吞掉finalize 里抛出的异常会被 JVM 忽略资源泄漏了却毫无提示。5.4 替代方案场景推荐做法显式释放资源try-with-resources 实现AutoCloseable兜底清理不可靠但有比没有好CleanerJDK 9特殊场景对象生命周期追踪PhantomReferenceReferenceQueue常规对象手动finally释放或走框架生命周期回调Cleaner用法JDK 9public class Room implements AutoCloseable { private static final Cleaner CLEANER Cleaner.create(); private final Cleaner.Cleanable cleanable; public Room() { this.cleanable CLEANER.register(this, () - System.out.println(清理资源)); } Override public void close() { cleanable.clean(); } }注意Cleaner只是 “兜底”仍然必须显式调用close()别指望它替你做资源管理。六、wait /notify线程协作的正确姿势6.1 硬性约束wait()、notify()、notifyAll()三个方法必须在synchronized块 / 方法内调用否则抛IllegalMonitorStateException。原因它们依赖 “当前线程持有该对象的监视器锁” 这个前提。6.2 各方法行为方法行为wait()释放锁进入WAITING直到被notify/notifyAll唤醒wait(long timeout)释放锁进入TIMED_WAITING超时自动唤醒wait(long, int)带纳秒精度实际按毫秒粒度处理notify()随机唤醒一个等待线程notifyAll()唤醒所有等待线程6.3 生产级经典模式wait 必须配 while// 生产者-消费者wait 必须配 while 循环防止虚假唤醒 synchronized (lock) { while (queue.isEmpty()) { // 不是 if lock.wait(); } data queue.poll(); }为什么用 while 而不是 if虚假唤醒spurious wakeupJVM 规范允许线程在没有收到任何 notify 的情况下被无故唤醒多消费者竞争线程被唤醒后条件可能已被其他线程抢先破坏必须重新检查。6.4 wait 与 sleep 的区别维度wait()sleep()归属Object 方法Thread 静态方法同步要求必须持有监视器锁不需要是否释放锁释放锁不释放锁唤醒方式notify/notifyAll或超时到点自动醒现代并发开发优先用 JUC 的Condition、BlockingQueue替代 wait/notify更安全也更好用。七、10 个生产环境高频坑建议收藏1. 只重写 equals 忘掉 hashCodeHashSet去重失效、HashMap检索不到。最常见也最伤的低级 bug。2. 用可变对象做 HashMap 的 key放入后修改了参与 hashCode 的字段 → 哈希码变化 →get永远返回null。key 必须用不可变对象String、Integer、枚举、record。3. equals 参数写成本类equals(Person other)是重载不是重写Override会直接编译报错即使不报错Map/Set调用的是equals(Object)你的逻辑永远不会被执行。4. LombokData的继承陷阱Data生成的 equals/hashCode默认不含父类字段子类对象比较会漏掉父类属性且与父类比较可能破坏对称性。必须加EqualsAndHashCode(callSuper true)。5.BigDecimal的 equals 陷阱new BigDecimal(1.0).equals(new BigDecimal(1.00))为false值相等但 scale 不同。在HashMap/Set里做 key 或去重会出问题业务上要按数值相等请用compareTo。6. 数组直接用 equalsarray1.equals(array2)是引用比较数组没重写 equals必须用Arrays.equals一维/Arrays.deepEquals多维。7. JPA/Hibernate 实体的 equals/hashCode懒加载时实体是代理对象getClass()与真实实体不同若 equals 用getClass()判断代理与实体永远不相等。建议用稳定的业务主键参与 equals/hashCode别用可变字段。8. toString 泄漏敏感信息日志打印会泄露密码、密钥、token线上事故的高发原因务必排查。9. 单例 / 枚举天然适合做 key枚举的 equals/hashCode 稳定且唯一强烈推荐用作Mapkey。10.Objects.equals与混用基本类型包装类要用equals避免落入Integer缓存-128~127之外的比较陷阱。八、版本演进Object 相关的新特性版本相关变化JDK 7引入Objects.equals/hash/requireNonNull简化 equals/hashCode 编写JDK 9finalize标记Deprecated引入CleanerJDK 14record进入预览自动生成 equals/hashCode/toStringJDK 16record正式转正JEP 395JDK 18JEP 421 将finalize标记为 Deprecate for Removalrecord的语义新面试高频点public record Person(String name, int age) { }自动生成equals、hashCode、toString、gettername()、构造器equals 语义运行时类型相同 所有组件component逐一相等天然不可变字段final可安全用作 HashMap 的 key。局限不能继承其他类若组件本身是可变引用类型如List“不可变” 不彻底hashCode 仍有风险想自定义默认构造器行为需用 compact constructor 或工厂方法。九、总结equals 重写五原则自反、对称、传递、一致、非空hashCode 铁律equals 相等 ⇒ hashCode 必相等参与 equals 的字段全部进 hashCode三者常一起重写equals、hashCode、toString或用record一键生成clone 是 “过时设计”生产用拷贝构造器 / 工厂别用 clonefinalize 已死用 try-with-resources Cleanerwait/notify 必须在 synchronized 内wait 一律配 while 循环防虚假唤醒。Object 类真正值钱的不在方法本身而在于它定义的那套对象契约。理解了契约你才写得出正确的集合用法和可靠的实体设计。
返回列表