计算机科学A复习:Unit 9 – 继承 Inheritance

计算机科学A复习:Unit 9 – 继承 Inheritance
这个单元介绍有关「Inheritance 继承」的语法。继承是 Java 编程的优势之一,在需要写作多个相似的 Class 时使用继承可以节省大量精力和时间。
例如,假设所有的「School」类型的 class 都有学校名称和人数两个变量;而「HighSchool」类型的 class 不仅有学校名称、人数两个变量,还有课程类型(用来区分 IB、AP、VEC 学校)。
这两个 class 仅有的区别是「HighSchool」类型的 class 除了学校名称和人数之外还有一个变量用来存储课程类型。按照原来的写法,虽然两个 class 区别不大,我们仍然需要分别在两个 class 内部完成变量声明。
而使用「Inheritance 继承」之后,只需要在「School」内部声明学校名称和人数这两个变量,然后写程序语句要求「HighSchool」继承「School」。此时再在「HighSchool」内部声明一个存储课程类型的变量即可。
读到这里你大概也许对「Inheritance 继承」有模糊的概念了。我们在下面就来看看具体的写作语法,通过程序示例来具体理解它的使用场景。
U9.1 Creating Superclasses and Subclasses 父类和子类
我们继续用刚才「School」和「HighSchool」的例子。上面我们我们提到了「HighSchool」是继承自「School」的,这是说「HighSchool」几乎拥有「School」的所有特征,包括 methods, variables 等等。
因此我们并不需要再次在「HighSchool」里面声明用于存储学校名称和人数的两个变量——这两个变量已经从「School」继承而来。接下来只需要声明「HighSchool」特有的用于存储课程类型的变量即可:
‘’’
class Main {//Main class 用来写一些输出测试的语句
public static void main (String[] args) {
School a = new School();
System.out.println(“输出测试 [School] \n”+ a.toString());//输出测试 School
HighSchool b = new HighSchool();
System.out.println(“\n输出测试 [HighSchool] \n”+ b.toString());//输出测试 HighSchool
}
}
class School {//这里是 School class
int population = 100;//人数
String schoolName = “schoolName”;//学校名称
public String toString() {//设置输出格式
return “人数:”+population +”\n学校名称:”+ schoolName;
}
}
class HighSchool extends School {//这是从 School class 继承而来的 HighSchool class
String type = “AP”;//课程类型
public String toString() {//设置输出格式
return “人数:”+ population +”\n学校名称:”+ schoolName +”\n课程类型:”+type;
}
}
‘’’
这段示例程序有三个 class。Main class 用来写输出测试语句,School class 以及 HighSchool class 都依照上面例子的要求写作。
声明 HighSchool class 的语句和声明一般的 class 的语句有一点点不一样。这里写的「class HighSchool extends School」,意思就是新建一个 class 名为「HighSchool」继承自「School」。「extends」关键字就是进行继承的指令,直接加在普通的 class 声明语句后即可, extends关键字后面要空格再写上被继承的 class 的名称。
那么,被继承的 class 就像某个遗传物质的来源(爹)一样,被称作「父类 Superclasses」;新建的继承自父类的 class 就被称作「子类 Subclasses」。
一般的,可以用「is-a」来表示父类和子类之间的隶属关系。例如,这里的「HighSchool」是「School」的子类,所以考试题目有可能表述为「HighSchool is a School」。
运行上面的示例程序,最终输出如下:
尝试在这段示例程序的基础上再自己尝试写一个「School」的子类「college」。除了学校名称、人数以外,college 还需要一个额外的变量用于存储学位类型。默认的学位类型值为 BSc 。并写一个 toString method用于设置输出格式。(稍后的 ### U9.7 会介绍 toString 的示例程序和写作逻辑)
U9.2 Writing Constructors for Subclasses
前面有描述道「HighSchool」是继承自「School」的,这是说「HighSchool」几乎拥有「School」的所有特征」。之所以说是「几乎」,是因为子类并不会继承父类的 constructor。但是,子类仍然可以以其他途径使用父类的 constructor,所以接下来要做的事情就是了解子类调用父类的几种不同情景。
首先,最简单的第一种场景。如果父类中存在一个不含 parameter 的 constructor,这个 constructor 就被称为 no-argument constructor。子类在运行之初会自动调用父类的 no-argument constructor 。例如:
‘’’
class Main {//Main class 用来写一些输出测试的语句
public static void main (String[] args) {
School a = new School();
System.out.println(“输出测试 [School] \n”+ a.toString());//输出测试 School
HighSchool b = new HighSchool();
System.out.println(“\n输出测试 [HighSchool] \n”+ b.toString());//输出测试 HighSchool
}
}
class School {//这里是 School class
int population;//人数
String schoolName;//学校名称
School(){//便于展示,这里用 constructor 来赋 population 和 schoolName 的初值
population = 100;
schoolName = “Shude High School”;
}
public String toString() {//设置输出格式
return “人数:”+population +”\n学校名称:”+ schoolName;
}
}
class HighSchool extends School {//这是从 School class 继承而来的 HighSchool class
String type = “AP”;//课程类型
public String toString() {//设置输出格式
return “人数:”+ population +”\n学校名称:”+ schoolName +”\n课程类型:”+type;
}
}
‘’’
观察这段这段代码,可见 School class 内部存在一个 no-argument constructor。此时 HighSchool class 虽然没有自己的 constructor 也并未直接继承 School class 的 constructor,但是可以调用存在于父类 School 中的 no-argument constructor。调用父类中的 no-argument constructor 是自动完成的,不需要写作任何额外的语句,这被称为 called implicitly 。
为了测试这段代码,我为 HighSchool class 创建了一个新的 instance,称为 b 。b 包含的 population 以及 schoolName 变量是继承自父类的,并没有被直接的赋过初值。但是 superclass(也就是 School)内部存在一个 no-argument constructor ,作用是给上述两个变量赋初值。所以,b 在被创建的瞬间电脑自动调用了存在于父类中的 no-argument constructor 给 population 和 schoolName 两个变量完成了赋值。最后 HighSchool class 的实例(instance)b 被输出为:
输出测试 [HighSchool]
人数:100
学校名称:Shude High School
课程类型:AP
第二种场景是父类有两个或多个 constructor 。在这个场景下,我们使用 super 关键字指定一个 constructor 来调用。就像调用同名的 method 一样,只需要通过 parameter 的格式就可以区分出你希望调用的是哪一个 constructor:
‘’’
class Main {//Main class 用来写一些输出测试的语句
public static void main (String[] args) {
School a = new School(233);//**这里填写 parameter 的格式会决定调用哪一个constructor
System.out.println(“输出测试 [School] \n”+ a.toString());//输出测试 School
HighSchool b = new HighSchool();
System.out.println(“\n输出测试 [HighSchool] \n”+ b.toString());//输出测试 HighSchool
}
}
class School {//这里是 School class
int population;//人数
String schoolName;//学校名称
School(int pop){//只有一个 parameter
population = pop;
schoolName = “Shude High School”;
}
School (int pop, String name){
population = pop;
schoolName = name;
}
public String toString() {//设置输出格式
return “人数:”+population +”\n学校名称:”+ schoolName;
}
}
class HighSchool extends School {//这是从 School class 继承而来的 HighSchool class
String type = “AP”;//课程类型
HighSchool (){//用平常的格式给 子类 写一个 constructor
super(666);//然后使用super关键字来调用父类已经写好的 constructor
}
public String toString() {//设置输出格式
return "人数:"+ population +"\n学校名称:"+ schoolName +"\n课程类型:"+type;
}
}
‘’’
阅读上面的示例程序发现,在父类School中存在两个 constructors,第一个只要求传入一个parameter,即人数;第二个要求传入两个parameters,包括人数以及学校名称。
这个时候看到 HighSchool class 的部分,需要用一般的格式为 HighSchool class 写一个constructor,然后在这个 constructor 的第一行写 super 关键字表示手动调用父类的constructor。此处 super 关键字后需要有一个括号用于传递参数,且这个括号内填入的参数格式会最终决定调用父类的哪一个constructor。例如,这个地方我只填了一个参数,代表人数为 666,所以电脑会自动对应找到只需要传入一个 parameter 的第一个constructor。
注意,如果父类中不存在 no-argument constructor,就必须使用 super 关键字手动调用一个 constructor 或者在父类中不使用任何 constructor,否则会报错。
尝试阅读整段示例程序,在草稿纸上写下你预判的输出内容。然后在自己的eclipse运行这段程序进行检查。
U9.3 Overriding Methods
前面已经介绍了怎么通过继承得到一个独立的、新的,但是又复刻了父类几乎所有特征的 subclass(子类)。既然 subclass 已经包含了从父类继承而来的 methods,那如果又在 subclass 的内部再书写一个定义相同但包含不同语句的 Method 会发生什么呢?
这段示例程序有两个 class,其中 a 是父类;b 是子类,继承自 a 。在上图的情景下,直接调用 b class 内部的 testPrint method,输出结果是「进行一个测试输出」 ,因为 testPrint method 也是从 class a 继承而来的一部分。
在这样的 class b 内部已经存在一个名为 testPrint 的、不需要传入参数的 method 时,再写一个定义完全相同但内部所含的程序语句不同的 Method 就是「Overriding Methods 覆写方法」。即在子类内部声明一个与父类中某个方法的名称相同、需要的参数也相同的方法,子类中这样的方法优先级高于从父类中继承而来的相同方法,所以会产生覆盖的效果。例如:
已知 b 中已有一个从 a 继承而来的不需要传入参数 testPrint method,我又在 b 内声明了一个名为 testPrint 且不需要传入参数的 method。此时我新建的 testPrint method 就会覆盖掉原来从 a 继承而来的 testPrint method,再运行程序输出就变成了「看看这次输出了什么呢」。
以及,你可能会发现进行覆写的时候 eclipse 最左侧的竖列会出现绿色三角形「▲」的警示标志。把鼠标放上去会有文字描述,提示对应位置上的 method 覆盖了其他的方法,如下:
U9.4 super Keyword
前面已经提到可以单独使用 super keyword 来调用存在于父类中的 constructors。其实 super keyword 的用法和 this 有相似之处,区别在于 this keyword 用于指代「当前语境下正在被操作的object」;而 super keyword 指代的则是当前 class 的父类。
使用 super keyword,就可以方便的在子类中方便的调用或访问其父类中的变量、方法、以及前面所述的 constructors。注意,调用 super keyword 的语句必须书写于子类的某个方法中:
上文的这段程序,a 是父类、b 是继承自 a 的子类。考试中有可能表述为「b is a a」。在 class b 中,我创建了一个名为 test 的方法用于测试。这个方法中仅包含一个输出语句,意图输出「super.a」的返回值。根据 super 的定义,我们可以理解「super.a」是在访问父类中名为 a 的变量,因此返回值应该是变量 a 所存储的数据,看上图第11行可知这个数据是整数100。
同样的,你还可以使用 super keyword 调用父类中的方法:
U9.5 Creating References Using Inheritance Hierarchies
一直以来我们创建 object 的时候写作的 reference type(左侧)和 object type(右侧) 都是一致的。比如:
(joyceclp, 2022)
其实在某些情况下也可以不一致,例如:
(joyceclp, 2022)
这是 Java 编程中使用的继承功能时产生的「Hierarchies 层次结构」带来的特性。如果 Person 作为 reference type, 则 object type 可以是 Person 自己或者它的任意子类。用上面图片作例子来解释,Person 是 Student 的父类,那么当 Person 作为左侧的 reference type 的时候,右侧可以有两种写法:1) 在右侧写上 Person,这种写法最常见,以往我们都会把左右的 type 写成一致的;2) 在右侧写 Person 的任意子类,这里就可以写 Student。
再到具体的程序写作里尝试理解一下:
‘’’
public class Main {
public static void main (String[] args) {
a test = new b();//reference type not the same as object type
test.testPrint();//测试输出
}
}
class a {
int number = 666;
public void testPrint() {
System.out.print(number);
}
}
class b extends a {
public void testPrint() {
number = 2333;
System.out.println(number);
}
}
‘’’
上面这段程序中有两个 class,其中「b is a a」(意为: b 是继承自a的)。观察程序可以了解到,class a 当中有一个整数类型的变量名为 number 默认值为 666;而 class b 继承自 a 完成创建之后则会初始化 number 的值为 2333。此时如果用「a test = new b( )」这样左右 type 不同的写法来建立 object,就会在运行时得到 2333 的输出,表明此时 object 实际上的内容来源是 class b。
U9.6 Polymorphism
「Polymorphism 多态」顾名思义,此处指的是调用同一个方法得到不同的操作。前面 ### U9.3 已经介绍过「Overriding 覆写」,在子类中改写从父类继承而来的已有的方法,从而使得调用子类和父类中同时存在的同名 method 的时候可以触发不同的命令。
这里就需要注意区分「Overloading 重载」与「Overriding 覆写」的区别。### U9.3中我们已经介绍过 Overriding,这是指在子类中重写某个继承自父类的 method。这个 method 在被 Override 后无论是名称还是要求传入的参数都不变,但是其中包含的程序语句内容可以随意变更。
而「Overloading 重载」这个概念之前没有出现过,但是「Overloading」这个操作本身的写法大家都已经在之前的学习中使用过。这是指同一个 class 中出现了多个名称相同、但要求传入的参数不同的 method。因此「Overloading」本质上是通过传入的参数来区分需要被执行的 method:
比如上图中的示例程序包含两个要求传入的参数不同、但名称都为 a 的 methods。在 main method 中我们首先创建了一个名为 test 的 object 用于实验。然后分别在两次调用 method a 的时候传入一个整数、一个字符。程序运行后观察输出内容的区别,可见根据传入的参数不同,实际被执行的语句也被分流到了各自适用的情况里 —— 传入了一个整数的第一次调用首先输出「这里调用的是 method a,只需传入一个整数」;传入了一个 char 字符的第二次调用再在其后输出了「这里调用的也是 method a,但需要传入的是一个字符」。这就是经典的「Overloading」。
不论是「Overloading 重载」还是「Overriding 覆写」,表面上都是通过调用同名的方法执行了不同的程序语句,被称为「Polymorphism 多态」。
U9.7 Object Superclass
在之前的程序写作中,我们已经使用到诸如「.equals」这样的方法。虽然「.equals」并没有在class中被定义,但仍然可以正常发挥作用。这是因为 java 已经提前将常用的功能预置进了「Object」class;「Object」class 是所有其他 class 的父类,因此在任何的class中都可以调用 「Object」class 提供的功能。
除了常见的「.equals」被用于判断两个 reference type的内容是否相同,还有「.toString」可以被用于把其他 reference types 的内容转换为 String 类型的数据并输出:
‘’‘
public class Main {
public static void main (String [] agrs) {
Integer a = 23333;//这是用于存储整数的 reference type
System.out.println(a.toString());//输出
}
}
’‘’
最终得到的输出结果是23333。但是,这里并非是简单的输出了 int 类型的数字,而是把 Integer 类型的 23333 转换为了 String 类型的 “23333” 再返回并输出。
以及,你还可以覆写「.toString」来满足不同的返回值格式要求。
总结
这一个单元介绍了把父类作为模板从而创建一个相似的子类的语法。这可以大幅提升编写程序的效率。
练习
2008-6
参考答案
选D。题干创建了一个名为 d1 的、类型为 Derived 的 Instance。观察给出的代码可以发现,Derived 类本身是继承自 Base。因此 Derived 除了要执行自身的两个大括号之间的内容,还要首先执行从父类继承而来的 Base method。所以第一个输出的内容是 Base,再接自身的两个大括号之间的输出内容,即 Derived。最终的输出内容就是「Base Derived」。
- Title: 计算机科学A复习:Unit 9 – 继承 Inheritance
- Author: Haowen Liang
- Created at : 2023-04-20 00:00:00
- Updated at : 2025-07-03 03:17:18
- Link: https://www.liang-haowen.online/d47b615b9c4c/
- License: This work is licensed under CC BY-NC-SA 4.0.