Java基础面试专栏(十六):Object类的核心方法解析

2 阅读16分钟

上一篇专栏我们详解了ArrayList和LinkedList的底层差异、性能对比及适用场景,明确了两者在不同开发场景下的选择逻辑。今天我们聚焦Java基础中最核心的类——Object类,它是所有Java类的根父类,所有对象(包括数组)都隐式继承自Object类,其定义的方法贯穿整个Java开发,也是面试中高频考察的基础考点。很多开发者对Object类的方法仅停留在“知道名称”的层面,却不清楚其底层逻辑、使用场景及易错点,今天我们就从面试答题角度,拆透Object类的所有核心方法,搭配全新实战代码,帮你快速掌握答题思路,避开高频陷阱。

先给大家一个面试万能总结(一句话直达核心,适合开场快速应答):Java的Object类包含以下方法:getClass(), hashCode(), equals(), toString(), clone(), finalize(), notify(), notifyAll(), wait()(含三个重载版本)。这些方法定义了所有Java对象的通用行为,覆盖对象比较、哈希存储、线程通信、反射等核心场景。

一、Object类的核心定位(面试开篇必答)

在Java中,Object类是整个类继承体系的顶层父类,无论是我们自定义的类、Java内置的类(如String、ArrayList),还是数组,都默认继承自Object类(无需显式写extends Object)。Object类的设计初衷,是为所有Java对象提供一套通用的方法,统一对象的基本行为,避免重复开发。

Object类共定义了11个方法(不同JDK版本略有差异),其中大部分方法可被子类重写,以适配具体业务场景;少数方法被final修饰(如getClass()),无法被重写,确保其核心逻辑的一致性。接下来我们逐一拆解每个方法的作用、使用场景及实战细节。

二、Object类核心方法详解(结合实战,面试重点)

我们先整理出所有核心方法的基础信息,形成清晰对比表,方便记忆答题,后续再针对重点方法展开详解并搭配实战代码:

方法名称方法作用常见使用场景是否可重写
toString()返回对象的字符串表示形式,默认返回“类名@哈希码十六进制”调试输出、日志打印、对象信息展示
equals(Object obj)判断两个对象是否“逻辑相等”,默认比较对象内存地址(==)自定义对象内容比较、集合中元素去重
hashCode()返回对象的哈希码(int类型),用于哈希表的存储和查找HashMap、HashSet等集合类的底层存储
getClass()返回对象的运行时类(Class对象),获取类的元数据反射操作、类型检查、动态获取类信息否(final修饰)
clone()创建并返回对象的副本,默认实现为浅拷贝对象复制、避免原对象被修改影响副本是(需实现Cloneable接口)
finalize()对象被垃圾回收器回收前调用,用于资源清理(JDK9已标记为废弃)早期用于关闭文件、释放数据库连接(现不推荐使用)
wait()wait(long timeout)wait(long timeout, int nanos)让当前线程进入等待状态,释放对象锁,需在同步块中使用线程间通信(如生产者-消费者模型)、线程等待唤醒
notify()唤醒等待该对象锁的一个线程(随机唤醒),需在同步块中使用线程间通信、唤醒等待线程
notifyAll()唤醒等待该对象锁的所有线程,需在同步块中使用线程间通信、批量唤醒等待线程
registerNatives()本地方法,用于注册Object类的本地方法实现,由JVM内部调用JVM底层实现,开发者无需关注和使用否(private修饰)

1. 最常用方法:toString()、equals()、hashCode()(面试高频)

这三个方法是日常开发中使用频率最高、面试考察最频繁的组合,尤其equals()与hashCode()的同步重写,是面试中的核心易错点,我们结合实战代码详细解析。

(1)toString()方法

Object类中toString()的默认实现为:public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); },返回的是“类的全限定名@哈希码十六进制”,这种格式对开发者不友好,因此子类通常会重写该方法,返回对象的核心属性信息,方便调试和日志输出。

(2)equals()方法

Object类中equals()的默认实现为:public boolean equals(Object obj) { return (this == obj); },本质是比较两个对象的内存地址(即判断是否为同一个对象)。但在实际开发中,我们通常需要判断“两个对象的内容是否相等”(如两个用户对象,用户名和密码相同即认为相等),此时就需要重写equals()方法。

