第五章《类的继承》第2节:子类对象的构建过程

时间:2022-12-30 17:58:29

5.1小节讲解了类的继承机制。通过学习5.1小节的内容可知:子类能够继承父类的属性和方法,在此基础上,子类常常会扩展出属于自身特有的属性和方法,因此子类对象中的属性和方法可以分为两部分:从父类中继承而来的部分和自身所定义的部分。图5-3展示了Person和Student父子两个类对象的基本结构。

第五章《类的继承》第2节:子类对象的构建过程

图5-3 Person类对象与Student类对象

图5-3中,左边的是Person类的对象,右边是Student类对象。Student类对象被分为两部分,其中底色较深的部分表示从父类继承而来的属性,而底色较浅的部分表示自身所定义的属性。通过观察不难看出:Student类对象中底色较深的部分其实就是一个Person类的对象,因此,对于一个子类对象而言,父类对象是它的组成部分。从另一个角度来说:子类对象中包含了一个父类对象。既然子类对象中包含了一个父类对象,那么在创建一个子类对象时,是先创建哪一部分呢?

在Java语言中,虚拟机总是按照“先父后子”的顺序完成子类对象的创建。也就是说,当创建一个子类对象时,虚拟机总是会先创建子类对象中的那个父类对象,然后在父类对象的基础上再去增加属于子类自身的属性和方法,最终形成一个子类对象。为了验证这个结论,我们可以先在父类(Person)的无参数构造方法中添加一条输出语句,其代码如下:

Person(){
System.out.println("父类构造方法被执行");
}

之后在子类(Student)中添加一个无参数的构造方法,该方法中也有一条输出语句,其代码如下:

Student(){
System.out.println("子类构造方法被执行");
}

完成以上代码的修改后,运行【例05_02】即可证明虚拟机是按照“先父后子”的顺序创建子类对象的。

【例05_02 子类对象的构建过程】

Exam05_02.java

public class Exam05_02 {
public static void main(String[] args) {
Student s = new Student();
}
}

再次提醒各位读者:【例05_02】其实涉及到Person、Student和Exam05_02三个类。由于前文已经提供了Person和Student类的完整代码,所以此处仅给出Person、Student这两个类经修改之后的构造方法源代码,读者在运行此案例时还需从《范例5-2》中把更新后的Person和Student类源文件复制粘贴到本工程的src文件夹下。

Exam05_02类的main()方法中仅有一条语句,这条语句创建了一个子类(Student)对象,其运行结果如图5-4所示:

第五章《类的继承》第2节:子类对象的构建过程

图5-4 创建子类对象时先调用父类构造方法

通过图5-4可以看出:在创建子类对象时,首先调用了父类的构造方法,之后才调用子类的构造方法,这充分证明了创建子类对象时是遵循“先父后子”的创建顺序。很多读者都会问:在创建子类(Student)对象时,并没有调用其父类(Person)的构造方法,那么这个构造方法是在什么时候被调用的呢?

其实,虽然程序员并没有在程序中主动调用父类(Person)的构造方法,但为了遵循“先父后子”的创建顺序,编译器会在编译源代时会自动把调用父类构造方法的语句添加到程序中,并且会把父类的构造方法添加到子类构造方法的第一行。在子类构造方法中调用父类的构造方法,并不是直接使用构造方法的名称完成调用,而是使用了super关键字。因此,经过编译之后的子类(Student)构造方法代码如下:

Student(){
super();
System.out.println("子类构造方法被执行");
}

需要指出:编译器在添加调用父类构造方法的语句时,只会把父类无参数的构造方法添加到程序中,因此,如果父类中没有无参数的构造方法,只能由程序员手动把调用构造方法的语句添加到子类的构造方法中,并且还要把该语句置于第一行以保证它最先执行。

