2 Object —— Java 类体系的根节点

12 阅读9分钟

Object —— Java 类体系的根节点

适用版本: JDK 8 难度等级: 基础 核心方法: getClass、hashCode、equals、clone、toString、wait/notify、finalize


一、Object 在 Java 类型体系中的位置

Java 是一门单根继承的语言,java.lang.Object 是所有类的终极祖先。即使你定义的类没有显式使用 extends,编译器也会自动将它指向 Object。

// 这两种写法完全等价
public class MyClass {}
public class MyClass extends Object {}

Object 的 12 个方法全景图

                     Object (12个方法)
                           │
        ┌──────────────────┼──────────────────┐
        │                  │                  │
   本地方法(7)        非final方法(3)      final方法(2)
   hashCode()         equals()           getClass()
   clone()            toString()         notify()
   registerNatives()  finalize()         notifyAll()
   getClass()                            + 3个 wait 重载
方法修饰符说明
registerNatives()private static native注册本地方法映射
getClass()public final native返回运行时类对象
hashCode()public native返回哈希码
equals(Object)public判断对象逻辑相等
clone()protected native创建并返回拷贝
toString()public返回字符串表示
notify()public final native唤醒单个等待线程
notifyAll()public final native唤醒所有等待线程
wait(long)public final native限时等待
wait(long, int)public final纳秒级限时等待
wait()public final无限等待
finalize()protectedGC 前回调

二、registerNatives —— 静态代码块中的秘密

public class Object {
    private static native void registerNatives();
    static {
        registerNatives();
    }
}

这个 static 块在 Object 类首次加载时执行。它的作用是将 Java 层的方法名与 JVM 内部的 C++ 函数地址做绑定映射:

// OpenJDK 中的 JNI 方法映射表
static JNINativeMethod methods[] = {
    {"hashCode",    "()I",   (void *)&JVM_IHashCode},
    {"wait",        "(J)V",  (void *)&JVM_MonitorWait},
    {"notify",      "()V",   (void *)&JVM_MonitorNotify},
    {"notifyAll",   "()V",   (void *)&JVM_MonitorNotifyAll},
    {"clone",       "()Ljava/lang/Object;", (void *)&JVM_Clone},
};

JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls, methods,
        sizeof(methods)/sizeof(methods[0]));
}

这样做的好处是可以提前完成方法绑定,避免每次调用时都要进行耗时的符号查找。


三、getClass —— 运行时类型信息

public final native Class<?> getClass();

getClass() 返回的是引用变量实际指向的对象的运行时 Class 对象,而不是声明类型。

public class GetClassDemo {
    public static void main(String[] args) {
        Number num = Integer.valueOf(42);

        Class<?> clazz = num.getClass();
        System.out.println(clazz.getName());          // java.lang.Integer
        System.out.println(clazz.getSimpleName());     // Integer
        System.out.println(clazz.getSuperclass());     // class java.lang.Number

        // 反射获取所有方法
        for (java.lang.reflect.Method method : clazz.getDeclaredMethods()) {
            System.out.println(" - " + method.getName());
        }
    }
}

注意,getClass()final 修饰,任何子类都无法覆写,保证了运行时类型判断的可靠性。


四、hashCode —— 哈希世界的通行证

public native int hashCode();

4.1 哈希码的核心约定

JDK 文档明确规定了 hashCode 的三条铁律:

  1. 一致性:同一对象多次调用 hashCode 必须返回相同值(前提是 equals 比较用到的信息未变)
  2. 等价性a.equals(b) == truea.hashCode() == b.hashCode()
  3. 非必须不等a.equals(b) == false 不要求 hashCode 不同,但不同能提升哈希表性能

4.2 OpenJDK 中 hashCode 的生成策略

HotSpot 提供了六种 hashCode 生成模式,通过 JVM 参数 -XX:hashCode=N 切换:

模式算法特点
0Park-Miller 随机数默认值
1基于对象内存地址 + 随机数性能好
2固定值 1仅用于测试
3自增序列简单递增
4内存地址直接转换最直观
5线程状态 + xorshiftJDK 8 默认

4.3 自定义 hashCode 实现示例

public class Person {
    private String name;
    private int age;

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