(3)hashCode()方法

hashCode()方法返回对象的哈希码,是一个int类型的值,其核心作用是为哈希表(如HashMap、HashSet)提供支持——哈希表通过哈希码快速定位对象的存储位置,提升查找效率。

核心规则(面试必记):equals()方法返回true的两个对象,其hashCode()必须相等;equals()方法返回false的两个对象,其hashCode()可以相等(哈希碰撞),但不相等更能提升哈希表效率。因此,重写equals()方法时,必须同步重写hashCode()方法,否则会导致哈希表使用异常(如HashMap中相同内容的对象无法去重)。

实战代码示例(重写toString()、equals()、hashCode())

场景:自定义User类,重写三个方法,实现“用户名和密码相同即认为对象相等”,同时保证哈希表使用正常。

import java.util.Objects;

// 自定义User类,重写Object类的三个核心方法
class User {
    private String username;
    private String password;

    // 构造方法
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // 重写toString(),返回对象核心属性
    @Override
    public String toString() {
        return "User{username='" + username + "', password='" + password + "'}";
    }

    // 重写equals(),判断两个User对象内容是否相等
    @Override
    public boolean equals(Object o) {
        // 1. 自身判断(同一个对象,直接返回true)
        if (this == o) return true;
        // 2. 空判断和类型判断(类型不同,直接返回false)
        if (o == null || getClass() != o.getClass()) return false;
        // 3. 强制转换,比较核心属性
        User user = (User) o;
        // 用Objects.equals()避免空指针异常(推荐使用)
        return Objects.equals(username, user.username) && Objects.equals(password, user.password);
    }

    // 重写hashCode(),与equals()同步
    @Override
    public int hashCode() {
        // 用Objects.hash()生成哈希码,与equals()的比较属性一致
        return Objects.hash(username, password);
    }

    // getter/setter方法
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

// 测试类,验证三个方法的使用
public class ObjectMethodTest1 {
    public static void main(String[] args) {
        User user1 = new User("zhangsan", "123456");
        User user2 = new User("zhangsan", "123456");
        User user3 = new User("lisi", "654321");

        // 测试toString()
        System.out.println("user1的toString()结果:" + user1);
        System.out.println("user2的toString()结果:" + user2);

        // 测试equals()
        System.out.println("user1.equals(user2):" + user1.equals(user2)); // true(内容相同)
        System.out.println("user1.equals(user3):" + user1.equals(user3)); // false(内容不同)
        System.out.println("user1 == user2:" + (user1 == user2)); // false(内存地址不同)

        // 测试hashCode()
        System.out.println("user1的hashCode:" + user1.hashCode());
        System.out.println("user2的hashCode:" + user2.hashCode()); // 与user1相等(equals为true)
        System.out.println("user3的hashCode:" + user3.hashCode()); // 与user1不相等(equals为false)

        // 测试哈希表使用(HashMap)
        java.util.HashMap<User, String> map = new java.util.HashMap<>();
        map.put(user1, "张三");
        map.put(user2, "张三"); // 由于equals和hashCode同步,会覆盖原有值(去重)
        map.put(user3, "李四");
        System.out.println("HashMap中的元素个数:" + map.size()); // 2个(user1和user2被识别为同一个对象)
    }
}

运行结果说明:重写toString()后,对象的打印信息更直观;重写equals()后,可实现“内容相等即对象相等”;同步重写hashCode()后,HashMap能正确识别相同内容的对象,实现去重,避免逻辑错误。

2. 反射相关:getClass()方法

getClass()方法被final修饰,无法被重写,其作用是返回对象的运行时类(Class对象),通过Class对象可以获取类的元数据(如类名、方法、属性等),是Java反射机制的核心入口。

注意:getClass()方法返回的是“运行时类”,而非编译时类。例如,子类对象调用getClass(),返回的是子类的Class对象,而非父类的,这与instanceof关键字(判断对象是否是某个类的实例)有所区别。

实战代码示例(getClass()与反射应用)

场景:通过getClass()获取对象的运行时类,动态获取类的名称、方法和属性,体现反射的核心用法。

import java.lang.reflect.Field;
import java.lang.reflect.Method;

// 父类
class Animal {
    private String name;

