对象的初始化过程及其深入理解

一、什么是一个类

        在Java语言里面,类用class描述,拥有变量和函数。没有提供get或set方法的变量称之为字段。有get或者set的任意方法或全部方法的字段称之为属性。所有的类都继承自Object类,并且继承了Object类提供的class属性。
        类和对象的关系,简单说:
        类可以看成一类对象的模板,对象可以看成该类的一个具体实例。

二、子类和父类

        继承是从父类中派生出新的类,这个类称之为子类或者派生类。子类拥有父类“所有”的属性和方法,并且子类能够扩展其它特性。比如说人,人可以作为一个父类,它可以派生出子类,比如男人,男人具备了人的一切特性。并且男人还有其它特性,比如说长胡子。

        如果父类的属性或者方法是private修饰的,那么父类的属性和方法依然会被子类继承到。但是,如果父类没有提供公共的访问方法的话,那么子类将无法访问到此属性或者方法,即使子类拥有此属性或者方法。举个例子,我是潘多拉的继承人,并且我继承到了潘多拉留下的魔盒,但是潘多拉并没有提供钥匙给我,所以尽管我现在拥有了潘多拉魔盒,但是仍然不能打开盒子。这就是拥有但是不能访问的现象。

        在Java中,通常父类用private修饰的属性会提供公共的访问方法,通常是public修饰的get方法。(相当于例子中的钥匙)

三、对象的初始化过程

        加载某个类到JVM中,由第一次调用这个类的静态成员触发。而构造函数又是一种特殊的静态函数,因此new一个对象的时候,JVM虚拟机将会开始加载这个类。

        类是对象的模版,对象是类的具体实例。

        通常对象的初始化过程如下:

                步骤1:第一次调用类的构造函数创建对象,也就是说第一次调用一个类的静态成员的时候,JVM将会动态的加载这个类到JVM中。用来描述这个类信息的是一个class对象。JVM加载的就是这个class对象。如果这个类有静态代码块,那么此时静态代码块将会被执行。

                步骤2:class对象加载完毕。接下来就是将此类中所有的属性将会被设置为默认值。比如,int类型的属性的默认是 0,  boolean类型的属性默认值是false等。此步骤可以理解为“默认初始化”。

                 步骤3:接着,执行构造函数的第一行,通常都是super()。基本上每个构造函数第一行都会有一句隐式super(),它将会调用父类的构造函数。子类来自于父类,所以父类的class对象也已经被JVM加载了。根据第二步,父类中所有的属性都被“默认初始化”为默认值。如果父类是Object的话,super()执行完毕都没有什么效果。现在,根据用户的定义, 比如有int age = 22;  在内存中,经过步骤2,age默认值是“0”。步骤3的作用就是把age初始化为"22",这一步可以理解为“显示初始化”。

                步骤4:显示初始化完毕,将会调用"构造代码块"。构造代码块的作用是为这个类的每一个对象进行属性的初始化。 如果我们没有在步骤3里面为字段进行“显示初始化”,那么我们可以在“构造代码块”中为字段进行初始化。这步可以理解为“构造代码块初始化”

                步骤5:构造代码块执行完毕,就下来就是执行构造函数中剩下的语句了。

例如

/**
 * Created by jay.zhou on 2018/2/22.
 */
public class Fu {
    public int count = 1;
    static {
        System.out.println("父类的class文件被JVM加载,类中所有字段被设置为默认值,int类型的值的默认值是0");
    }

    {
        System.out.println(count);
        System.out.println("根据count的值知道,count的值被显示初始化为1");
        System.out.println("父类的构造代码块执行");
        count = 2;
        System.out.println("count的值已经被修改");
    }

    public Fu() {
        super();
        System.out.println("父类的构造函数执行");
        System.out.println(count);
    }
}

class Zi extends Fu {
    static {
        System.out.println("子类的class文件被JVM加载,类中所有字段被设置为默认值,int类型的值的默认值是0");
    }

