第5章 继 承
继承是面向对象程序设计(ObjectOrientedProgramming,OOP)的三大特征之一,描
述了类不同抽象级别之间的关系:“isa”的关系,即“特殊与一般”的关系。换句话说,一般
(父类)是特殊(子类)更高级别的抽象。子类可以继承父类所有的非private类型的属性和
方法,也可以具有自己独有的属性和方法。通过类的继承关系,使公共的特性能够共享,提
高了软件的重用性。但在Java中只允许单继承。
5.1 继承的语法
在Java中描述两个类之间的继承关系时,使用关键字extends,格式如下: 
class SubClass extends SuperClass{ 
… 
} 
其中,SubClass为子类,SuperClass为父类(或超类)。
在第4章中定义了Person类: 
class Person { 
private String name; 
private char sex='M'; 
Person(String name){ 
this.name=name; 
} 
Person(String name,char sex) { 
this.name=name; 
this.sex=sex; 
} 
public void show() { 
String str="下面展示姓名和性别"; 
System.out.println(str); 
System.out.println("姓名: "+name+" 性别: "+sex); 
} 
}
现在要定义一个学生类(Stu),由于“学生是人”,所以学生类和Person类之间是“isa” 
的关系,即“继承”关系,那么就可以按例5.1的方法定义Stu类。
例5.1 Stu类的定义(ch05\Stu.java)。 
class Stu extends Person { 
long id; 
·105· 

private String name; //仅为演示用,实际编程中无须声明该变量 
private char sex='M'; 
public Stu (String name, long id, char sex){ 
super(name,sex); 
this.id=id; 
} 
}
前面讲过子类可以继承父类的非private类型的属性和方法,在这个例子中可以看到: 
虽然在Person类中定义了name、sex属性,但它们是private类型的数据,如果Stu也想拥
有这些属性,就必须重新定义,不能继承于父类,也可以定义这些属性为protected;但show 
方法在Person类中是以public的身份定义的,所以Stu类虽然没有显式地定义该方法,却
拥有该方法,因为它继承了父类的show()方法,编写测试类如下: 
class UseStu { 
public static void main(String[] args) { 
Stu s=new Stu("王强",20094140213L,'M'); 
s.show(); 
} 
}
运行结果: 
下面展示姓名和性别
姓名: 王强性别: M 
另外,还可以在子类中定义子类独有的属性和方法,例如本例中的id。
在这里需要说明以下几点。
(1)父类的构造函数不能被子类继承。
(2)子类不能继承或访问父类中的private属性和方法。
(3)父类中的friendly(包访问权限)的属性和方法只有在父类和子类在同一包中时才
能被子类继承和访问。
(4)父类中由protected或public修饰的属性和方法,都可以被子类继承访问(无论子
类是否与父类在同一包中)。
5.2 成员变量的隐藏和方法的覆盖
当子类和父类中定义的成员变量的名字相同时,子类可以隐藏父类的成员变量。同样, 
子类也可以通过方法重写(或称为覆盖,Overriding)来隐藏从父类继承的方法。方法覆盖
是指子类中定义的方法的头部(方法的名字、返回类型、参数个数和类型)与父类的方法完全
相同,而方法体可以不同。但在进行方法覆盖时要注意:在覆盖时访问权限只能放大或相
同,不能缩小;覆盖方法不能抛出新的异常(关于异常,将在第8章介绍)。
例5.2 成员变量的隐藏和方法的覆盖(ch05\OverriddingTest.java)。 
class Father { 
·106·

String s="Father"; 
int i=1; 
public void f() { 
System.out.println("Father s="+s); 
System.out.println("Father i="+i); 
} 
}c
lass Child extends Father { 
String s="Child"; //隐藏了父类的成员变量s 
public void f(){ //覆盖了父类的f()方法,但访问权限只能是public 
System.out.println("Child s="+s); 
System.out.println("Child i="+i); 
} 
}c
lass OverriddingTest { 
public static void main(String[] args) { 
Father f=new Father(); 
Child c=new Child(); 
f.f(); 
c.f(); 
} 
}
运行结果: 
Father s=Father 
Father i=1 
Child s=Child 
Child i=1 
方法覆盖与方法重载的区别如下:方法覆盖发生在父类和子类之间,即子类重写了父
类的某个方法,子类中定义的方法的头部(方法的名字、返回类型、参数个数和类型)与父类
的方法完全相同,而方法体可以不同;重载是在同一类中出现的现象,是指一个类中可以有
多个方法具有相同的名字,但这些方法的参数不同。
5.3 super 
如果想在子类中使用父类的非private类型的变量和方法(特别是被隐藏的变量和方
法),可以使用super关键字。例如,要在例5.2中访问父类的变量s,就要使用super.s,试验
在子类Child的f()方法中加入如下的代码,查看输出结果。 
System.out.println("Father s="+super.s); 
super.f(); 
如果要在子类的构造方法中访问父类的构造方法,也要使用super关键字,例如例5.1 
中的super(name,sex);但要注意,该调用语句必须出现在子类构造方法非注释语句的第
·107·