    public void eat() {
        System.out.println("动物进食");
    }
}

// 子类
class Dog extends Animal {
    private int age;

    public void bark() {
        System.out.println("小狗汪汪叫");
    }
}

// 测试getClass()与反射
public class ObjectMethodTest2 {
    public static void main(String[] args) {
        Animal animal = new Dog(); // 父类引用指向子类对象

        // 测试getClass():返回运行时类(Dog)
        Class<?> clazz = animal.getClass();
        System.out.println("对象的运行时类名:" + clazz.getName()); // 输出Dog的全限定名
        System.out.println("对象的简单类名:" + clazz.getSimpleName()); // 输出Dog

        // 反射获取类的属性
        System.out.println("\n类的所有属性:");
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            System.out.println(field.getName() + "(访问修饰符:" + field.getModifiers() + ")");
        }

        // 反射获取类的方法
        System.out.println("\n类的所有方法:");
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            System.out.println(method.getName() + "(参数个数:" + method.getParameterCount() + ")");
        }

        // 对比getClass()与instanceof
        System.out.println("\nanimal instanceof Animal:" + (animal instanceof Animal)); // true
        System.out.println("animal instanceof Dog:" + (animal instanceof Dog)); // true
        System.out.println("clazz == Animal.class:" + (clazz == Animal.class)); // false
        System.out.println("clazz == Dog.class:" + (clazz == Dog.class)); // true
    }
}

运行结果说明:父类引用指向子类对象时,getClass()返回子类的Class对象,体现“运行时类型”;通过反射可以动态获取类的属性和方法,这也是getClass()方法的核心应用场景。

3. 对象复制:clone()方法

clone()方法的作用是创建并返回当前对象的副本,Object类中clone()的默认实现为“浅拷贝”——即复制对象本身,但对象中的引用类型字段,仅复制引用地址,不复制引用指向的对象(原对象和副本的引用字段指向同一个对象)。

注意事项(面试必记):

① 子类要使用clone()方法,必须实现Cloneable接口(标记接口,无任何抽象方法),否则会抛出CloneNotSupportedException异常;

② 默认clone()是浅拷贝,若需实现“深拷贝”(复制对象及所有引用类型字段指向的对象),需手动重写clone()方法,对引用类型字段也进行复制。

实战代码示例(浅拷贝与深拷贝)

场景:自定义Person类,包含引用类型字段(Address),分别实现浅拷贝和深拷贝,对比两者的差异。

// 引用类型:地址类
class Address {
    private String city;
    private String street;

    public Address(String city, String street) {
        this.city = city;
        this.street = street;
    }

    // 重写toString(),方便查看
    @Override
    public String toString() {
        return "Address{city='" + city + "', street='" + street + "'}";
    }

    // getter/setter方法
    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }
}

// 实现浅拷贝的Person类
class Person implements Cloneable {
    private String name;
    private int age;
    private Address address; // 引用类型字段

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

    // 重写clone(),默认浅拷贝
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone(); // 调用Object类的clone(),实现浅拷贝
    }

    // 重写toString()
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + ", address=" + address + "}";
    }

    // getter/setter方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }
}

// 实现深拷贝的Person类(继承自浅拷贝类,重写clone())
class DeepClonePerson extends Person {
    public DeepClonePerson(String name, int age, Address address) {
        super(name, age, address);
    }

    // 重写clone(),实现深拷贝
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // 1. 先拷贝当前对象(浅拷贝)
        DeepClonePerson clone = (DeepClonePerson) super.clone();
        // 2. 拷贝引用类型字段(Address),实现深拷贝
        Address originalAddress = super.getAddress();
        Address cloneAddress = new Address(originalAddress.getCity(), originalAddress.getStreet());
        clone.setAddress(cloneAddress);
        return clone;
    }
}

