计算机科学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」继承自「Schoolextends关键字就是进行继承的指令,直接加在普通的 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

by Oscar.L
E-mail [email protected]
No Comments

Send Comment Edit Comment

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
Previous
Next