--- title: "01-Object通用方法" created: 2025-11-25 tags: - Java --- # Object通用方法 ## 核心概念 `Object` 类是java中的根类,提供了面向对象编程的基础能力,所有类都隐式继承自 `Object` 类。 在`Object` 类中提供了一些重要的方法,支持对象的比较、哈希码生成、对象状态的表示、线程间通信等功能,其中除了线程同步的方法外,都可以被重写。 理解这些方法的设计原理和正确使用方式,是Java开发的基础。 ```java public class Object { // 比较相关 public boolean equals(Object obj) public native int hashCode() // 字符串表示 public String toString() // 对象复制 protected native Object clone() throws CloneNotSupportedException // 线程通信 public final native void wait() throws InterruptedException public final native void notify() public final native void notifyAll() // 垃圾回收(已废弃) protected void finalize() throws Throwable // 反射相关 public final native Class getClass() } ``` --- ## 一、equals()方法 用于比较两个对象是否“等价”,这个等价:`equals()` 与 \=\= 的区别主要在于对引用类型的判断逻辑不一样,\=\= 是比较两个引用是不是指向同一个对象,也就是“是不是一个地址”,而 `equals()` 是比较两个对象的内容是否相等,比如两个 `String` 内容一样,`equals()` 是 true,但 \=\= 可能是 false。 ### 1.1 默认实现 ```java // Object类的默认实现:比较引用地址 public boolean equals(Object obj) { return (this == obj); } ``` ### 1.2 equals的5大约定(规范) 我们自己写的类如果想要通过 `equals()` 正确比较内容,就需要重写这个方法,并且要遵守一套规则——也就是“equals 通用约定”,包括五个特性:自反性(`x.equals(x)` 一定为 true)、对称性(`x.equals(y)` 与 `y.equals(x)` 结果相同)、 传递性(若 `x.equals(y)` 且 `y.equals(z)`,则 `x.equals(z)`)、 一致性(多次比较结果不变)、非空性(任何对象 `x` 调用 `x.equals(null)` 都应返回 false)。 | 约定 | 说明 | 示例 | | --- | --- | --- | | **自反性** | x.equals(x) 必须为true | a.equals(a) → true | | **对称性** | x.equals(y) = y.equals(x) | a.equals(b) = b.equals(a) | | **传递性** | x=y, y=z → x=z | a=b, b=c → a=c | | **一致性** | 多次调用结果一致 | 对象不变,结果不变 | | **非空性** | x.equals(null) 必须为false | a.equals(null) → false | ### 1.3 标准重写模板 为了满足这些特性,我们一般遵循一个模板:先判断两个对象是不是同一个引用,如果是就返回 true;再判断类是否相同;然后强转类型,最后逐个比较字段内容。 ```java public class User { private Long id; private String name; private Integer age; @Override public boolean equals(Object o) { // 1. 检查是否同一对象(性能优化) if (this == o) return true; // 2. 检查null和类型 if (o == null || getClass() != o.getClass()) return false; // 3. 类型转换 User user = (User) o; // 4. 比较关键字段 return Objects.equals(id, user.id) && Objects.equals(name, user.name) && Objects.equals(age, user.age); } } ``` ### 1.4 常见错误 ```java // ❌ 错误1:参数类型写错(重载而非重写) public boolean equals(User other) { // 应该是Object return this.id.equals(other.id); } // ❌ 错误2:没有处理null public boolean equals(Object o) { User user = (User) o; // o为null时NPE return this.id.equals(user.id); } // ❌ 错误3:使用instanceof导致对称性问题 // 子类和父类比较时可能不对称 public boolean equals(Object o) { if (!(o instanceof User)) return false; // 子类也会通过 // ... } ``` ### 1.5 getClass() vs instanceof ##### `getClass()` `getClass()` 是 `Object` 类中的 final 方法,不可被重写,用于获取对象的**实际运行时类型**,返回一个 `Class` 类型对象,常用于类型判断或反射操作。 它和 [[02-instanceof|instanceof]] 的区别在于判断粒度不同: - `instanceof` 判断“是否属于某类或其子类”,适合继承结构中做兼容性判断; - `getClass()` 判断的是“是否完全属于同一个类”,适用于精确类型匹配,常用于 `equals()` 的实现中避免子类干扰。 ```java // getClass() 精确匹配 Object obj = new ArrayList<>(); System.out.println(obj.getClass() == ArrayList.class); // true ✅ System.out.println(obj.getClass() == List.class); // false ❌ // instanceof 检查继承关系 System.out.println(obj instanceof ArrayList); // true ✅ System.out.println(obj instanceof List); // true ✅ System.out.println(obj instanceof Collection); // true ✅ // getClass():运行时类型,精确匹配 // instanceof:检查 is-a 关系,包括子类 // 陷阱:null 的处理 Object nullObj = null; // System.out.println(nullObj.getClass()); // NPE ❌ System.out.println(nullObj instanceof Object); // false ✅ ``` ```java Object o = new ArrayList<>(); System.out.println(o instanceof List); // true System.out.println(o.getClass() == List.class); // false ``` **选择建议:** - 一般情况用 `getClass()`,保证对称性 - 如果需要子类与父类相等,用 `instanceof`(需谨慎设计) --- ## 二、hashCode()方法 `hashCode()`用于返回哈希值,为哈希表(如 HashMap)提供快速索引,将对象映射到 “桶位置”。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价,这是因为计算哈希值具有随机性,两个值不同的对象可能计算出相同的哈希值。所以在重写 equals() 方法时应当总是重写 hashCode() 方法,保证等价的两个对象哈希值也相等。 ### 2.1 默认实现 ```java // Object类的默认实现:返回对象内存地址的哈希值 public native int hashCode(); ``` ### 2.2 hashCode的3大约定 | 约定 | 说明 | | --- | --- | | **一致性** | 对象不变,hashCode不变 | | **equals相等→hashCode相等** | a.equals(b)=true → a.hashCode()=b.hashCode() | | **hashCode相等≠equals相等** | hashCode相等不要求equals相等(允许哈希冲突) | ### 2.3 为什么重写equals必须重写hashCode? 如果重写了 `equals()`,也必须重写 `hashCode()`,原因是 Java 中的集合类,比如 `HashMap`、`HashSet`,查找元素时是先用 `hashCode()` 找“桶位置”,再用 `equals()` 判断是否是“同一个”。如果两个对象 `equals()` 相等而 `hashCode()` 不一致,在 HashMap 这类基于哈希的结构中,它们会被分到不同的桶,导致找不到或插入重复,破坏集合的正确性。 ```java // 只重写equals,不重写hashCode public class User { private Long id; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return Objects.equals(id, user.id); } // 没有重写hashCode } // 问题:HashMap无法正常工作 User u1 = new User(1L); User u2 = new User(1L); System.out.println(u1.equals(u2)); // true Map map = new HashMap<>(); map.put(u1, "user1"); System.out.println(map.get(u2)); // null!而不是"user1" // 原因:u1和u2的hashCode不同,HashMap先用hashCode定位桶 // hashCode不同,直接认为是不同的key ``` ### 2.4 标准重写模板 至于如何重写`hashcode()`,目标就是:**相等的对象** `hashCode()` **必须相同,不相等的尽量分散,避免冲突**。最常见的写法是使用字符串哈希算法,将所有参与比较的域转换成R进制的数,R一般取31,因为31 是个奇质数,分布更均匀,且与 2 的幂不整除,可以更好地打散哈希值。此外,`31*x` 在底层可以被 JVM 优化为 `(x << 5) - x`,效率更优。 ```java @Override public int hashCode() { return Objects.hash(id, name, age); } // 或者手动实现(性能更好) @Override public int hashCode() { int result = 17; // 非零初始值 result = 31 * result + (id != null ? id.hashCode() : 0); result = 31 * result + (name != null ? name.hashCode() : 0); result = 31 * result + (age != null ? age.hashCode() : 0); return result; } ``` ### 2.5 为什么用31作为乘数? ```java result = 31 * result + fieldHashCode; ``` - **31是奇素数**:减少哈希冲突 - **31 = 32 - 1 = 2^5 - 1**:可优化为位运算 `(result << 5) - result` - **JVM会自动优化**:性能好 --- ## 三、equals与hashCode的关系 ### 3.1 核心关系图 ``` equals相等 ──必须──→ hashCode相等 ↑ ↓ └──── 不一定 ←───────┘ ``` ### 3.2 HashMap中的应用 ``` // HashMap查找过程 1. 计算key的hashCode 2. 根据hashCode定位桶(数组下标) 3. 在桶内遍历链表/红黑树,用equals比较 // 如果只重写equals不重写hashCode put(u1, "v1") → hashCode=100 → 桶[100] get(u2) → hashCode=200 → 桶[200] → 找不到! // 正确情况 put(u1, "v1") → hashCode=100 → 桶[100] get(u2) → hashCode=100 → 桶[100] → equals比较 → 找到! ``` ### 3.3 完整示例 ```java public class User { private Long id; private String name; private Integer age; // 构造方法、getter/setter省略 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return Objects.equals(id, user.id) && Objects.equals(name, user.name) && Objects.equals(age, user.age); } @Override public int hashCode() { return Objects.hash(id, name, age); } } // 使用 User u1 = new User(1L, "Tom", 20); User u2 = new User(1L, "Tom", 20); System.out.println(u1.equals(u2)); // true System.out.println(u1.hashCode() == u2.hashCode()); // true Set set = new HashSet<>(); set.add(u1); set.add(u2); System.out.println(set.size()); // 1(正确去重) ``` --- ## 四、toString()方法 用于返回对象的字符串表示形式,在打印对象、拼日志时都会被自动调用,比如 `System.out.println(obj)` 背后其实就是调用了 `obj.toString()`。 ### 4.1 默认实现 如果没有重写,默认返回的是 `类名@对象哈希码`(哈希码是 `hashCode()` 的十六进制形式),如 `User@5f184fc6`,没有实际业务意义。 ```java // Object类的默认实现 public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } // 示例 User user = new User(); System.out.println(user.toString()); // com.example.User@1b6d3586 ``` ### 4.2 重写规范 因此通常建议我们**重写该方法**,返回更有价值的信息: ```java @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } // 输出:User{id=1, name='Tom', age=20} ``` 这样在调试、日志输出、打印对象信息时就能更直观地看到内容。 - 很多日志框架(如 SLF4J)和工具方法(如 `Arrays.toString()`)依赖对象的 `toString()`; - 使用 Lombok 时,`@ToString` 注解可自动生成该方法; - 覆盖 `toString()` 时,注意避免输出敏感信息(如密码等); - 复杂对象推荐使用 `StringJoiner` 或 `StringBuilder` 拼接,提高效率; - 可以使用 `Objects.toString(field)` 来避免 `null`。 ### 4.3 使用工具类 ```java // 方式1:IDE自动生成(推荐) // 方式2:Lombok @ToString public class User { private Long id; private String name; } // 方式3:Apache Commons @Override public String toString() { return ToStringBuilder.reflectionToString(this); } // 方式4:Google Guava @Override public String toString() { return MoreObjects.toStringHelper(this) .add("id", id) .add("name", name) .toString(); } ``` ### 4.4 日志应用 ```java // 日志输出时自动调用toString() log.info("用户信息: {}", user); // 自动调用user.toString() // 敏感信息处理 @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", password='******'" + // 隐藏敏感信息 '}'; } ``` --- ## 五、clone()方法 ### 5.1 默认实现 ```java // Object类的默认实现:native方法,浅拷贝 protected native Object clone() throws CloneNotSupportedException; ``` ### 5.2 使用clone的步骤 ```java // 1. 实现Cloneable接口(标记接口) public class User implements Cloneable { private Long id; private String name; private Address address; // 引用类型 // 2. 重写clone方法 @Override public User clone() { try { return (User) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(); // 不会发生 } } } ``` ### 5.3 浅拷贝 vs 深拷贝 ```java // 浅拷贝:只复制对象本身,引用类型共享 User u1 = new User(1L, "Tom", new Address("Beijing")); User u2 = u1.clone(); System.out.println(u1 == u2); // false(不同对象) System.out.println(u1.getAddress() == u2.getAddress()); // true(共享引用) u2.getAddress().setCity("Shanghai"); System.out.println(u1.getAddress().getCity()); // Shanghai(被影响) ``` ```java // 深拷贝:递归复制所有引用对象 @Override public User clone() { try { User cloned = (User) super.clone(); cloned.address = this.address.clone(); // 深拷贝引用对象 return cloned; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } ``` ### 5.4 为什么不推荐使用clone()? | 问题 | 说明 | | --- | --- | | **设计缺陷** | 需要实现Cloneable接口,但接口没有clone方法 | | **浅拷贝陷阱** | 默认浅拷贝,容易出错 | | **构造函数绕过** | clone不调用构造函数,可能破坏不变性 | | **异常处理** | CloneNotSupportedException是受检异常 | ### 5.5 推荐的替代方案 ```java // 方案1:拷贝构造函数(推荐) public class User { public User(User other) { this.id = other.id; this.name = other.name; this.address = new Address(other.address); // 深拷贝 } } User u2 = new User(u1); // 方案2:静态工厂方法 public static User copy(User other) { User user = new User(); user.id = other.id; user.name = other.name; user.address = new Address(other.address); return user; } User u2 = User.copy(u1); // 方案3:序列化(深拷贝,性能较差) public static T deepCopy(T obj) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(obj); ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return (T) ois.readObject(); } ``` --- ## 六、wait/notify/notifyAll方法 ### 6.1 为什么在Object类中? - 这些方法操作的是**对象的监视器锁(Monitor)** - 每个对象都有一个监视器锁 - 所以定义在Object类中,所有对象都能使用 ### 6.2 基本用法 ```java synchronized (lock) { while (!condition) { lock.wait(); // 释放锁,进入等待状态 } // 条件满足,继续执行 } synchronized (lock) { condition = true; lock.notify(); // 唤醒一个等待线程 // 或 lock.notifyAll(); // 唤醒所有等待线程 } ``` ### 6.3 生产者-消费者示例 ```java public class Buffer { private Queue queue = new LinkedList<>(); private int capacity = 10; public synchronized void produce(int value) throws InterruptedException { while (queue.size() == capacity) { wait(); // 队列满,等待消费 } queue.offer(value); notifyAll(); // 通知消费者 } public synchronized int consume() throws InterruptedException { while (queue.isEmpty()) { wait(); // 队列空,等待生产 } int value = queue.poll(); notifyAll(); // 通知生产者 return value; } } ``` ### 6.4 注意事项 ```java // ❌ 错误:没有在synchronized块中调用 lock.wait(); // IllegalMonitorStateException // ❌ 错误:用if而不是while synchronized (lock) { if (!condition) { // 应该用while lock.wait(); } // 可能被虚假唤醒,condition仍为false } // ✅ 正确:用while循环检查条件 synchronized (lock) { while (!condition) { lock.wait(); } } ``` --- ## 七、finalize()方法(已废弃) ### 7.1 设计初衷 ```java // 在对象被GC回收前调用,用于释放资源 protected void finalize() throws Throwable { // 清理资源 } ``` ### 7.2 为什么废弃? | 问题 | 说明 | | --- | --- | | **不确定性** | 不知道何时执行,甚至可能不执行 | | **性能问题** | 有finalize的对象GC更慢 | | **复活问题** | finalize中可以让对象复活,导致内存泄漏 | | **异常吞没** | finalize中的异常会被忽略 | ### 7.3 替代方案 ```java // 方案1:try-with-resources(推荐) try (FileInputStream fis = new FileInputStream("file.txt")) { // 使用资源 } // 自动关闭 // 方案2:Cleaner(JDK 9+) public class Resource implements AutoCloseable { private static final Cleaner cleaner = Cleaner.create(); private final Cleaner.Cleanable cleanable; public Resource() { cleanable = cleaner.register(this, () -> { // 清理逻辑 }); } @Override public void close() { cleanable.clean(); } } ``` --- ## 八、高频面试题 ### Q1: 为什么重写equals必须重写hashCode? **答:** - HashMap等集合先用hashCode定位桶,再用equals比较 - 如果只重写equals,两个相等的对象可能有不同的hashCode - 导致HashMap无法正确工作(put和get定位到不同的桶) ### Q2: equals方法的5大约定是什么? **答:** 1. **自反性**:x.equals(x) = true 2. **对称性**:x.equals(y) = y.equals(x) 3. **传递性**:x=y, y=z → x=z 4. **一致性**:多次调用结果一致 5. **非空性**:x.equals(null) = false ### Q3: hashCode相等,equals一定相等吗? **答:** - 不一定。hashCode相等可能是哈希冲突 - 但equals相等,hashCode必须相等 ### Q4: 为什么hashCode用31作为乘数? **答:** - 31是奇素数,减少哈希冲突 - 31 = 2^5 - 1,可优化为位运算 - JVM会自动优化 `31 * i` 为 `(i << 5) - i` ### Q5: 浅拷贝和深拷贝的区别? **答:** - **浅拷贝**:只复制对象本身,引用类型共享同一对象 - **深拷贝**:递归复制所有引用对象,完全独立 ### Q6: 为什么不推荐使用clone()? **答:** - Cloneable接口设计有缺陷 - 默认浅拷贝容易出错 - 不调用构造函数,可能破坏不变性 - 推荐使用拷贝构造函数或静态工厂方法 ### Q7: wait()和notify()为什么在Object类中? **答:** - 这些方法操作对象的监视器锁 - 每个对象都有监视器锁 - 所以定义在Object类中,所有对象都能使用 ### Q8: wait()和sleep()的区别? **答:** | 特性 | wait() | sleep() | | --- | --- | --- | | 所属类 | Object | Thread | | 释放锁 | 是 | 否 | | 唤醒方式 | notify/超时 | 超时 | | 使用场景 | 线程通信 | 暂停执行 | --- ## 九、最佳实践 ### ✅ 推荐做法 ```java // 1. 使用IDE或Lombok生成equals/hashCode @EqualsAndHashCode public class User { private Long id; private String name; } // 2. 使用Objects工具类 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return Objects.equals(id, user.id); } @Override public int hashCode() { return Objects.hash(id, name); } // 3. 使用拷贝构造函数代替clone public User(User other) { this.id = other.id; this.name = other.name; } // 4. 使用try-with-resources代替finalize try (Resource r = new Resource()) { // 使用资源 } ``` ### ❌ 避免做法 ```java // 1. 只重写equals不重写hashCode @Override public boolean equals(Object o) { ... } // 缺少hashCode重写 // 2. equals参数类型写错 public boolean equals(User other) { ... } // 应该是Object // 3. 使用clone() User u2 = u1.clone(); // 不推荐 // 4. 使用finalize() protected void finalize() { ... } // 已废弃 ``` --- ## 相关链接 - [[01-继承与多态|01-继承与多态]] - 方法重写的规则 - [[02-HashMap源码解析|HashMap原理]] - equals和hashCode的应用 - [[01-线程基础与生命周期|线程通信]] - wait/notify详解 --- ## 记忆口诀 ``` equals五约定,自反对称传递性 一致非空要牢记,hashCode必同行 重写equals必重写hash,HashMap才能正常跑 clone有坑不推荐,拷贝构造是首选 wait释放锁等待,notify唤醒要在synchronized finalize已废弃,try-with-resources来替代 ``` --- ⬅️ [[05-动态代理|动态代理]] 🏠 [[00-Java|00-Java]] ➡️ [[02-instanceof|instanceof]]