由第4章的内容可知:初始化一个对象的属性往往都是由“有参数”的构造方法完成的。而前文也讲过:一个子类对象中的属性一般分为两部分,一部分是从父类中继承过来的,另一部分是子类自己定义的。初始化从父类继承来的那些属性要由父类有参数的构造方法来完成,然而编译器只会把父类“无参数”的构造方法添加到子类构造方法中,因此,把父类有参数的构造方法添加到子类的构造方法中,这个操作只能由程序员“手动”完成。添加父类有参数的构造方法仍然要通过super关键字实现,并且还要把这条语句置于子类构造方法的第一行,以保证“先父后子”的创建顺序。以Student类为例,如果为它定义一个有参数的构造方法,则需要按如下形式编写代码:

Student(String name ,char sex ,int age,int num){
super(name,sex,age);//①初始化父类对象的属性
this.num = num;//②初始化子类对象自身的属性
}

可以看到,这个带有参数的构造方法中有两条语句:语句①是通过super关键字调用父类的构造方法,它的作用是初始化从父类继承过来的各项属性。语句②是通过一条赋值语句初始化子类自身的属性。这样,经过这两条语句的执行,就能对一个子类(Student)对象的所有属性完成初始化操作。

下面的【例05_03】演示了如何通过带有参数的构造方法创建并初始化一个Student类对象,并且在创建对象之后,调用它的introduce()方法把对象的各项属性值输出到控制台上。需要提醒读者,为了能够顺利运行【例05_03】,需要把上述带参数构造方法添加到Student类中,也可以从《范例5-3》中复制新的Student类源文件来替换工程中现有的Student类。

【例05_03 通过带有参数的构造方法创建子类对象并初始化其各项属性】

Exam05_03.java

public class Exam05_03 {
public static void main(String[] args) {
Student s = new Student("张三",'男',20,10001);
s.introduce();
}
}

【例05_03】的运行结果如图5-5所示:

第五章《类的继承》第2节:子类对象的构建过程

图5-5 【例05_03】运行结果

程序员定义一个子类时,需要注意一种特殊情况:假如父类中定义了有参数的构造方法,并且没有定义无参数的构造方法,在这种情况下,程序员必须为子类至少定义1个构造方法,并且在构造方法中手动调用父类的构造方法。如果没有按照这种方式定义子类,将会出现语法错误。

为了验证这个结论,各位读者可以暂时把示例代码中Person类中的无参数构造方法删掉,并且把Student类中的构造方法也删掉,删除完这些构造方法之后,Student类在IDEA 中将会出现如图5-6所示的语法错误:

第五章《类的继承》第2节:子类对象的构建过程

图5-6 Student类出现语法错误

从图5-6可以看出:Student类中仅有一个num属性,但结构如此简单的一个类却出现了语法错误,这是怎么回事呢?原因很简单:按照“先父后子”的原则,创建子类对象时需要先创建一个父类对象。创建父类对象时又需要调用父类的构造方法。在当前的子类(Student)中,并未定义任何构造方法,所以将由编译器为其自动添加一个构造方法,而在添加的构造方法中,编译器又要调用父类(Person)的构造方法。但编译器仅会调用父类无参数的构造方法,此时父类(Person)中仅定义了有参数的构造方法,并不存在无参数的构造方法,因此编译器根本调用不到这个无参数的构造方法,这导致无法创建出父类对象。无法创建出父类对象又会导致无法创建出子类对象,因此会出现语法错误。

消除这种语法错误的办法有两个:

  1. 在父类中添加一个无参数的构造方法,这样编译器就能调用到这个构造方法。
  2. 在子类中手动添加一个构造方法,并且在构造方法中的第一行手动调用父类的有参数构造方法。

“先父后子”的创建原则可以推广到多代类的继承关系中。也就是说:如果子类的父类之上还有“辈份”更高的类,那么仍然按照“先父后子”的原则,以类“辈份”从高到低的顺序完成对象的创建,这个过程如图5-7所示:

第五章《类的继承》第2节:子类对象的构建过程

图5-7 类的多代继承

在图5-7中,A是B的父类,B又是C的父类。如果创建一个C类对象,虚拟机会按照类“辈份”从高到低的顺序,首先创建一个A类的对象,在此基础上再创建出B类对象,最后创建出C类对象。

除此文字版教程外,小伙伴们还可以点击这里观看我在本站的视频课程学习Java。