对象的初始化过程及其深入理解
一、什么是一个类
在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
你的打赏是对我最好的支持!