    @Override
    public int hashCode() {
        // 经典组合方式:31 作为乘数
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Person)) return false;
        Person other = (Person) obj;
        return age == other.age
            && java.util.Objects.equals(name, other.name);
    }

    public static void main(String[] args) {
        Person p1 = new Person("张三", 25);
        Person p2 = new Person("张三", 25);

        System.out.println("equals: " + p1.equals(p2));       // true
        System.out.println("hash相等: " + (p1.hashCode() == p2.hashCode())); // true

        java.util.HashSet<Person> set = new java.util.HashSet<>();
        set.add(p1);
        set.add(p2);
        System.out.println("Set size: " + set.size());        // 1,正确去重
    }
}

五、equals —— 逻辑相等的判定

public boolean equals(Object obj) {
    return (this == obj);
}

Object 的默认实现就是 == —— 内存地址比较。大多数子类需要覆写 equals 实现"内容相等"的比较。

5.1 equals 覆写的五条军规

  1. 自反性x.equals(x) 永远为 true
  2. 对称性x.equals(y) 为 true ⇒ y.equals(x) 也为 true
  3. 传递性x.equals(y)y.equals(z)x.equals(z)
  4. 一致性:信息未变时多次调用结果一致
  5. 非空性x.equals(null) 永远为 false

5.2 标准 equals 覆写模板

public class Book {
    private String isbn;
    private String title;

    public Book(String isbn, String title) {
        this.isbn = isbn;
        this.title = title;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;                 // 1. 同引用快速返回
        if (!(obj instanceof Book)) return false;      // 2. 类型检查
        Book other = (Book) obj;                       // 3. 安全转型
        return java.util.Objects.equals(isbn, other.isbn); // 4. 关键字段比较
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hashCode(isbn);
    }

    public static void main(String[] args) {
        Book b1 = new Book("978-7-111", "设计模式");
        Book b2 = new Book("978-7-111", "设计模式");
        Book b3 = new Book("978-7-112", "Java并发");

        System.out.println(b1.equals(b2)); // true
        System.out.println(b1.equals(b3)); // false
    }
}

六、clone —— 对象拷贝的底层机制

protected native Object clone() throws CloneNotSupportedException;

6.1 Cloneable 标记接口

Cloneable 接口不包含任何方法,它的唯一作用是:告诉 JVM 该类支持 clone() 操作。如果调用了未实现 Cloneable 的类的 clone() 方法,JVM 会抛出 CloneNotSupportedException

6.2 浅拷贝 vs 深拷贝

public class CloneDemo {
    // 浅拷贝示例
    static class Address implements Cloneable {
        String city;
        Address(String city) { this.city = city; }

        @Override
        protected Address clone() throws CloneNotSupportedException {
            return (Address) super.clone();
        }
    }

    // 浅拷贝——引用类型字段共享同一个对象
    static class UserShallow implements Cloneable {
        String name;
        Address address;

        UserShallow(String name, Address address) {
            this.name = name;
            this.address = address;
        }

        @Override
        protected UserShallow clone() throws CloneNotSupportedException {
            return (UserShallow) super.clone();
        }
    }

    // 深拷贝——递归克隆所有引用类型字段
    static class UserDeep implements Cloneable {
        String name;
        Address address;

        UserDeep(String name, Address address) {
            this.name = name;
            this.address = address;
        }

        @Override
        protected UserDeep clone() throws CloneNotSupportedException {
            UserDeep cloned = (UserDeep) super.clone();
            cloned.address = this.address.clone();  // 关键:手动克隆引用字段
            return cloned;
        }
    }

    public static void main(String[] args) throws Exception {
        // 浅拷贝演示
        UserShallow original = new UserShallow("李明", new Address("北京"));
        UserShallow cloned = original.clone();

        cloned.address.city = "上海";  // 修改克隆对象的地址
        System.out.println("浅拷贝: " + original.address.city);  // "上海"——原始对象也被改了

        // 深拷贝演示
        UserDeep originalDeep = new UserDeep("王芳", new Address("深圳"));
        UserDeep clonedDeep = originalDeep.clone();

        clonedDeep.address.city = "广州";
        System.out.println("深拷贝: " + originalDeep.address.city); // "深圳"——互不影响
    }
}