// 测试浅拷贝与深拷贝
public class ObjectMethodTest3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        // 1. 测试浅拷贝
        Address address = new Address("北京", "朝阳路");
        Person person1 = new Person("张三", 20, address);
        Person person2 = (Person) person1.clone();

        System.out.println("浅拷贝测试:");
        System.out.println("原对象person1:" + person1);
        System.out.println("副本对象person2:" + person2);

        // 修改原对象的引用字段(Address)
        address.setCity("上海");
        System.out.println("修改原对象的Address后:");
        System.out.println("原对象person1:" + person1);
        System.out.println("副本对象person2:" + person2); // 副本的Address也被修改(浅拷贝特性)

        // 2. 测试深拷贝
        Address address2 = new Address("广州", "天河路");
        DeepClonePerson person3 = new DeepClonePerson("李四", 22, address2);
        DeepClonePerson person4 = (DeepClonePerson) person3.clone();

        System.out.println("\n深拷贝测试:");
        System.out.println("原对象person3:" + person3);
        System.out.println("副本对象person4:" + person4);

        // 修改原对象的引用字段(Address)
        address2.setCity("深圳");
        System.out.println("修改原对象的Address后:");
        System.out.println("原对象person3:" + person3);
        System.out.println("副本对象person4:" + person4); // 副本的Address不受影响(深拷贝特性)
    }
}

运行结果说明:浅拷贝仅复制对象本身,引用类型字段仍指向同一个对象,修改原对象的引用字段会影响副本;深拷贝会复制对象及所有引用类型字段指向的对象,原对象和副本相互独立,修改其中一个不会影响另一个。

4. 线程通信:wait()、notify()、notifyAll()方法

这三个方法是Java线程间通信的核心方法,均需在synchronized同步块(或同步方法)中使用,否则会抛出IllegalMonitorStateException异常。其核心作用是实现线程的等待与唤醒,常用于生产者-消费者等线程协作场景。

核心区别:

① wait():让当前线程释放对象锁,进入等待状态,直到被notify()/notifyAll()唤醒,或等待超时;

② notify():唤醒等待该对象锁的一个线程(随机唤醒,无法指定);

③ notifyAll():唤醒等待该对象锁的所有线程,线程唤醒后会竞争对象锁,获得锁后继续执行。

实战代码示例(线程通信:生产者-消费者模型)

场景:实现简单的生产者-消费者模型,生产者生产商品,消费者消费商品,通过wait()和notifyAll()实现线程间协作,避免线程安全问题。

// 商品仓库类(共享资源)
class Warehouse {
    private int count; // 商品数量
    private static final int MAX_CAPACITY = 5; // 仓库最大容量

    // 生产商品(生产者调用)
    public synchronized void produce() throws InterruptedException {
        // 仓库满了,生产者等待
        while (count >= MAX_CAPACITY) {
            System.out.println("仓库已满,生产者等待...");
            wait(); // 释放对象锁,进入等待状态
        }
        // 生产商品
        count++;
        System.out.println("生产者生产1件商品,当前库存:" + count);
        notifyAll(); // 唤醒等待的消费者线程
    }

    // 消费商品(消费者调用)
    public synchronized void consume() throws InterruptedException {
        // 仓库空了,消费者等待
        while (count <= 0) {
            System.out.println("仓库为空,消费者等待...");
            wait(); // 释放对象锁,进入等待状态
        }
        // 消费商品
        count--;
        System.out.println("消费者消费1件商品,当前库存:" + count);
        notifyAll(); // 唤醒等待的生产者线程
    }
}

// 生产者线程
class Producer implements Runnable {
    private Warehouse warehouse;

    public Producer(Warehouse warehouse) {
        this.warehouse = warehouse;
    }