一行。注
意:如果在子类的构造方法中没有使用super调用父类的构造方法,编译器将自动
添加 
super(); 
即调用父类不带参数的构造方法,此时就应保证父类中有不带参数的构造方法(当父类未定
义任何构造方法时,系统会自动合成;一旦父类定义了一个或多个构造方法,系统将不再提
供默认的构造方法,必须手工定义),否则就会产生错误。如例5.1中,由于父类Person未
定义不带参数的构造方法,所以必须用super(name,sex)显式地调用父类中某个已定义的
构造方法。
5.4 final和sealed 
1.final关键字
final关键字可以用来修饰类、方法、变量(包括成员变量和局部变量及方法中的参数)。
(1)当final修饰类时,意味着该类不能被继承,即该类不能有String类等子类。
例5.3 final修饰类(ch05\FinClass.java)。 
final class FinClass { //最终类 
int i; 
FinClass() { 
System.out.println("This is a final class."); 
} 
}c
lass SubFinClass extends FinClass { //错误,不能从最终类继承
}
编译时会出现下面的出错信息: 
FinClass.java:7: 无法从最终类继承
class SubFinClass extends FinClass { 
(2)当final修饰方法时,代表该方法不能被重写。
(3)当final修饰成员变量时,该变量可以理解为常量,必须赋以初值(可在声明时赋
值,或在类的构造方法中赋值),并且该变量的值不能再改变;当final修饰局部变量时,该局
部变量只能被赋一次值;当final修饰方法中的参数时,该参数的值不能被改变。
例5.4 final修饰方法和变量(ch05\UseFinal.java)。 
class UseFinal { 
final int i=1; 
final int j; //最终变量若不在声明时赋值,就要在其所属的类的构造方法中赋值 
int k; 
UseFinal() { 
j=2; 
·108·

} 
final void f(){ //最终方法,在子类中不能被覆盖 
System.out.println("This is a final method."); 
} 
void g() { 
//i++;错误,不能重新指定最终变量的值 
//j++;错误,不能重新指定最终变量的值 
k++; 
final String s="Hello "; 
//s="Hi";错误,当final 修饰局部变量时,该变量只能被赋一次值 
final String str; 
str="Java"; 
System.out.println(s+str+" i="+i+" j="+j+" k="+k); 
} 
void h(final int a) { 
//a++;错误,不能指定最终参数 
System.out.println("a="+a); 
} 
public static void main(String[] args){ 
UseFinal uf=new UseFinal(); 
uf.f(); 
uf.g(); 
uf.h(100); 
} 
} 
2.sealed类
通过final关键字,使得一些类不能被继承,这在一定程度上对继承关系进行了限制,优
化了代码重用。但在有些时候,可能只要求某些被指定的子类,才能从一个类进行继承,即
部分地限制类的继承,这就要用到sealed关键字。被sealed修饰的类称为密封类,它只允
许被指定的类继承。
可以在class和interface之前使用sealed修饰符。由sealed定义的密封类必须定义子
类。子类可以用permits显式指出;也可以不指定,而与父类定义在同一个Java源文件中。
sealed类的子类必须由sealed、non-sealed、final中的一个关键字修饰,即sealed类的子类可
以是密封类、非密封类或最终类。
如下为定义在同一个文件中的例子,通过这种方式可定义哪些类可以被继承: 
public sealed class SealedClassDemo { 
final class ASon extends SealedClassDemo{} 
}f
inal class BSon extends SealedClassDemo{} 
如下用permits显式定义可以继承的子类,这时可以不用写在同一个文件中: 
sealed class SealedClassDemo2 permits CSon,DSon{} 
non-sealed class CSon extends SealedClassDemo2{} 
·109·

final class DSon extends SealedClassDemo2{} 
5.5 多 态
多态是OOP的三大特征之一,此处结合5.4节讲述的覆盖来理解多态的含义。当一个
类(如Instrument类)有多个子类(Wind、Percussion、Stringed),并且这些类都重写了父类
中的某个方法(voidplay()方法)时,如图5.1所示,根据前面所讲的内容,下面的代码很容
易理解: 
图5.1 Instrument及其子类 
Wind w=new Wind(); //产生Wind 类的对象
w.play(); //调用Wind 类中的play 方法
Percussion p=new Percussion(); //产生Percussion 类的对象
p.play(); //调用Percussion 类中的play 方法
Stringed s=new Stringed(); //产生Stringed 类的对象
s.play(); //调用Stringed 类中的play 方法
那么下面的代码又如何理解呢? 
Instrument insw=new Wind(); 
insw.play(); 
把Wind类的对象赋值给Instrument 类型的变量(insw)对吗? insw 调用的是
Instrument类中的play方法还是Wind类中的play方法? 
子类和父类之间的关系是“isa”的关系,即“特殊与一般”的关系,可以说管乐器(Wind) 
是乐器(Instrumen),打击乐器(Percussion)是乐器(Instrument),弦乐器(Stringed)是乐器
(Instrument),所以下面的代码是正确的: 
Instrument insw=new Wind(); 
Instrument insp=new Percussion(); 
Instrument inss=new Stringed(); 
这就是常说的向上转型(upcasting)。向上转型后的对象(简称上转型对象),如insw、insp、
inss。例如play(),在调用方法时,其实调用的仍是子类中所重写的play方法,而不是父类
的play方法。这是因为Java对Override方法调用采用的是运行时绑定,也就是按照对象
的实际类型来决定调用的方法,不是按照对象的声明类型来决定调用的方法。但Overload 
方法则相反,在编译时已经进行了方法绑定,按照对象的声明类型决定调用的方法。
例5.5 多态示例(ch05\Music.java)。
·110·

class Instrument { 
public void play() { 
System.out.println("Instrument.play()"); 
} 
}c
lass Wind extends Instrument { 
public void play() { 
System.out.println("Wind.play()"); 
} 
}c
lass Percussion extends Instrument { 
public void play() { 
System.out.println("Percussion.play()"); 
} 
}c
lass Stringed extends Instrument { 
public void play() { 
System.out.println("Stringed.play()"); 
} 
}p
ublic class Music { 
static void tune(Instrument i) { 
i.play(); 
} 
public static void main(String[] args) { 
Instrument[] ins=new Instrument[3]; 
int i=0; 
ins[i++]=new Wind(); 
ins[i++]=new Percussion(); 
ins[i++]=new Stringed(); 
for (i=0; i<ins.length; i++) 
tune(ins[i]); 
} 
}
运行结果: 
Wind.play() 
Percussion.play() 
Stringed.play() 
那么能否将父类的对象赋值给子类类型的变量呢? 答案是否定的,除非进行强制类型转换。
例如: 
Wind w=new Instrument(); //错误
Wind w=(Wind) new Instrument(); //正确
·111·

1.类型判断操作符
要进行强制类型转换,往往需要先知道变量的具体类型。instanceof是Java中用来判
断变量类型的操作符,经常用于判断对象类型。代码如下: 
class Animal{} 
class Mammal extends Animal{} 
class Fish extends Animal{} 
public class Zoo { 
public static void main(String args[]) { 
Animal a=new Mammal(); 
if (a instanceof Animal) 
System.out.println("Animal"); 
if (a instanceof Mammal) 
System.out.println("Mammal"); 
if (a instanceof Fish) 
System.out.println("Fish"); 
} 
}
编译、运行上述代码,会根据对象a的类型,进行输出: 
Animal 
Mammal 
由结果可以看出,对象a既属于Animal类型,也属于Mammal类型。
在JDK16中,增强了instanceof操作符的功能,在使用instanceof进行类型判断的同
时,生成对应类型的模式匹配变量,将类型判断、类型转换和变量声明写到一条语句,精简了
代码。如下代码将num 类型判断、类型转换、变量a的声明和赋值简化为if括号中的一条
语句,在if语句块中,可以正常使用Integer变量a。 
Object num=15; 
if (num instanceof Integer a){ 
System.out.println("a=" +(++a)); 
}
运行上述代码片段会输出: 
a=16 
结合例5.5中的Instrument和Wind类,可以使用instanceof操作符进行类型判断和模
式变量声明,代码如下: 
Instrument inst=new Wind(); 
inst.play(); 
if (inst instanceof Wind wind){ 
wind.play(); 
} 
·112·

上述代码中,instanceof语句相当于进行了强制类型转换,即将父类类型又转换成子类
类型。运行上述代码片段会输出: 
Wind.play() 
Wind.play() 
Java是面向对象的语言,一般而言,子类会对父类进行扩展。将子类对象赋值给父类
对象变量,其原有的对象成员依然会存在;反之,会存在一定问题。
2.模式变量的可见范围
instanceof模式变量的有效范围仅为instanceof判断为true的范围,instanceof判断为
false的范围内模式变量不可见。例如以下代码: 
1 public static void test2() { 
2 Object num=15; 
3 if (!(num instanceof String s)) { 
4 return; 
5 } 
6 System.out.println(s); 
7 } 
8 public static void test3() { 
9 Object num=15; 
10 if (!(num instanceof String s)) { 
11 System.out.println("not String"); 
12 } 
13 System.out.println(s); 
14 } 
编译时test2方法中无错误,test3方法中提示第13行s不可解析。
test2方法,在if语句块中,instanceof判断为false,不能使用模式变量s。在第6行,因
为在第4行instanceof判断为false时已经是return,所以第6行的代码只有在instanceof 
判断为true时才能执行,因此可以访问模式变量s。
test3方法和test2方法非常类似,区别仅在第4行和第11行,第4行是在instanceof判
断为false时返回,第11行为显示信息。接下来,不管instanceof判断结果为true还是
false,都会执行第13行的代码,而模式变量仅在instanceof判断为true时可见,所以第13 
行是不能访问模式变量s的,编译错误。
利用可见范围可以写出简洁的代码,如下为判断num 是否大于2的Integer: 
if (num instanceof Integer a&&a>2) { 
System.out.println("a=" +a); 
}
注意,仅从语法上讲,以上代码中的“&&”不能换成“|”。对于numinstanceofInteger 
a|a>2,由于“|”表示或,所以当instanceof判断为false时,模式变量a在后一个条件不可见。
如果仅需要判断结果,可以写为如下形式: 
return num instanceof Integer a&&a>2; 
·113·

5.6 继承与组合
通过使用继承,提高了类的可重用性,减少了代码的重复书写,提高了效率。除了继承
这种方式外,还可以通过组合的方式来重复使用类。所谓组合,就是在一个新类中创建已有
类的对象,即新类由已有类的对象组成。例如,下面的学生成绩管理程序,Score类中使用
了已有类(Student类和Course类)中的对象,所以这种重复使用类的方式就是组合。
例5.6 组合示例,学生成绩管理程序(ch05\StuApp.java)。 
package app; 
//学生类
class Student { 
String name; 
long id; 
public Student() { 
} 
public Student(String name,long id){ 
this.name=name; 
this.id=id; 
} 
} //课程类
class Course { 
long id; 
String name; 
Course(long id, String name){ 
this.id=id; 
this.name=name; 
} 
} //成绩类
class Score { 
Student stu; 
Course course; 
double grade; 
Score(Student stu,Course course,double grade){ 
this.stu=stu; 
this.course=course; 
this.grade=grade; 
} 
} //应用类
class StuApp { 
private static Course[] courses=new Course[5]; 
·114·