深入解读Object类

Java是单根继承的,每个类都继承自这个超类,那这个根是谁呢?

我们都知道是Object类,不过它的出镜率真的很低。

问题一:我们平常定义一个类的时候为什么不需要写extends Object?

如果没有显式继承自其它非Object类,编译器默认会让这个类继承成Object类。这一点可以通过反编译.class文件确认。

问题二:Object类有哪些属性或者成员方法?

属性是一个都没有,方法倒是有好几个。

1、getClass

源代码:

public final native Class<?> getClass();
复制代码

getClass方法用于获取对象的运行时类,在反射的时候用的比较多。

注意到这个方法被final和native修饰,那说明

  • 不能覆写
  • 实现在C/C++层

返回的Class对象就是表示类静态同步方法锁定的对象。


2、hashCode

源代码:

public native int hashCode();
复制代码

hashCode方法用户获取对象的hash值。 在强烈依赖hashCode的地方是必须的。比如HashMap中对key进行hash计算,其实是利用key的hashCode。

这个方法的实现依然在native层,具体实现暂时不得而知。


3、equals

源代码:

    public boolean equals(Object obj) {
        return (this == obj);
    }
复制代码

equals方法用于比较两个对象是否相等。

默认的实现就是通过比较引用来判断是不是相等。 这种实现比较简单粗暴,首先引用相等就是同一个对象,肯定是相等的。但是在实际应用中引用不等,对象未必不相等。比如Integer类的版本:

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }
复制代码

注意:覆写equals方法之后,一定要覆写hashCode!!!

为什么?

首先这个是Object规范中要求的,两个相等的实例必须有相同的hashCode。那我们探究一下根本原因。

以HashMap为例,上面提到对key进行hash,其实是你用了key的hashCode。HashMap中判断两个key是否相同,除了equals还要判断hashCode是否一致。如果没有覆写,本来我们本意认为是同一个key的,现在变成了两个,这个影响可能很小但也可能是灾难性的。


4、clone

源代码:

protected native Object clone() throws CloneNotSupportedException;
复制代码

这个方法就是创建并返回一个对象的拷贝。

这个方法有三个值得注意的点:

  • 方式是native实现信息的拷贝。
  • 方法是protected,外部没法调用。
  • 如果没有实现Cloneable接口,调用 的时候会抛出CloneNotSupportedException异常。

如果外部需要调用clone方法,要么通过反射,要么将它覆写成public方法。

这个Cloneable接口中没有定义任何方法,所以实现Cloneable接口没有别的作用,就代表具备了使用clone方法的权利。

官方文档上有这样一句话:

The precise meaning of "copy" may depend on the class of the object

意思拷贝的实际效果会因类而已。官方文档居然有这种模糊的表述,究竟是何原因? native的实现源码不得而知,但是从文档上看,是先创建一个对象,然后对属性做浅拷贝。那既然是浅拷贝,基本数据类型和复合数据类型拷贝的效果肯定是不一样的,一个是按值,一个是按引用。

我们自己要实现对象拷贝,该怎么做?

下面以ArrayList的clone方法为例:

    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }
复制代码
  1. 通过super.clone()由其父类的clone方法创建对象(前提是父类的clone方法实现的没问题)
  2. 对复杂类型数据进行深拷贝
  3. 对基本数据类型直接赋值即可

那如果有final成员变量,赋值就不管用了,这里就会出现了不可调和的矛盾,这里需要注意。

似乎final成员变量的问题可以通过构造函数解决,但是《Effective Java》中提到:

如果Cloneable接口是要对某个类起到作用,类和它的所有超类都必须遵守一个相当复杂的,不可实施的,并且基本没有文档说明的协议。由此得到一种语言之外的(extralingustic)机制:无需调用构造器就可以创建对象。


5、toString

源代码:

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

toString方法就是把对象用字符串的形式表示,这个在日志中使用较多,当然也可以是类型转换,比如Integer类。

默认的实现其实没有什么用,"类名@hashCode",直接打印对象输出就是这样的格式。

官方文档建议最好覆写这个方法,返回一些有价值的、可读性强的信息。


6、notify

源代码:

    public final native void notify();
复制代码

notify方法就是唤醒等待一个的线程。

那如果有多个等待的线程怎么办?别慌,其实是会任意挑一个线程的,选择哪个线程取决于操作系统对多线程管理的实现。

notify之后,被唤醒的线程并不能立即运行,必须等到当前线程释放了锁以后。

调用notify方法的线程必须持有对象锁,那持有对象要通过如下三种途径:

  • 调用该对象的同步实例方法
  • 进入该对象的同步代码块
  • 调用该对象的同步静态方法

7、notifyAll

源代码:

    public final native void notifyAll();
复制代码

notifyAll和notify的区别就是notifyAll一次性唤醒所有等待的线程。


8、wait

源代码:

    public final native void wait(long timeout) throws InterruptedException;
    
    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 > 0) {
            timeout++;
        }

        wait(timeout);
    }
    
    public final void wait() throws InterruptedException {
        wait(0);
    }
复制代码

notify的前提是有等待的线程,那线程为什么会处于等待状态,什么事情也做不了,那是因为wait方法。

wait方法重载了三个版本,主要分为有超时和没有超时。有超时的,时间一到就解除,除非中间被唤醒;没有超时的那就一直等待,直到被唤醒。

处于等待状态的线程,除了被唤醒、超时解除以外,还有两种方式,一种是常见的被其他线程中断(interrupt),另外一种是“虚假唤醒”。

至于这个“虚假唤醒”是啥,我现在还是没能很好的理解,需要继续研究。

9、finalize

源代码:

protected void finalize() throws Throwable { }
复制代码

finalize是GC准备回收对象的时候调用来执行清理工作的。

很遗憾的是,Java语言不能保证finalize及时执行,也不能保证它会执行。

那能保证什么?

保证调用finalize之后,线程持有的用户可见的同步锁都被释放掉。还有如果finalize方法执行过程中发生了异常,异常会被忽略,然后流程中止。

既然不可靠,那这个方法就比较鸡肋。如果我们真的需要清理一些资源,比如打开的文件, 或者其他流,需要关闭。我们可以使用try-finally来解决。

转载于:https://juejin.im/post/5a9e9cb751882577b45e863e

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值