【JDK源码】java.lang.Object

Object类是所有类的基类,所有类都默认隐式的继承Object类,该类的背后是面对对象思想的体现。
Object类位于java.lang包中,lang包里包含了java最核心和最重要的类,在编译时会自动导入,下面让我们了解Object有哪些方法和属性吧!

image not show


(1)public Objict();
Java中规定:对于任意类都必须含有构造函数,如果用户没有自定义,会由于继承Object类的原因默认存在一个空参的构造函数。通常情况下我们通过A a=new A(args...)来创建对象,其中A是类名,A(args…)即该类中对应的构造函数。
我们发现Object中并没有构造函数,但其实构造函数是隐式存在,我们还要思考一个问题,构造函数的访问权限一定是public的吗?
答案是否定的。前面提到我们通常通过调用构造函数来创建对象,但是我们还有其他创建对象的方法,例如:反射或clone,所以我们可以说构造函数的权限并非一定需要是public。
(2)private static native void registerNative();
在刚开始阅读JDK源码的时候,我们需要关注一个关键字native,这个关键字的要点如下:
  • JNI:java native interface,即java本地接口
  • 无方法体:被native修饰的方法不需要在java里实现方法体
  • 底层操作:java进行底层硬件层面的操作能力很弱,于是我们通过JNI让例如C/C++实现我们的方法体,然后帮助java进行底层硬件层面的操作,具体实现可以自个去查阅
对于registerNative()这个方法本身,类似于中介,它的作用是将C/C++实现的方法体,搬运到被native修饰的方法里面,使得该方法能够被使用,方法作用过程如下:

图片无法显示

我们发现该方法的修饰符是private且并没有运行,那么这个方法是如何被使用的?实际上从Object类的源码我们发现该方法的下面紧邻的一个静态代码块:
  private static native void registerNatives();
    static {
        registerNatives();
    }
(3)protected native Object clone();
该方法的作用克隆一个调用clone()方法的对象,克隆对象独立于原对象,是两个方法属性都相同的不同的对象,拥有不同的堆地址,该方法的返回值是指向克隆对象地址的一个变量。
举个栗子:
public class CloneTest {

    public static void main(String[] args) {
        Object o = new Object();
        Object o1 = (CloneTest) o.clone();
    }
}
很悲催的是,这段代码报错了:Error:java: clone() 在 java.lang.Object 中是 protected 访问控制,怎么会呢?protected的访问权限是:在同一个包内或者不同包的子类可以访问,但是任何类都继承Object类,不就符合了不同包子类这个条件吗?
实际上很多人都误会了这句话的运用概念,它并不是说在子类中可以用父类的引用访问protected修饰符,正确的逻辑应该是在同一个包内或者不同包的子类的引用可以访问,将代码改成下面就行了:
public class CloneTest {

    public static void main(String[] args) {
        CloneTest o = new CloneTest();
        CloneTest o1 = null;
        try {
            o1 = (CloneTest) o.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        System.out.println(o.equals(o1));
    }
}
输出如下:

alse
java.lang.CloneNotSupportedException: CloneTest
at java.lang.Object.clone(Native Method)

从输出结果我们可以确定克隆对象与原对象的地址并不相同,换句话来说就是开辟了一块堆空间来储存克隆对象,但是输出的异常信息:java.lang.CloneNotSupportedException又是怎么回事呢?
原来在java里定义了这方法的语法规范:clone()的正确调用是需要实现Cloneable接口,如果没有实现Cloneable接口,则会抛出CloneNotSupportedException异常,于是将代码修改成如下即可:
public class CloneTest implements Cloneable{

