×

上篇聊了 equalshashCode,这两个是 Object 类中被重写得最多的方法。这篇我们把剩下的方法扫一遍——有的天天用,有的已经过时了,还有的根本不需要你自己去调用。

一、toString()——打印对象时自动调用的方法

你写 System.out.println(obj) 的时候,实际上就是在调用 obj.toString()

public class Student {
    String name = "张三";
    int age = 20;
}

Student s = new Student();
System.out.println(s);
// 输出:Student@1a2b3c4d  ← 类名@十六进制哈希值(Object 默认实现)
// 这玩意儿谁看得懂?

默认的 toString()类名@哈希值的十六进制——说白了就是对象的内部编号。调试的时候默认输出什么也看不出来,所以几乎所有自定义类都应该重写 toString()

@Override
public String toString() {
    return "Student{name='" + name + "', age=" + age + "}";
}

// 再用 System.out.println(s) 输出:
// Student{name='张三', age=20}
// 这才是人能看的格式

IDE 也一样能一键生成 toString()。建议每个实体类(POJO、DTO、VO)都加上——写日志、调试、排查问题的时候,每一条日志都能看清对象的内容,省多少时间

二、getClass()——运行时的类型信息

getClass() 返回的是这个对象的 Class 对象,里面包含了类的完整信息——类名、方法、字段、注解等等。它是 Java 反射的入口。

Student s = new Student();
Class<?> clazz = s.getClass();

System.out.println(clazz.getName());          // com.example.Student
System.out.println(clazz.getSimpleName());    // Student
System.out.println(clazz.getSuperclass());    // class java.lang.Object

上篇写 equals 的时候我们用 getClass() != obj.getClass() 做类型检查,这就是 getClass() 最典型的用法——判断两个对象是不是同一个类。

这里补充一个知识点:instanceofgetClass() 的区别。

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

Animal dog = new Dog();

System.out.println(dog instanceof Dog);      // true
System.out.println(dog instanceof Animal);   // true(因为 Dog 继承 Animal)
System.out.println(dog.getClass() == Dog.class);   // true
System.out.println(dog.getClass() == Animal.class); // false(运行时类型是 Dog)

instanceof 检查的是「是不是这个类型或子类型」;getClass() == xxx.class 检查的是「是不是精确地这个类型」。面试经常考这个区别。

三、clone()——对象的拷贝

你想复制一个对象:

Student s1 = new Student("张三", 20);
Student s2 = s1;  // 这根本不是复制——s2 和 s1 指向同一个对象!
s2.setName("李四");
System.out.println(s1.getName()); // 李四 —— s1 也被改了

要真正复制一个对象,Object 提供了 clone() 方法。但它有几个限制:

  • 你的类必须实现 Cloneable 接口(标记接口,里面没有方法)
  • clone()protected 的,你需要在子类中重写为 public
  • 默认的 clone()浅拷贝——只能复制基本类型,引用类型只复制引用
public class Student implements Cloneable {
    private String name;
    private int age;
    private Address address;  // 引用类型

    public Student(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    @Override
    public Student clone() {
        try {
            return (Student) super.clone();  // 浅拷贝
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

浅拷贝的问题:如果 address 是个引用类型,两个 Student 的 address 指向同一个地址对象——改一个另一个也跟着变。要「深拷贝」需要手动复制所有引用字段:

@Override
public Student clone() {
    try {
        Student cloned = (Student) super.clone();
        cloned.address = this.address.clone();  // 引用字段也复制
        return cloned;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

说实话,Java 的 clone() 机制设计得有点别扭——又是 Cloneable 接口(标记而已),又是 protected 方法,还要抛异常……

现在更推荐用拷贝构造器或者工厂方法来代替 clone()

// 拷贝构造器
public Student(Student other) {
    this.name = other.name;
    this.age = other.age;
    this.address = new Address(other.address); // 深拷贝
}

// 使用
Student s2 = new Student(s1);

清爽,可控,不依赖 Cloneable 这种怪胎。

四、finalize()——别碰这个方法

finalize() 是 Object 类中的一个方法,GC 在回收对象之前会调用它一次。听起来好像很美好——可以在对象被回收前释放资源?

但实际上:

  • 什么时候被调用不确定——GC 的时机不归你控制
  • 可能根本不被调用——程序退出时如果 GC 没跑,finalize() 就永远不会执行
  • 性能差——会显著拖慢 GC 速度

Java 9 就已经标记为 @Deprecated(since="9") 了。现在要释放资源就用 try-with-resources 或者 AutoCloseable 接口。

// ✅ 正确做法
try (FileInputStream fis = new FileInputStream("test.txt")) {
    // 使用 fis...
} // 自动关闭,不需要 finalize

// ❌ 别用 finalize

五、notify() / notifyAll() / wait()

这三个方法是 多线程通信 用的,不涉及多线程的时候基本用不到。等我们讲到多线程那部分的时候再展开。先知道有这么几个方法就行了。

六、三兄弟总结

方法默认行为建议
toString()类名@哈希值十六进制强烈建议重写,IDE 生成即可
getClass()返回运行时的 Class<?> 对象直接使用,别重写(final 方法重写不了)
clone()浅拷贝,需实现 Cloneable优先用拷贝构造器替代
finalize()GC 前调用一次别用,Java 9 已弃用

七、Object 方法之外的补充:Objects 工具类

跟 Object 类相似但不相同的是 java.util.Objects——它是一个工具类(全是静态方法),用来处理对象相关的操作。前面我们已经用过 Objects.equals()Objects.hash(),这里再补充几个好用的:

String s = null;

// Objects.toString() —— 对象转字符串,null 安全
System.out.println(Objects.toString(s));         // "null"
System.out.println(Objects.toString(s, "默认值")); // "默认值"

// Objects.requireNonNull() —— 快速判空,返回非空值或抛异常
public Student(String name) {
    this.name = Objects.requireNonNull(name, "姓名不能为空");
}

// Objects.isNull() / nonNull() —— 判空
if (Objects.isNull(s)) {
    System.out.println("s 是 null");
}

这些工具方法让代码更简洁,而且避免了空指针——平时写代码多留意一下 java.util 包下的工具类,能省不少事。

八、小结

  • toString() 推荐每个实体类都重写,提升开发效率
  • getClass() 获取运行时 Class 信息,反射的入口
  • clone() 默认浅拷贝,有坑;更推荐拷贝构造器
  • finalize() 已过时,别碰
  • Objects 工具类提供了一大堆 null 安全的操作方法,记得用

Object 类两篇讲完。下一篇我们聊最后一个子分类——枚举与包装类enum 关键字、Integer 缓存池、自动装箱/拆箱……全是面试常客。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

作者

2248768396@qq.com

相关文章