[Java 基础]Object 类
java.lang.Object 是 Java 所有类的直接或间接父类,Java 中每个类都默认继承 Object 类(即使你没写 extends Object)。
Object 中的常用方法:
方法名 | 功能简介 |
---|---|
toString() | 返回对象的字符串表示 |
equals(Object) | 判断两个对象是否“逻辑相等” |
hashCode() | 返回哈希值,常用于集合类 |
getClass() | 返回此对象的运行时类 |
clone() | 克隆对象(实现 Cloneable 时有效) |
finalize() | 对象被 GC 回收前调用(不推荐使用) |
toString
我们使用 System.out.println() 打印对象,调用的就是对象的 toString 方法,将对象的 toString 方法返回的结果打印出来。
public class Person {private String name;public Person(String name) {this.name = name;}// 默认输出是:类名@哈希值// 重写 toString() 提供可读性@Overridepublic String toString() {return "Person[name=" + name + "]";}
}
equals
equals 方法是用来比较两个对象是否逻辑相等,Object 类的 equals 方法直接比较的是两个对象地址。我们一般需要修改 equals 方法,比如:两个公民对象,如果身份证 id 是一样的,从我们的现实生活的经验中判断,这两个人肯定是同一个人,即他们是相等的。
对于基本类型,我们可以直接使用 == 比较他们是不是相等的,如果他们的值是相等的,==
返回 true,否则返回 false,对于对象,我们需要使用 equals 方法判断两个对象是不是相等的。
重写 equals 方法的要点,要遵循如下的特性:
- 自反性:x.equals(x) 必须返回 true
- 对称性:x.equals(y) 和 y.equals(x) 必须返回相同结果
- 传递性:如果 x.equals(y)且y.equals(z),则 x.equals(z) 必须为 true
- 一致性:多次调用 x.equals(y) 应该返回相同结果
- 非空性:x.equals(null) 必须返回 false
重写 equals() 方法后,必须重写 hashCode() 方法,以维护 Java 对象的通用约定,并确保基于哈希的集合(如 HashSet、HashMap 和 HashTable)能够正常工作。Java 的 Object 类对 equals() 和 hashCode() 方法之间有明确的约定:
- 如果两个对象根据 equals() 方法相等,那么它们的 hashCode() 方法必须返回相同的值
- 如果两个对象的 hashCode() 方法返回相同的值,它们根据 equals() 方法不一定相等(哈希冲突)
- 哈希表的功能: 哈希表使用 hashCode() 方法来确定对象在表中的位置(桶)。如果 equals() 被重写但 hashCode() 没有,则两个逻辑上相等的对象(equals() 返回 true)可能具有不同的哈希码。这会导致以下问题:1. 无法找到对象: 当你试图在 HashSet 中查找一个对象时,HashSet 会使用对象的 hashCode() 来定位它所在的桶。如果 hashCode() 与原始对象的 hashCode() 不同(即使它们逻辑上相等),HashSet 将无法找到该对象,即使它已经存在于集合中。2. 重复元素: HashSet 旨在防止重复元素。如果两个逻辑上相等的 对象具有不同的哈希码,HashSet 会将它们视为不同的对象,从而允许将重复元素添加到集合中。3. HashMap 的不一致性: HashMap 也依赖于 hashCode() 和 equals() 来正确存储和检索键值对。如果键的 hashCode() 和 equals() 不一致,HashMap 的行为将变得不可预测。
下面是一个重写 equals 方法的例子:
public class Person {private String name;public Person(String name) {this.name = name;}// 重写equals方法@Overridepublic boolean equals(Object obj) {// 1. 检查是否是同一个对象if (this == obj) {return true;}// 2. 检查是否为null或类型不同if (obj == null || getClass() != obj.getClass()) {return false;}// 3. 类型转换Person person = (Person) obj;// 4. 比较关键字段return name != null ? name.equals(person.name) : person.name == null;}// 重写equals时也应该重写hashCode@Overridepublic int hashCode() {return name != null ? name.hashCode() : 0;}
}
Person p1 = new Person("Tom");
Person p2 = new Person("Tom");// == 比较的是地址
System.out.println(p1 == p2); // false// 默认 equals 也是比较地址
// 可通过重写实现“内容相等”
System.out.println(p1.equals(p2)); // true(需重写后)
hashCode
hashCode 方法返回对象的 hashCode 编码,在上面的 equals 方法中我们讲过,要确保两个对象如果 equals 方法判断他们是相等的,那么他们的 hashCode 方法返回的哈希码也必须是相等的。
// 在集合类(如 HashMap、HashSet)中要重写 hashCode 与 equals 保持一致性
@Override
public int hashCode() {return name.hashCode(); // 简单写法
}
getClass
getClass 方法返回的是此 Object 的运行时类。
Person p = new Person("Jerry");
System.out.println(p.getClass().getName()); // 输出类的全名
clone
按照惯例,如果一个类希望能够被克隆,它应该重写Object.clone()
方法,并将其访问修饰符改为public
,通常返回它自己的类型(需要进行类型转换)。在重写的方法中,通常会先调用super.clone()
来获得一个基本的副本。
Object.clone()
的默认行为是执行浅拷贝。 这里涉及到连个概念,浅拷贝(Shallow Copy)与深拷贝(Deep Copy)。
浅拷贝:
- 对于原始数据类型(
int
,double
,boolean
等),直接复制值。 - 对于引用类型(对象引用),复制的是引用本身,而不是被引用的对象。这意味着原始对象和克隆对象中的引用字段将指向内存中的同一个对象。
- 后果: 如果原始对象或克隆对象修改了共享引用对象的状态,这种改变会同时反映在另一个对象中。在大多数情况下,这可能不是期望的行为,因为它打破了克隆对象与原始对象之间的独立性。
深拷贝:
- 为了实现深拷贝,你需要在重写的
clone()
方法中,对所有引用类型的字段(如果这些字段也是可克隆的)手动调用它们的clone()
方法,从而创建这些引用对象的独立副本。 - 复杂性: 如果对象包含多层嵌套的引用类型,实现深拷贝可能会变得非常复杂,因为你需要递归地克隆所有被引用的对象。
class Address implements Cloneable {String city;String street;public Address(String city, String street) {this.city = city;this.street = street;}@Overridepublic Object clone() throws CloneNotSupportedException {return super.clone(); // Address类的浅拷贝}@Overridepublic String toString() {return "Address [city=" + city + ", street=" + street + "]";}
}class Student implements Cloneable {String name;int age;Address address; // 引用类型public Student(String name, int age, Address address) {this.name = name;this.age = age;this.address = address;}// 浅拷贝的实现@Overridepublic Object clone() throws CloneNotSupportedException {return super.clone(); // 默认是浅拷贝}// 深拷贝的实现(需要手动处理引用类型字段)public Object deepClone() throws CloneNotSupportedException {Student clonedStudent = (Student) super.clone();// 对引用类型字段进行深拷贝clonedStudent.address = (Address) address.clone();return clonedStudent;}@Overridepublic String toString() {return "Student [name=" + name + ", age=" + age + ", address=" + address + "]";}
}public class CloneDemo {public static void main(String[] args) throws CloneNotSupportedException {Address originalAddress = new Address("New York", "Broadway");Student originalStudent = new Student("Alice", 20, originalAddress);// 浅拷贝示例Student shallowClonedStudent = (Student) originalStudent.clone();System.out.println("Original Student (Shallow): " + originalStudent);System.out.println("Shallow Cloned Student: " + shallowClonedStudent);// 修改浅拷贝后的地址,观察原始对象的变化shallowClonedStudent.address.city = "Los Angeles";System.out.println("After changing shallowClonedStudent's city:");System.out.println("Original Student (Shallow): " + originalStudent); // Original也会改变System.out.println("Shallow Cloned Student: " + shallowClonedStudent);System.out.println("---");// 深拷贝示例Address originalAddress2 = new Address("London", "Oxford Street");Student originalStudent2 = new Student("Bob", 22, originalAddress2);Student deepClonedStudent = (Student) originalStudent2.deepClone();System.out.println("Original Student (Deep): " + originalStudent2);System.out.println("Deep Cloned Student: " + deepClonedStudent);// 修改深拷贝后的地址,观察原始对象的变化deepClonedStudent.address.city = "Paris";System.out.println("After changing deepClonedStudent's city:");System.out.println("Original Student (Deep): " + originalStudent2); // Original不会改变System.out.println("Deep Cloned Student: " + deepClonedStudent);}
}
尽管clone()
方法提供了对象复制的功能,但它在实际开发中很少被推荐使用,并且存在一些缺点:
- 破坏封装性:
clone()
方法需要访问对象的内部状态,这可能违反封装原则。 **Cloneable**
接口的不足:Cloneable
是一个标记接口,它没有定义clone()
方法。这意味着编译器无法检查你是否正确地重写了clone()
方法,或者是否处理了CloneNotSupportedException
。- 不调用构造函数:
clone()
方法通过直接复制内存来创建对象,不调用任何构造函数。这可能导致一些需要构造函数来正确初始化的逻辑被跳过。 - 不可变对象问题: 对于包含
final
字段的对象,clone()
方法无法修改这些final
字段,因为它们只能在构造函数中初始化。 - 链式克隆的复杂性: 当一个对象包含其他对象的引用时,为了实现深拷贝,你必须确保所有被引用的类也实现了
Cloneable
并正确地重写了clone()
方法。这会导致一个“病毒式”的Cloneable
实现。 - 异常处理:
Object.clone()
会抛出受检查异常CloneNotSupportedException
,即使你的类实现了Cloneable
,也需要在签名中声明或捕获它。
鉴于这些缺点,通常有更好的替代方案来实现对象复制:
- 拷贝构造函数(Copy Constructor):创建一个接收同类型对象作为参数的构造函数,并在其中手动复制字段。 这是最常见和推荐的方式,它提供了更好的控制,允许你选择是进行浅拷贝还是深拷贝,并且可以处理
final
字段。 - 拷贝工厂方法(Copy Factory Method): 提供一个静态工厂方法来创建对象的副本。
- 序列化/反序列化(Serialization/Deserialization): 通过将对象序列化到字节流(例如内存中的
ByteArrayOutputStream
),然后从该字节流反序列化回来,可以实现深拷贝。 优点是简单,自动处理深拷贝。 缺点是性能开销较大,且所有涉及的类都必须实现Serializable
接口。 - 第三方库: 许多现代框架和库提供了更强大、更灵活的对象映射和复制工具(如Apache Commons Lang的
SerializationUtils.clone()
)。