大二结束这次暑假,计划的挺多的,迫于环境因素,还是无法完成所有计划,毕竟还是挺多事的啊。
那么进入本文的主题:我们在初学过程中多多少少还是有一些知识遗漏,今天我发现一点遗漏的知识其实在很早之前遇到过,也试验过,但是实验并不完全,因为当初知识能力有限,想不出更多的情况作为实验变量。今天就遇到了之前实验没有考虑周到的地方,本问题在think in java 第五章 、第七章和第八章中均有相关描述。
目录
至此希望各位大佬不要吝啬自己的见解!我也希望多多接受一些建议
编译环境说明:
- jdk1.8
- windows 10 nt
- eclipse 2019
理解要点:
- 在一个子类在实例化过程中会首先调用父类的构造器来实现父类的绝对优先初始化过程
- 子类实例化过程中,如果没有显示的调用父类的构造器,那么就会默认的调用父类的无参构造器
参考资料:
《think in java》第五章 第七章 第八章
一、一个对象是怎么产生的?
我们在开始学习java的时候,会讲到实例化对象的相关操作。通常我们都知道实例化一个对象是通过new关键词,new之后呢?jvm就根据被实例化的类中的成员数据占用空间大小开辟一片内存空间,以保证对象存活,并把这片内存空间的指针(内存空间地址)赋值给别名,关于jvm内存分配和别名相关的细节在这里不做讨论,本文把焦点投向在对象创建的初始化过程中去。
二、对象初始化过程
在new之后,就是类的初始化进程,此时初始化的顺序,应该是按照static修饰过的成员,再次就是成员初始化,往后就是构造器。
构造器的作用在于对本类实例化后保证对象的正常运行和调用。所以构造器的作用在实例化过程中是必须被调用的。当然使用类的成员不一定需要实例化,只需要被static修饰一下成员,方可不通过实例化就可以通过类名.(static修饰过的)成员调用。static作用本文不做讨论。
类的构造器有默认构造器和带参构造器两种,默认构造器也称为无参构造器、隐式构造器。如果我们一个构造器都没有写,那么在实例化的时候虚拟机也会为我们创造一个默认构造器,这是众所周知的;如果我们定义了一个构造器,那么我们就必须以定义过的构造器实例化该类,比如我定义了一个带参构造器,那么我们必须以带参形式的方式实例化该类,而且参数必须按照构造器的顺序和类型传参,以达到对象初始化所必须的数据(对象)。例如TestDemo.java:
class Demo_01{ }
class Demo_02{
Demo_02(int i){
System.out.println("Demo_02(" + i + ")");
}
}
public class TestDemo{
public static void main(String[] args){
new Demo_01();//正常实例化
//new Demo_02();//实例化类出现错误
}
}
在TestDemo.java中,我们尝试以无参构造的方式去实例化Demo_01和Demo_02,在使用无参构造器(默认构造器)实例化Demo_01没有出现报错,但我们并没有定义无参构造器,这是因为jvm为我们创建的。在我们通过无参构造的方式去实例化Demo_02的时候,出现了错误,这是因为我们定义了构造器(也称之为构造方法),所以jvm不会在为我们创建默认构造器了,而是必须使用定义的构造器来实现实例化Demo_02。
三、继承中的构造器初始化过程
既然构造器是为了保证类在实例化过程中保证成员完全可用,那么在继承中出现子类(又名:导出类、派生类、继承类)要基于父类(又名:基类)的成员的情况又该怎么办?这时候构造函数的作用就起到了至关重要的作用。在继承中,子类的实例化过程中涉及到构造器的调用,子类在实例化的时候jvm首先会扫描该类是否存在父类,如果有父类,那么首先调用父类构造方法,如果父类的还有父类,那么首先就调用父类的父类构造器,以此类推。请看改造后的TestDemo.java例子:
class Demo_01{
Demo_01(){
System.out.println("Demo_01()");
}
}
class Demo_02 extends Demo_01{ }
public class TestDemo extends Demo_02{
TestDemo(){
System.out.println("TestDemo()");
}
public static void main(String[] args){
new TestDemo();
}
}//end~
/**输出结果:
Demo_01()
TestDemo()
*/
例子中,实例化的是一个继承了两个类的TestDemo,但是我们可以看到实例化中初始化过程是基类Demo_01最先开始实例化的,接着会调用Demo_02的默认构造器,因为代码中没有重写默认构造器,所以Demo_02看起来没有任何响应,其实它是被调用了的,接着调用TestDemo的构造方法,到此初始化过程就结束了,这跟刚刚的结论是吻合的。
四、类在实例化过程中调用父类的默认构造方法来实现父类初始化
刚刚的代码在实现起来似乎没有任何问题,一切都是按照我们的计划进行的,我们也没有显式的调用构造器的顺序,这一切看起来似乎都很正常。直到遇到以下的代码结构,编译器会报错:
class Demo_04{
Demo_04(int i){
System.out.println("Demo_04(" + i + ")");
}
}
class Demo_05 extends Demo_04{//这里将会报错
Demo_05(){
System.out.println("Demo_05()");
}
Demo_05(int i){
System.out.println("Demo_05(" + i + ")");
}
}
以上代码会报错:
Implicit super constructor Class3() is undefined. Must explicitly invoke another constructor
原因在于,在实例化Demo_05的时候,不管用那种构造方法,在第一步去初始化父类的时候,没有显式的说明父类以怎样的方式去初始化自己,所以编译器就采用调用的父类的默认构造器(无参构造器)来保证父类的初始化,但是Demo_04的构造方法是被我们写过的带参构造器,在编译器试图去找无参构造器的时候就没有默认构造器(无参构造器)可以使用,因为Demo_04初始化只能通过带参构造实现,这时候就会提示“默认构造函数没有定义,必须显式的调用其他构造方法”的编译器提示。
如果要实现对父类的初始化就必须在其子类构造器中显式的调用父类的构造方法,或者在父类中添加无参构造器来实现对父类的初始化过程。代码实现如下:
class Demo_04{
Demo_04(int i){
System.out.println("Demo_04(" + i + ")");
}
}
class Demo_05 extends Demo_04{//这里错误消失
Demo_05(){
super(1);
System.out.println("Demo_05()");
}
Demo_05(int i){
super(1);
System.out.println("Demo_05(" + i + ")");
}
}
class Demo_04{
Demo_04(){}
Demo_04(int i){
System.out.println("Demo_04(" + i + ")");
}
}
class Demo_05 extends Demo_04{//这里错误消失
Demo_05(){
System.out.println("Demo_05()");
}
Demo_05(int i){
System.out.println("Demo_05(" + i + ")");
}
}