    @Override
    public void run() {
        try {
            // 循环生产商品
            for (int i = 0; i < 10; i++) {
                warehouse.produce();
                Thread.sleep(500); // 模拟生产耗时
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 消费者线程
class Consumer implements Runnable {
    private Warehouse warehouse;

    public Consumer(Warehouse warehouse) {
        this.warehouse = warehouse;
    }

    @Override
    public void run() {
        try {
            // 循环消费商品
            for (int i = 0; i < 10; i++) {
                warehouse.consume();
                Thread.sleep(800); // 模拟消费耗时
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 测试线程通信
public class ObjectMethodTest4 {
    public static void main(String[] args) {
        Warehouse warehouse = new Warehouse();
        // 创建生产者和消费者线程
        Thread producerThread = new Thread(new Producer(warehouse));
        Thread consumerThread = new Thread(new Consumer(warehouse));

        // 启动线程
        producerThread.start();
        consumerThread.start();
    }
}

运行结果说明:生产者线程生产商品,当仓库满时,调用wait()进入等待状态;消费者线程消费商品,当仓库空时,调用wait()进入等待状态;一方操作完成后,调用notifyAll()唤醒另一方,实现线程间的协调协作,避免了线程安全问题。

5. 废弃方法:finalize()方法

finalize()方法的作用是在对象被垃圾回收器(GC)回收前调用,用于释放对象占用的资源(如文件流、数据库连接等)。但该方法在JDK9中已被标记为废弃(@Deprecated),不推荐使用。

废弃原因:

① 执行时机不确定:GC的执行时机由JVM决定,finalize()的调用时间无法预测,可能导致资源无法及时释放;

② 可能导致内存泄漏:若finalize()方法中出现异常,会导致对象无法被正常回收,造成内存泄漏;

③ 替代方案:现在通常使用try-with-resources语句或手动关闭资源,确保资源及时、可靠地释放。

三、高频面试陷阱(必记,避开踩坑)

Object类的面试易错点,主要集中在方法的使用规范和底层逻辑,记住以下4点,轻松避开所有陷阱:

陷阱1:重写equals()但不重写hashCode()

错误原因:违背“equals()为true的两个对象,hashCode()必须相等”的规则,导致哈希表(如HashMap、HashSet)无法正确识别相同内容的对象,出现去重失败、查找异常等问题。

陷阱2:认为clone()方法可以直接使用,无需实现Cloneable接口

错误原因:Cloneable接口是标记接口,子类必须实现该接口,才能调用clone()方法,否则会抛出CloneNotSupportedException异常;且默认clone()是浅拷贝,并非深拷贝。

陷阱3:在非同步块中调用wait()、notify()、notifyAll()方法

错误原因:这三个方法的调用前提是当前线程持有该对象的锁(synchronized锁定),否则会抛出IllegalMonitorStateException异常,这是线程通信的核心规范。

陷阱4:依赖finalize()方法释放资源

错误原因:finalize()方法执行时机不确定,无法保证资源及时释放,且可能导致内存泄漏,实际开发中应使用try-with-resources或手动关闭资源。

四、常见面试场景与答题技巧

结合日常开发和面试高频场景,总结3个核心答题要点,帮你快速应对面试提问,避免踩坑:

  1. 核心定位答题逻辑:先说明Object类是所有Java类的根父类,所有对象都隐式继承自它,再列出所有核心方法,重点突出常用方法(toString()、equals()、hashCode())。

  2. 方法详解答题逻辑:讲解每个方法时,先说明作用,再补充使用场景和注意事项(如equals()与hashCode()的同步重写、clone()的浅拷贝特性、线程方法的同步要求),结合简单实例说明。

  3. 易错点答题逻辑:重点强调equals()与hashCode()的重写规范、clone()的使用前提、线程方法的调用场景,避开常见陷阱,体现专业性。

五、面试总结

  1. 核心梳理:Object类是Java类继承体系的顶层父类,定义了11个核心方法,覆盖对象比较、哈希存储、反射、线程通信、对象复制等通用场景。其中toString()、equals()、hashCode()是日常开发和面试的重点,wait()、notify()、notifyAll()是线程通信的核心,clone()需注意浅拷贝与深拷贝的区别,finalize()已废弃,不推荐使用。

  2. 高频面试题(提前准备,直接应答):

① Object类有哪些核心方法?(列出核心方法,重点说明常用的5-6个)

② 为什么重写equals()方法必须同步重写hashCode()方法?(结合哈希表的工作原理,说明两者的关联规则)

③ clone()方法实现拷贝的注意事项有哪些?(实现Cloneable接口、浅拷贝与深拷贝的区别)

④ wait()和notify()方法的使用前提是什么?两者的作用是什么?(需在同步块中使用,实现线程等待与唤醒)

⑤ finalize()方法的作用是什么?为什么被废弃?(资源清理,执行时机不确定、可能导致内存泄漏)