第5 章类的继承 课程练习 5.类的复用:组合与继承 1 1 类的复 5 用 . :组合与“复用”也被称作“重用”,是重复使用的意思,即将已有的软件元素使用在新的软件开发 继承中。这里所说的“软件元素”包括程序代码、测试用例、设计文档、设计过程、需求分析文档甚 至领域知识和经验等。通常,可重用的元素称作软构件。构件的大小称为构件的粒度,可重 用的软构件越大,重用的粒度越大。使用软件重用技术可以减少软件开发活动中大量的重 复性工作,这样就能提高软件的生产率,降低开发成本,缩短开发周期。由于软构件大多经 过严格的质量认证,并在实际运行环境中得到校验,重用软构件还有助于改善软件质量。 一般来说,软件重用可分为如下3个层次。 (1)知识重用(例如,软件工程知识的重用)。 (2)方法和标准的重用(例如,面向对象方法或国家制定的软件开发规范的重用)。 (3)软件成分和架构的重用 。 Java提供了实现代码重用的两种方式:组合以及继承 。 .组合:新的类由现有类的对象所组成。 .继承:按照现有类的类型派生出新类。 1.类的组合 5.1 如果一个类把另外一个类的对象作为自己的成员变量,即内嵌其他类的对象作为自己 的成员,称为类的组合。类的组合是实现软件重用的一种重要方式。组合表示类的对象之 间是“has-a”(有一个)的包含关系,即一类对象包含另一类对象,如,Ahousehasaroom 。 例如,需要计算圆柱的体积,计算公式如下 : 柱的体积=底面积× 高 如果一个类过于复杂,可以将其拆分成多个类,拆分成的类成为组合类的子对象。例 如,一个完整圆柱由底面和高组成,可以将底面的圆类拆分出去,通过在圆柱类中声明一个 圆类对象,将两个类组合起来。 1.关联关系的UML建模 圆类对象作为圆柱类的一个成员,两者的关系是关联关系,就是圆柱类(Pilar)关联于 圆类(Cice)。图5. larl rl1是Pir类和Cice类关联关系的UML描述。 如果A类中的成员变量是用B类(接口)声明的对象,那么A和B的关系是关联关系, 称A类的对象关联于B类的对象或A类的对象组合了B类的对象。如果A关联于B,那 么可以通过一条实线连接A和B的UML类图,实线的起始端是A的UML图,终点端是B 的UML图,但终点端使用一个指向B的UML图的方向箭头表示实线的结束。 2.类组合实现复用的代码设计 【代码5-1】类的组合 。 ·124 · 图5.1 Pillar类和Circle类关联关系的UML描述 Circle.java 1 public class Circle { 2 /**半径*/ 3 private double radius; 4 5 public Circle(double radius) { 6 this.radius =radius; 7 } 89 public double getRadius() { 10 return radius; 11 } 12 13 public void setRadius(double radius) { 14 this.radius =radius; 15 } 16 17 /**计算面积*/ 18 public double getArea() { 19 return Math.PI * radius * radius; 20 } 21 } Pillar.java 1 public class Pillar { 2 /**圆柱的底面对象*/ 3 private Circle bottomCircle; 4 /**圆柱的高*/ 5 private double height; 6 7 /**将Circle 类对象作为Pillar 类构造器的参数*/ 8 public Pillar(Circle bottomCircle, double height) { 9 this.bottomCircle =bottomCircle; 10 this.height =height; 11 } 12 13 public Circle getBottomCircle () { 14 return bottomCircle; 15 } 16 17 public void setBottomCircle (Circle bottomCircle) { 18 this.bottomCircle =bottomCircle; ·125· 19 } 20 21 public double getHeight() { 22 return height; 23 } 24 25 public void setHeight(double height) { 26 this.height =height; 27 } 28 29 /**计算体积*/ 30 public double getVolume() { 31 return bottomCircle.getArea() * height; 32 } 33 } TestPillar.java 1 public class TestPillar { 2 public static void main(String[]args) { 3 /**创建Circle 对象*/ 4 Circle bottomCircle =new Circle(1.5); 5 /**利用已有的Circle 对象创建Pillar 对象,以实现对象组合*/ 6 Pillar pillar =new Pillar(bottomCircle, 5.0); 7 System.out.println("圆柱的体积为: " +pillar.getVolume()); 8 } 9 } 代码5-1中定义Pillar类时使用了Circle类对象作为其成员,意味着一个Pillar对象包 含一个Circle对象。这样就允许在新类(Pillar类)中直接复用旧类(Circle类)的public方 法(如getArea()方法)。在使用时通过在Pillar类的构造器中传入Circle类对象来实现对 象的组合。 通过以上实例可以看出,组合就是把旧类(Circle类)对象作为新类的成员变量组合进来, 用以实现新类(Pillar类)的功能,用户看到的是新类(Pillar类)的方法(如getVolume()方法), 而不能看到被组合对象的方法(如getArea()方法)。因此,通常需要在新类里使用private修饰 被组合的旧类对象。利用组合来实现复用,是复用现有代码的功能,而非它的形式。 如果要计算圆锥的体积,则可继续将Circle对象作为圆锥(Cone)类的成员变量,达到 多次复用的目的。 5.1.2 类的继承 如果需要复用一个类,除了把该类当成另一个类的组合成分外,还可以把这个类当成基 类来继承进而派生出新的类。不管是组合还是继承,都允许在新类(对于继承是子类)中直 接复用旧类的方法。利用继承实现复用,是在不改变现有类的基础上,复用现有类的形式并 在其中添加新代码。 继承是所有OOP(面向对象的编程)语言,包括Java语言不可缺少的组成部分。面向 对象程序设计的核心是定义类。前面介绍的类是直接定义的,除此之外,还可以在已经定义 类的基础上定义新的类,由新的类继承已经定义类的部分代码实现部分代码的重用。本章 通过学生和研究生之间存在的继承,也称泛化关系,讨论一般继承(泛化)关系的Java描述 以及所引岀的有关问题。 ·126· 下面以从学生类派生出研究生类为例,介绍类继承的设计方法。 1.派生关系的UML建模 研究生也是学生,即研究生是学生的一部分,学生是研究生的抽象。这种关系在面向对 象的程序设计中用继承表示。对于本例,可以说是Student类派生出GradStudent类,也可 以说GradStudent类继承了Student类。这种基类和派生类的关系称为is-a(是一个、是一 种)关系,例如,研究生is-a学生。 图5.2是GradStudent类和Student类继承关系的UML描述。如果一个类是另一个 类的子类,那么UML通过使用一条实线连接两个类的UML图来表示两者之间的继承关 系,实线的起始端是子类的UML图,终点端是父类的UML图,但终点端使用一个空心的 三角形表示实线的结束。 图5.2 类的继承关系 2.由Student类派生GradStudent类的代码设计 Java类继承的语法格式为: [修饰符]class 子类名称extends 父类名{ } 修饰符:可选,用于指定类的访问权限,可选值为public、abstract和final。 【代码5-2】 GradStudent类继承Student类的Java代码。 Student.java 1 /**学生类*/ 2 public class Student { 3 /**学号*/ 4 private long studentID; 5 /**学生姓名,使用了protected */ 6 protected String studentName; ·127· 78 /**构造器*/ 9 public Student(long studentID, String studentName) { 10 this.studentID =studentID; 11 this.studentName =studentName; 12 } 13 14 public long getStudentID() { 15 return studentID; 16 } 17 18 public void setStudentID(long studentID) { 19 this.studentID =studentID; 20 } 21 22 public String getStudentName() { 23 return studentName; 24 } 25 26 public void setStudentName(String studentName) { 27 this.studentName =studentName; 28 } 29 30 /**输出信息*/ 31 public void print() { 32 System.out.println("学号: " +studentID); 33 System.out.println("姓名: " +studentName); 34 } 35 } GradStudent.java 1 /**研究生类*/ 2 public class GradStudent extends Student { 3 /**导师姓名*/ 4 private String tutorName; 5 /**研究方向*/ 6 private String resDirection; 7 8 /**构造器*/ 9 public GradStudent (long studentID, String studentName, String tutorName, String resDirection) { 10 //调用父类构造方法 11 super(studentID, studentName); 12 this.tutorName =tutorName; 13 this.resDirection =resDirection; 14 } 15 16 public String getTutorName() { 17 return tutorName; 18 } 19 20 public void setTutorName(String tutorName) { 21 this.tutorName =tutorName; 22 } 23 24 public String getResDirection() { 25 return resDirection; 26 } 27 28 public void setResDirection(String resDirection) { ·128· 29 this.resDirection =resDirection; 30 } 31 32 /**输出信息*/ 33 public void print() { 34 System.out.println("学号: " +this.getStudentID()); 35 System.out.println("姓名: " +studentName); 36 System.out.println("导师姓名: " +tutorName); 37 System.out.println("研究方向: " +resDirection); 38 } 39 40 public void doResearch() { 41 System.out.println(this.getStudentName() +" is doing research"); 42 } 43 } TestExtends.java 1 /**测试类*/ 2 public class TestExtends { 3 /**主方法*/ 4 public static void main(String[]args) { 5 Student st =new Student(123456, "王舞"); 6 st.print(); 7 GradStudent gs =new GradStudent(654321, "李司", "张伞", "人工智能"); 8 gs.print(); 9 } 10 } 程序运行结果: 学号: 123456 姓名: 王舞 学号: 654321 姓名: 李司 导师姓名: 张伞 研究方向: 人工智能 说明: (1)关键字extends表示扩展或派生,即以一个类为基础派生出一个新类。这个新生成 的类称为派生类或直接子类;原始的类作为派生类形成的基础存在,称为基类,也称为派生 类的超类或父类。在本例中,classGradStudentextendsStudent表明GradStudent类是以 Student为基类扩展而成的派生类。派生类也可以继续扩展成新的派生类。 从另一方面看,extends关键字使派生类继承了基类的属性和方法,因此派生类无法脱 离基类而存在。 (2)子类会继承父类的所有属性和方法(静态成员、构造方法除外)。但是,对于父类的 私有属性和方法,子类是无法访问的(只是拥有,但不能使用)。虽然子类不能直接访问从父 类继承过来的私有属性,但是可以通过父类提供的public访问器间接访问。从父类继承过 来的成员,会保持其原有的访问权限和功能,它可以被子类中自己定义的任何实例方法 使用。 (3)父类中的静态成员虽然未被子类继承,但可以通过子类名或子类对象访问它们。 可以认为,父类的静态成员被父类及其所有子类对象所共享。 (4)注意,在类Student中成员studentName改用protected修饰,而不是用private修 ·129· 饰。因为private将所修饰的成员的访问权限限制在本类中(即不允许子类直接访问父类的 private成员),而protected允许将所修饰成员的访问权限扩展到派生类中。这样,父类的 studentName成员才能被子类GradStudent中的print()方法直接访问(当然,也可用 getStudentName()方法访问),见GradStudent类代码的第35行。子类GradStudent中的 print()方法通过调用父类提供的getStudentID()来访问父类的private成员studentID,见 GradStudent类代码的第34行。一般情况下,父类数据成员的访问权限都设置为private, 是否要设置成protected,视具体情况而定。这里将studentName的修饰符设为protected, 只是为了说明子类对父类成员的访问情况。 (5)继承表明了两个类之间的父子关系,让父类和子类之间建立起了联系,子类自动拥 有父类的成员(静态成员、构造方法除外),包括成员变量和成员方法,使父类成员得以传承 和延续(如本例中的studentName和studentID);子类可以重新定义(重写)父类的成员,使 父类成员适应新的需求(例如,子类GradStudent的print()方法对父类的print()方法进行 了重写/覆盖,方法覆盖的内容在后续章节介绍);子类也可以添加新的成员,使类的功能得 以扩充(例如,子类GradStudent新增了数据成员tutorName、resDirection,新增了方法成员 doResearch())。但是,子类不能删除父类的成员。 (6)利用继承,可以先编写一个具有共有属性和方法的父类(如Student类),根据该父 类再编写具有特殊属性和方法的子类(如GradStudent类),子类继承父类的状态和行为,并 根据需要增加它自己的新的状态和行为,也可以修改从父类继承过来的行为。本科生也是 学生,这样,可以在Student类的基础上再派生本科生Undergraduate类,代码如下。 1 /**本科生类*/ 2 public class Undergraduate extends Student { 3 /**专业名称*/ 4 private String major; 5 6 /**带参构造器*/ 7 public Undergraduate(long studentID, String studentName, String major) { 8 super(studentID, studentName); 9 this.major =major; 10 } 11 12 public String getMajor() { 13 return major; 14 } 15 16 public void setMajor(String major) { 17 this.major =major; 18 } 19 20 public void print() { 21 super.print(); 22 System.out.println("专业名称: " +major); 23 } 24 } 目前,学生类派生出了本科生类和研究生类,学生类是本科生类和研究生类的共同 父类。 3.Java继承规则 Java语言的继承有如下特征。 ·130· (1)每个子类只能有一个直接父类(不允许多重继承),但一个父类可以派生出多个子 类,见图5.3(a)。 (2)派生具有传递性。Java允许多层继承,如果类A 派生了类B,类B又派生了类C, 则C不仅继承了B,也继承了A,见图5.3(b)。这样,一个类就能拥有多个间接父类。 (3)不可循环派生。若A派生了类B,类B又派生了类C,则类C不可派生A,见图5.3(c)。 图5.3 Java继承规则 (4)组合优先。就是能使用组合就尽量使用组合,而不使用继承。 5.1.3 super关键字 super是Java的一个关键字,它有两种用法。 1.用super调用父类(对象)的可见成员 可用super访问父类的可见数据成员。例如,GradStudent类的print()方法访问 studentName也可用super.studentName进行访问。 【代码5-3】 GradStudent类的print()方法用super访问父类的可见数据成员。 1 public void print() { 2 System.out.println("学号: " +this.getStudentID()); 3 System.out.println("姓名: " +super.studentName); 4 System.out.println("导师姓名: " +tutorName); 5 System.out.println("研究方向: " +resDirection); 6 } 可用super调用父类的可见方法成员。调用父类方法的格式为: super.方法名(参数) 例如,可将GradStudent类的print()方法进行如下改造,用super.print()调用父类的 print()方法。 【代码5-4】 GradStudent类的print()方法用super调用父类的可见方法成员。 1 public void print() { 2 //用super 调用父类的方法 3 super.print(); 4 System.out.println("导师姓名: " +tutorName); 5 System.out.println("研究方向: " +resDirection); 6 } 2.用super()代表父类构造器 父类的构造方法不能被子类继承,它们被显式或隐式地调用。使用super关键字显式 ·131· 调用父类的构造方法。与this()一样,它必须放在调用函数中的第1行,即当调用派生类的 构造器实例化时首先要调用基类的构造器对从基类继承的成员进行实例化,再对本类新增 成员进行实例化。调用父类构造方法的格式为: super()或者super(参数) 注意:要调用父类构造方法就必须使用关键字super,而且这个调用必须是构造方法 的第一条语句。 代码5-2中GradStudent类的第11行就是用super显式调用了父类的构造方法,如下: //调用父类构造方法 super(studentID, studentName); 注意: (1)与this()不同,使用super()必须为所调用成员的访问权限允许,否则无法调用。 (2)this()和super()不能同时存在,因为都要在第一行。 5.1.4 继承关系下的构造方法调用 在Java中构造一个类的实例时,将会调用沿着继承链的所有父类的构造方法。当构造 一个子类的对象时,子类构造方法会在完成自己的任务之前,首先调用它的父类的构造方 法。如果父类继承自其他类,那么父类构造方法又会在完成自己的任务之前,调用它自己的 父类的构造方法。这个过程持续到沿着这个继承体系结构的最后一个构造方法被调用为 止。这就是构造方法链。 如果父类没有定义构造方法,则调用编译器自动创建的不带参数的默认构造方法。如 果父类定义了public的无参的构造方法,则在调用子类的构造方法前会自动先调用该无参 的构造方法。如果父类只有有参的构造方法,没有无参的构造方法,则子类必须在构造方法 中显式调用super(参数列表)来指定某个有参的构造方法。如果父类定义有无参的构造方 法,但无参的构造方法声明为private,则子类同样必须在构造方法中显式调用super(参数 列表)来指定某个有参的构造方法。如果父类没有其他的有参构造方法,则子类无法 创建。 如果没有显式地调用父类构造方法,编译器会自动地将super()作为子类构造方法的 第一条语句。例如,代码5-5中Student类的无参构造器24行之前,编译器会自动加上一条 语句super();。但是,其有参构造器的第30行之前就不会自动加上此条语句。因为super() 与this()不能同时存在。所以,以下两段代码等价。 public Student() { System.out.println("(2) Student 无参构造器"); } 等价于: public Student() { super(); System.out.println("(2) Student 无参构造器"); } ·132· 代码5-5是Student-GradStudent类层次中的构造方法链演示。代码较之前有所精简, 为了说明问题引入了另一个类Person,让Student类继承自Person类。 【代码5-5】 在Student-GradStudent类层次中的构造方法链。 1 /**测试类*/ 2 public class TestExtends { 3 /**主方法*/ 4 public static void main(String[]args) { 5 GradStudent gs =new GradStudent(654321, "李司", "张伞", "人工智能"); 6 } 7 } 89 /**Person 类*/ 10 class Person { 11 /**无参构造器*/ 12 public Person() { 13 System.out.println("(1) Person 无参构造器"); 14 } 15 } 16 17 /**学生类*/ 18 class Student extends Person { 19 private long studentID; 20 private String studentName; 21 22 /**无参构造器*/ 23 public Student() { 24 System.out.println("(2) Student 无参构造器"); 25 } 26 27 /**带参构造器*/ 28 public Student(long studentID, String studentName) { 29 //调用本类另一构造器 30 this(); 31 this.studentID =studentID; 32 this.studentName =studentName; 33 System.out.println("(3) Student 带参构造器"); 34 } 35 } 36 37 /**研究生类*/ 38 class GradStudent extends Student { 39 private String tutorName; 40 private String resDirection; 41 42 /**带参构造器1 */ 43 public GradStudent(long studentID, String studentName) { 44 //调用父类构造方法 45 super(studentID, studentName); 46 System.out.println("(4) GradStudent 带参构造器1"); 47 } 48 49 /**带参构造器2 */ 50 public GradStudent (long studentID, String studentName, String tutorName, String resDirection) { 51 //调用本类类另一构造器 52 this(studentID, studentName); 53 this.tutorName =tutorName; ·133·