6.3 clone 方法的推荐替代方案

在实际项目中,建议优先使用拷贝构造器或工厂方法,更加显式和安全:

public class Order {
    private String id;
    private double amount;

    // 拷贝构造器——更清晰的语义
    public Order(Order source) {
        this.id = source.id;
        this.amount = source.amount;
    }

    // 静态工厂方法
    public static Order copyOf(Order source) {
        return new Order(source);
    }

    public static void main(String[] args) {
        Order o1 = new Order("ORD-001", 99.90);
        Order o2 = Order.copyOf(o1);
        System.out.println(o2.amount); // 99.90
    }
}

七、toString —— 对象的自我描述

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

默认输出形如 com.example.Book@1a2b3c4d。日常工作中有几个重要的实践准则:

  • 日志打印前覆写 toString:否则日志中只有无意义的哈希码
  • Lombok 的 @ToString 注解:自动生成易读的 toString
  • 避免在 toString 中暴露敏感字段:如密码、token 等
public class Product {
    private String sku;
    private String name;
    private double price;

    @Override
    public String toString() {
        return String.format("Product{sku='%s', name='%s', price=%.2f}",
                sku, name, price);
    }
}

八、wait / notify —— 线程间通信的基石

8.1 方法签名

public final void wait() throws InterruptedException
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final native void notify()
public final native void notifyAll()

8.2 正确使用模式

public class WaitNotifyDemo {
    private boolean dataReady = false;

    public synchronized void produceData() {
        System.out.println("生产者: 开始准备数据...");
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
        dataReady = true;
        System.out.println("生产者: 数据就绪,通知消费者");
        notifyAll();  // 唤醒所有等待线程
    }

    public synchronized void consumeData() throws InterruptedException {
        // 标准等待模式:循环检查条件
        while (!dataReady) {
            System.out.println("消费者: 数据未就绪,进入等待...");
            wait();
        }
        System.out.println("消费者: 收到通知,开始消费数据");
    }

    public static void main(String[] args) throws InterruptedException {
        WaitNotifyDemo demo = new WaitNotifyDemo();

        Thread consumer = new Thread(() -> {
            try { demo.consumeData(); }
            catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });

        Thread producer = new Thread(demo::produceData);

        consumer.start();
        Thread.sleep(500);  // 确保消费者先进入 wait
        producer.start();

        consumer.join();
        producer.join();
    }
}

8.3 关键注意事项

  • 必须在 synchronized 块中调用:否则抛出 IllegalMonitorStateException
  • 使用 while 而非 if:防止虚假唤醒(spurious wakeup)
  • notify 随机唤醒一个 / notifyAll 唤醒全部:优先使用 notifyAll 避免遗漏
  • wait 会释放锁,sleep 不会:这是两者最本质的区别

8.4 wait(long, int nanos) 的微妙细节

public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0)
        throw new IllegalArgumentException("timeout value is negative");
    if (nanos < 0 || nanos > 999999)
        throw new IllegalArgumentException("nanosecond timeout value out of range");
    if (nanos >= 500000 || (nanos != 0 && timeout == 0))
        timeout++;
    wait(timeout);
}

注意这里的四舍五入逻辑:当纳秒 >= 500000 时,毫秒部分会加 1。这使得 wait(0, 500000) 等价于 wait(1),而非 wait(0)


九、finalize —— 不推荐的资源清理方式

protected void finalize() throws Throwable {}

finalize() 的设计初衷是让对象在被 GC 回收前有机会释放外部资源,但实践中不应依赖它

  • 调用时机不确定:对象可被 GC 标记后到实际执行 finalize 可能有任意长延迟
  • 可能永远不被调用:程序正常退出时,GC 不保证执行所有 finalize
  • 性能开销巨大:带有 finalize 的对象回收需要 GC 至少两轮
  • 复活风险:在 finalize 中重新引用对象,会导致对象"复活",造成内存管理混乱

推荐替代方案

public class ResourceHolder implements AutoCloseable {
    private java.io.FileInputStream stream;