    public static void main(String[] args) {
        CloneTest o = new CloneTest();
        CloneTest o1 = null;
        try {
            o1 = (CloneTest) o.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        System.out.println(o.equals(o1));
    }
}

Cloneable和Serializable一样都是标记型接口,它们内部都没有方法和属性,前者用来标记该类可以调用clone()方法,后者用来标记该类可以被可序列化。
(4)public final native Class<?> getClass();
该方法的作用返回是调用者的类对象,类对象对类的方法和属性都进行解析封装,在反射中我们会经常使用。
(5)public boolean equals(Object obj);
该方法在日常开发中使用率极高,用于判断两个对象是否相等,常常与==进行比较辨析:
  • ==:可用于基础数据类型和引用数据类型的比较,前者比较的是值,后者比较的是地址
  • equals:只能用于引用类型的比较,比较的是地址
不妨来看看在Object中equals()方法的实现:
    public boolean equals(Object obj) {
        return (this == obj);
    }
我们发现Object类中的equals()方法判断也是通过==来实现,那么我们猜想这个方法是不是冗余呢?
实际上存在即合理,在某种业务场景下,我们想要实现对引用类型的比较是通过比较值来判断的,这时使用==或原始的equals()方法无疑都是行不通的,那么我们就可以重写equals()方法,达到我们的需求,例如String类中就已经重写了equals()方法,因此两个String对象调用equals()方法进行比较时,实际比较的是它们的储存的字符串。
但是重写equals()方法时我们需要注意官方的一个说明:一旦重写此方法,通常需要同时重写hashCode()方法,以维护 hashCode() 方法的常规协定,该协定声明相等的对象必须具有相等的哈希码。如果重写equals()方法却不重写hashCode()方法,可能会存在对象相等而哈希值不相等的情况,最好避免这种情况的发生。
(6)public native int hashCode();
hashCode()方法的作用是返回调用者的哈希值,在官方中具有以下规范:
  • 程序运行期间,同一对象多次调用hashCode()方法得到的哈希值一定相同,在程序多次运行中,同一对象的哈希值不必保持一致
  • 两个对象相同,那么这两个对象的哈希值相同
  • 两个对象的哈希值相同,两个对象不一定相同
那么这个哈希值的计算是如何得到的呢?
我们可以看到这个方法是native方法,因此无法直接看到用C/C++实现的方法体,有兴趣的可以看看:https://blog.csdn.net/xusiwei1236/article/details/45152201,这篇博客详细探究了hashCode()方法的原生实现方式,从里面可以知道:原生方法中实现5种计算hash值的方法,其中有一种是通过计算对象的地址得到hash值,因此我们不能笼统的说哈希值就是通过计算对象地址得到的
最后面临一个很严峻的问题,hashCode()方法有什么用?比较对象?已经有equals()方法了呀!实际上该方法的主要作用是为了提高哈希表的性能,因为哈希表就是基于哈希值判断元素位置的呀!
以Set集合举例,我们都知道Set集合不允许存储重复元素,那么它底层是如何判断加入元素是否为重复元素的呢?
最简单的想法就是,将准备加入的元素和Set里元素进行一一遍历对比,这种做法的时间复杂度是O(n),作为程序猿的我们怎么能允许这种糟糕情况存在呢?
于是乎聪明的程序猿们就采用了空间换时间的方案,用hashCode()方法进行元素比较,根据得到哈希值来确定元素的位置,只要确定哈希值对应的位置没有元素就可以将新元素插入,这种方案的时间复杂度是O(1),大概逻辑如图:

图片无法显示

实际上Set的内部是通过Map来实现的,通过Map的key不重复性进行去重。
(7)public String toString();
该方法用于返回调用对象的字符串表示,那么其内部如何实现将对象转化成字符串表示呢?首先我们来看看方法原型:
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
由上面代码我们可以知道对象的所谓字符串表示是指:对象的包名类名@对象哈希值的16进制,举个栗子:
public class Test {
    public static void main(String[] args) {
        Object o=new Object();
        System.out.println(o);
       //输出java.lang.Object@677327b6
    }
}
于是我们结合hashCode()方法的官方规范可以得到这样的结论:同类型的不同对象, 调用toString()方法的返回值可能相同
延伸的问题来了,很多人都知道打印对象实际就是打印对象的toString()方法:

System.out.println(o);
System.out.println(o.toString());

但却不知道其内部是如何实现的,甚至不了解System.out.println()这句代码进行了哪些执行过程,我觉得作为一名程序猿应该心存对这些技术细节的探究精神,一张图了解执行过程:

image not show

由上图不难发现System.out.println()实际经历了3个过程:
  1. 通过System.out调用了System中PrinStream的静态变量out
  2. 通过变量out调用PrinStream类中的println()方法
  3. println()方法体中调用String类中的静态方法valueOf()
至此,对打印对象却隐式调用toString()方法的实现过程已经大致理清,这些都是我们通过源码可以轻易了解的知识。
(8)protected void finalize();
该方法与JVM的GC(垃圾回收)机制有关,当对一个对象进行垃圾回收时,它的finalize()方法会被自动调用,来看下它的方法原型:
protected void finalize() throws Throwable { }
我们发现该方法被定义成空方法体,任何Java类都可以重写Object类的finalize()方法,在该方法中进行清理对象占用资源的逻辑操作:
class Person {
    @Override
    public void finalize() {
        System.out.println("该对象被垃圾回收...");
    }
}

public class Test {
    public static void main(String[] args) {
        Person p = new Person();
        p = null;
        //通知垃圾回收器强制进行垃圾回收
        System.gc();
       //输出:该对象被垃圾回收...
    }
}
需要注意的是在程序终止前若没有进行垃圾回收,finalize()方法不会被自动调用,例如我们不进行强制垃圾回收,在上面代码中并不会自动执行finalize()方法,这涉及到垃圾回收机制的执行时机,在日后进行JVM的学习中我会全面总结。
(9)public final native void wait(…);
该方法与多线程之间的协作有关,调用wait(…)方法的线程无法争夺CPU的使用权,即无法被执行,这一状态称为等待状态
处于等待状态的线程会失去同步锁,只能被唤醒超时打断后且获得同步锁之后才能进行状态的转换。
举个栗子理解理解:
class MyThread extends Thread {
    //重写run方法
    public void run() {
        while (true) {
            System.out.println("线程被执行啦!");
            synchronized (this) {
                try {
                    //等待5秒后继续执行
                    wait(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        //调用start()方法启动线程
        thread.start();
    }
}
上面的栗子中,每次循环线程MyThread都会进入5秒的等待状态,之后超时从等待状态转换成就绪状态继续执行,这里要注意的是诸如whit(…)notify()等方法都需要在同步代码块中使用,不然会报监视器异常:java.lang.IllegalMonitorStateException
(10) public final native void notify()/notifyAll();
该方法与多线程之间的协作有关,往往与wait(…)方法配套使用,notify()/notifyAll()方法可以唤醒处于等待状态的线程。
class MyThread extends Thread {
    //重写run方法
    public void run() {
        synchronized (this) {
            try {
                System.out.println("语句1:线程被执行啦!");
                //线程处于等待状态
                wait();
                System.out.println("语句2:线程被唤醒啦!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

    }
}

public class Test {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        //调用start()方法启动线程
        thread.start();
        try {
        //使线程休眠5秒
            thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (thread) {
        //唤醒失去锁处于等待状态的thread线程
            thread.notify();
        }
    }
}

主线程与子线程之间的运行是相互独立的,谁运行取决于CPU的调度算法,让我们大概梳理一下这两个线程之间的运行次序:

图片无法显示

需要注意的是:notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行,多线程这一块是比较大且重要的知识点,等以后深入学习肯定要回头写一篇多线程的总结。

至此,基于我自己的理解的Object类已经总结完毕,知识框架或许还不完善,知识点可能有缺漏甚至错误,都需要随着自己的深入学习来进行完善,下一篇:String类!
发布了19 篇原创文章 · 获赞 51 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_42370146/article/details/96858172
今日推荐