    {
        System.out.println("子类的构造代码块执行");
    }

    public Zi() {
        super();
        System.out.println("子类的构造函数执行");
    }

    public static void main(String[] args) {
        new Zi();
    }
}
父类的class文件被JVM加载,类中所有字段被设置为默认值,int类型的值的默认值是0
子类的class文件被JVM加载,类中所有字段被设置为默认值,int类型的值的默认值是0
1
根据count的值知道,count的值被显示初始化为1
父类的构造代码块执行
count的值已经被修改
父类的构造函数执行
2
子类的构造代码块执行
子类的构造函数执行

解释:

         在main()中,调用子类的构造函数,由于构造函数是一种特殊的静态函数,将会触发JVM加载子类。子类的存在必须依附于其父类,原因在下面会解释。因此,父类与子类一样,将会被加载到JVM虚拟机中,并根据步骤1,调用它们的静态代码块。父类静态代码块优先于子类静态代码块执行,可以简单的理解,先有了爸爸才会有儿子。

         根据步骤2,此时,类Fu和类Zi,它们的属性的值都是默认值,比如Fu类中的count被设置为0。

         接着,调用 Zi类的构造函数,它的第一行是super()。那么将会执行Fu类的构造函数。Fu类继承于Object类,在Fu类的构造函数中,调用完super() 后,将不会产生任何效果。接着,根据步骤3,Fu类将会进行“显示初始化”,根据private int count = 1; Fu类属性count的值从“0”被赋值为了“1”。这一点在"构造代码块"中的打印可以证明。

         根据步骤4,此时进行“构造代码块”初始化。构造代码块能为这个类的每个对象进行初始化,然而事实上很少有人使用构造代码块初始化,基本上用的是在“构造函数中”进行初始化。

      “构造代码块”执行完毕,将会执行“构造函数初始化”,此时将会执行构造函数中进行的定义的语句。

        此时,在Zi类中的构造函数的super()语句就全部执行完毕了。Zi类将会像Fu类执行完super()语句一样,进行“显示初始化”,“构造代码块初始化”,最后执行Zi类构造函数中剩下的语句。

       综上:对象初始化过程的步骤是:

                 1.被JVM加载,执行静态代码块

                 2.默认初始化 ,  属性被赋予默认值

                 3.显示初始化,定义的时候被赋予什么值,现在就是什么值

                 4.构造代码块初始化,构造代码块中重新为属性赋予新值

                 5.最后是构造函数初始化,一锤定音的还是构造函数中赋予的值

对象的初始化过程较为复杂,如果有没有看懂的欢迎留言。

四、子类对象与父类对象的联系

        类好比一张图纸。在创建对象的过程中,子类对象根据子类的图纸,创建出来的对象,为什么它能够访问到从父类继承到的属性呢?问题是父类也是一张图纸,我们在创建子类对象的时候,并没有调用父类的构造函数,去创建一个父类对象。
比如  new Zi(); 为什么这个对象它拥有父类的count这个字段
        子类对象与父类对象的关系,如图所示。

           

           此图说明,在初始化子类对象之前,父类对象就已经被初始化完毕。

           子类对象能够访问到父类对象的属性,这说明每一个子类对象都独立的拥有一个属于它自己的父类对象

           因此,可以说,子类对象的创建必须依附于父类对象。在创建子类对象的过程中,先创建一个父类对象A,然后创建一个子类对象B,最后把A对象安装到B对象中去。

五、要点

          加载一个类到JVM中,由第一次调用这个类的静态成员触发。构造函数其实是一种特殊的静态函数。

          每个子类对象都独立的拥有一个属于它自己的父类对象。

你看我都这么努力的分享知识给你了,鼓励一下又何妨O(∩_∩)O

你的打赏是对我最好的支持!

                    

posted @ 2022-07-17 12:12  小大宇  阅读(6)  评论(0编辑  收藏  举报