    public ResourceHolder(String path) throws java.io.FileNotFoundException {
        this.stream = new java.io.FileInputStream(path);
    }

    @Override
    public void close() throws java.io.IOException {
        if (stream != null) {
            stream.close();
            stream = null;
        }
    }

    public static void main(String[] args) throws Exception {
        // try-with-resources —— JDK 7+ 的最佳实践
        try (ResourceHolder holder = new ResourceHolder("data.txt")) {
            // 使用资源
        }
        // 自动调用 close(),无需 finally 块
    }
}

十、综合实战:手写一个安全的缓存类

import java.util.concurrent.TimeUnit;

/**
 * 基于 Object 核心方法实现的线程安全 LRU 缓存
 * 综合运用 hashCode、equals、clone、wait/notify 等
 */
public class ObjectBasedCache {

    static class CacheEntry implements Cloneable {
        String key;
        String value;
        long expireAt;

        CacheEntry(String key, String value, long ttlMillis) {
            this.key = key;
            this.value = value;
            this.expireAt = System.currentTimeMillis() + ttlMillis;
        }

        boolean isExpired() {
            return System.currentTimeMillis() > expireAt;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof CacheEntry)) return false;
            CacheEntry that = (CacheEntry) o;
            return java.util.Objects.equals(key, that.key);
        }

        @Override
        public int hashCode() {
            return java.util.Objects.hash(key);
        }

        @Override
        protected CacheEntry clone() {
            try {
                return (CacheEntry) super.clone();
            } catch (CloneNotSupportedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private final java.util.LinkedHashMap<String, CacheEntry> store;
    private final int maxSize;

    public ObjectBasedCache(int maxSize) {
        this.maxSize = maxSize;
        this.store = new java.util.LinkedHashMap<String, CacheEntry>(16, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(
                    java.util.Map.Entry<String, CacheEntry> eldest) {
                return size() > ObjectBasedCache.this.maxSize;
            }
        };
    }

    public synchronized void put(String key, String value, long ttl, TimeUnit unit) {
        store.put(key, new CacheEntry(key, value, unit.toMillis(ttl)));
        notifyAll();  // 唤醒可能在 get 中等待的线程
    }

    public synchronized String get(String key) throws InterruptedException {
        CacheEntry entry = store.get(key);
        if (entry == null || entry.isExpired()) {
            return null;
        }
        return entry.value;
    }

    public synchronized CacheEntry getClone(String key) throws InterruptedException {
        CacheEntry entry = store.get(key);
        if (entry == null || entry.isExpired()) {
            return null;
        }
        return entry.clone();  // 返回克隆副本,防止外部修改
    }

    @Override
    public String toString() {
        return "Cache[size=" + store.size() + "/" + maxSize
                + ", keys=" + store.keySet() + "]";
    }

    public static void main(String[] args) throws InterruptedException {
        ObjectBasedCache cache = new ObjectBasedCache(3);

        cache.put("user:1", "Alice", 5, TimeUnit.SECONDS);
        cache.put("user:2", "Bob", 5, TimeUnit.SECONDS);
        cache.put("user:3", "Charlie", 5, TimeUnit.SECONDS);

        System.out.println(cache);  // size=3

        // 触发淘汰:添加第4个元素,最老的被移除
        cache.put("user:4", "David", 5, TimeUnit.SECONDS);
        System.out.println(cache);  // size=3

        // 验证深拷贝不影响原始缓存
        CacheEntry cloned = cache.getClone("user:4");
        cloned.value = "被篡改的值";
        System.out.println("原始缓存: " + cache.get("user:4"));  // David
    }
}

十一、面试高频考点

问题关键要点
hashCode 与 equals 的关系equals 相等则 hashCode 必等,反之不必然
为什么要同时覆写HashMap 先用 hashCode 找桶,再用 equals 判等
wait 与 sleep 的区别wait 释放锁 + 需在 synchronized 中调用;sleep 不释放锁
浅拷贝 vs 深拷贝浅拷贝共享引用对象,深拷贝递归克隆
为什么 clone 是 protected防止任意类随意调用,仅开放给有 Cloneable 声明的子类
finalize 为什么废弃(JDK 9)不保证执行时机、性能损耗大、对象复活风险