第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·