<span>类的初始化顺序</span>
一、无继承类的初始化顺序。
执行顺序:静态块--->>main()函数--->>构造块--->>构造方法。
1、静态代码块。用staitc声明,jvm加载类时执行,仅执行一次。(类初始化一次)
(1)静态代码块其实就是给类初始化的,而构造代码块是给对象初始化的。
(2)静态代码块中的变量是局部变量,与普通函数中的局部变量性质没有区别。
(3)一个类中可以有多个静态代码块。
2、构造代码块。类中直接用{}定义,每一次创建对象时执行。(统一初始化所有对象)
(1)对象一建立就运行构造代码块了,而且优先于构造函数执行。
(2)这里要强调一下,有对象建立,才会运行构造代码块,类不能调用构造代码块的,而且构造代码块与构造函数的执行顺序是前者先于后者执行。
(3)构造代码块与构造函数的区别是,构造代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。
3、构造方法。(初始化指定对象)
(1)对象一建立,就会调用与之相应的构造函数,也就是说,不建立对象,构造函数时不会运行的。
(2)构造函数的作用是用于给对象进行初始化。
(3)一个对象建立,构造函数只运行一次,而一般方法可以被该对象调用多次。
###########################################################################
二、继承类的初始化顺序。
Java类的初始化顺序
(静态变量、静态代码块)> 类里的 main()(如果有的话) > (变量、初始化块) > 构造函数。
(父类 静态变量、静态代码块)> (子类 静态变量、静态代码块) >子类main()(如果有的话) > (父类先给变量分配内存,然后 变量、初始化块) > 父类构造函数 > (子类先给变量分配内存,然后变量、初始化块) > 子类 构造函数 。
其中:
静态变量与静态代码块 的顺序取决于代码中出现的顺序,变量与初始化块也一样。
子类加载的时候,如果发现有父类,则优先父类,父类还有父类以此递归,顺序按上面的初始化顺序。
示例代码:
输出:
-
静态变量
-
静态初始化块
-
main1
-
变量
-
初始化块
-
构造器
-
main2
继承代码示例:
-------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------
输出:
父父类-静态变量
父父类-静态初始化块
父类-静态变量
父类-静态初始化块
子类-静态变量
子类-静态初始化块
子类-main1
父父类-变量
父父类-初始化块
父父类-构造器
父类-变量
父类-初始化块
父类-构造器
子类-变量
子类-初始化块
子类-构造器
子类-main2
###########################################################################
三、类初始化举例一。
让我们先来看两个类:Base和Derived类。注意其中的whenAmISet成员变量,和方法preProcess()
1.
public
class
Base
2.
{
3.
Base() {
4.
preProcess(); //构造方法里面调用成员方法,如果成员方法被覆写,则会调用子类的成员方法。
5.
}
6.
7.
void
preProcess() {}
8.
}
01.
public
class
Derived
extends
Base
02.
{
03.
public
String whenAmISet =
"set when declared"
;
04.
05.
@Override
void
preProcess() //这里覆写了父类的方法
06.
{
07.
whenAmISet =
"set in preProcess()"
;
08.
}
09.
}
如果我们构造一个子类实例,那么,whenAmISet 的值会是什么呢?
1.
public
class
Main
2.
{
3.
public
static
void
main(String[] args)
4.
{
5.
Derived d =
new
Derived();
6.
System.out.println( d.whenAmISet );
7.
}
8.
}
再续继往下阅读之前,请先给自己一些时间想一下上面的这段程序的输出是什么?是的,这看起来的确相当简单,甚至不需要编译和运行上面的代码,我们也应该知道其答案,那么,你觉得你知道答案吗?你确定你的答案正确吗?
很多人都会觉得那段程序的输出应该是“set in preProcess()”,这是因为当子类Derived 的构造函数被调用时,其会隐晦地调用其基类Base的构造函数(通过super()函数),于是基类Base的构造函数会调用preProcess() 函数,因为这个类的实例是Derived的,而且在子类Derived中对这个函数使用了override关键字,所以,实际上调用到的是:Derived.preProcess(),而这个方法设置了whenAmISet 成员变量的值为:“set in preProcess()”。
当然,上面的结论是错误的。如果你编译并运行这个程序,你会发现,程序实际输出的是“set when declared ”。怎么为这样呢?难道是基类Base 的preProcess() 方法被调用啦?也不是!你可以在基类的preProcess中输出点什么看看,你会发现程序运行时,Base.preProcess()并没有被调用到(不然这对于Java所有的应用程序将会是一个极具灾难性的Bug)。
虽然上面的结论是错误的,但推导过程是合理的,只是不完整,下面是整个运行的流程:
- 进入Derived 构造函数。
- Derived 成员变量的内存被分配。
- Base 构造函数被隐含调用。
- Base 构造函数调用preProcess()。
- Derived 的preProcess 设置whenAmISet 值为 “set in preProcess()”。
- Derived 的成员变量初始化被调用。
- 执行Derived 构造函数体。
等一等,这怎么可能?在第6步,Derived 成员的初始化居然在 preProcess() 调用之后?是的,正是这样,我们不能让成员变量的声明和初始化变成一个原子操作,虽然在Java中我们可以把其写在一起,让其看上去像是声明和初始化一体。但这只是假象,我们的错误就在于我们把Java中的声明和初始化看成了一体 。在C++的世界中,C++并不支持成员变量在声明的时候进行初始化,其需要你在构造函数中显式的初始化其成员变量的值,看起来很土,但其实C++用心良苦。
在面向对象的世界中,因为程序以对象的形式出现,导致了我们对程序执行的顺序雾里看花。所以,在面向对象的世界中,程序执行的顺序相当的重要 。
下面是对上面各个步骤的逐条解释。
- 进入构造函数。
- 为成员变量分配内存。
- 除非你显式地调用super(),否则Java 会在子类的构造函数最前面偷偷地插入super() 。
- 调用父类构造函数。
- 调用preProcess,因为被子类override,所以调用的是子类的。
- 于是,初始化发生在了preProcess()之后。这是因为,Java需要保证父类的初始化早于子类的成员初始化,否则,在子类中使用父类的成员变量就会出现问题。
- 正式执行子类的构造函数(当然这是一个空函数)。
###########################################################################
四、举例子二。
###########################################################################
五、初始化详解。
如果:A extends B
1.若要加载类A,应先加载父类B。而只要加载一个类,静态字段就会分配内存,静态代码块就会执行。则先为父类B(Object)的静态变量分配内存以及执行父类的静态语句块(执行先后顺序按由书写执行决定)。
2.然后再加载子类A,及为类A的静态变量分配内存以及执行类A的静态语句块。(并且1、2步骤只会在类第一次加载的时候执行,即最多执行一次)
结论一:子类的加载,必须先加载其父类,类的加载伴随着,静态变量的内存分配,静态语句的执行。
可以通过:Class.forname(A)加载类A。在父类的静态代码中输出父类中的静态变量,并在子类的静态代码块中输出子类的静态字段来验证1,2结论。
3.若需实例化类A,则在子类的构造方法执行之前,先调用其父类B的构造函数。并且在调用其父类B的构造函数前、在父类中会发生:
(1).父类B中的非静态变量分配内存以及执行父类中的非静态语句块.再调用父类B中的构造函数初始化初始化父类中的字段(因为父类中的字段要被子类继承,而字段的初始化,要通过自己的构造函数完成)。
(2).然后再给类A中的非静态变量分配内存以及执行类A非静态语句块.最后调用A中的构造函数初始化。( 并且第次实例化子类对象的时候过程3都会发生)
结论二:实例化对象的时候需要调用构造方法,由于子类继承父类的成员变量,所以实例化子类的时候,在子类的构造方法中一定要先调用了父类的构造方法(super()在子类的构造方法的第一行)来给子类中继承自父类的字段初始化,然后再执行子类中的构造函数来初始化子类特有的字段,这个步骤,会在每次实例化子类对象的时候重复执行。注意:一个类构造方法每次被调用之前,一定会先执行一次该类中的实例语句块。
5.对于静态方法和非静态方法都是被动调用,即系统不会自动调用执行,所以用户没有调用时都不执行,方法都存放在方法区中(静态区)等待用户的调用。只有用户调用时才给方法中的局部变量分配内存。
注意:加载不意味着执行!!!
总结:
子类的加载必先加载父类。类的加载伴随着静态变量的分配与静态语句块的执行(这个过程只会类第一次被加载时执行 )
子类的实例化,伴随着父类的实例化(先实例化父类,然后实例化子类),此时伴随着成员变量和实例语句块的执行(这个过程在每次实例化子类的时候都会发生)
继承:
1.子类会继承父类中所有的字段和方法(包括私有的字段和方法),但构造方法不能被继承。
2.子类即使继承了父类的私有字段,也不能直接访问,只能间接访问,或不能访问(但是可以通过父类提供的公开的方法中来做到间接地访问父类中私有的成员字段或方法,既然能被访问,说明子类为父类的私有字段分配了空间,继承了父类的私有字段,不然数据往那里存)
通过.net的调试器,可以清楚的验证这一结论,既然子类中可以访问父类的成员方法的成员变量那么可以理解为在子类对象中包含一个父类对象。
3.构造方法虽然不能被继承,但是子类中的任何一个构造方法执行前都会执行父类的构造方法。目的同继承为了减少代码的重复因为可以通过在子类中调用父类的构造来初始化子类,而不用在子类中在写一遍对父类中成员变量赋值的代码(如果子类中不显示地调用默认调用父类的无参构造,可以在父类中的无参构造中输出一名话,然后在实现化一个子类来验证)
4.子类可以通过方法的重写,或字段的重名来覆盖都父类中的方法或都字段。但是只要该字段或方法在父类中不是私有的,在子类中仍然可以通过”super.“字段名或方法名去访问。
5.this表示的就是当前对象,谁调用就指代谁,this就是对象的引象,保证对象的地址,每一个对象都有this存在堆中,可以理解为Object o=new Object();new返回的就是堆中对象中的this(this保存对象的引用,它的含义等价引用)静态代码段中没有this
6、super(或者base)都是指代当前子类对象中的父类型的特征。super指代子类从父类中继承过来的字段或方法,用于区分子类中重写父类的方法或者子类覆盖父类的中的字段,就可以通过”super.“方法名或字段名来显示的在子类中调用父类中的方法或字段,即使这些方法或字段被子类重写或覆盖。super不是引用,不保存内存地址,它只是存在于this中来指代子类从父类继承过来的数据(即父类中的数据),和子类中特有的数据。this能存在的地方super就存在,super是this的组成部分。
验证:
###########################################################################
今天在牛客碰到这样一道题:
- class A {
- public A foo() {
- return this;
- }
- }
-
- class B extends A {
- public A foo() {
- return this;
- }
- }
-
- class C extends B
- {
- //这样填写什么代码不会报错}
选项分别是:
A.public void foo(){}
B.public int foo(){return 1;}
C.public A foo(B b){return b;}
D.public A foo(){return A;}
正确答案:C
复习一下子类方法重写父类方法遵循“两同两小一大”的规则
子类覆盖父类要遵循“两同两小一大”
“两同”即方法名相同,形参列表相同
“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等。
(注:看到有网友有这样的疑问,父类方法返回值是double,子类修改成int为什么不行呢?
这是因为返回值类型更大或者更小,是对于同一类型而言的。也就是说,返回值的类型需要有继承关系才去考虑大小这个 概念。类型不同,肯定不是方法重写)
“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。
注意:覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法一个是实例方法,否则编译出错。
所以,根据这个原理,再来分析上面这道题。
A.public void foo(){}
返回值类型与父类不一致,所以不可能是方法的重写。又因为方法名相同,那么只能是方法重载,而方法重载有需要满足三个条件:形参个数、顺序、类型必须有一者不同,A选项都不满足,错
B.public int foo(){return 1;}
与A选项一样
C.public A foo(B b){return b;}
返回值类型与父类相同,但由于参数列表不同,所以是对父类方法的重载
D.public A foo(){return A;}
语法错误
###########################################################################