第5 章 Java继承与高级特性 继承是一种基于已有类创建新类的机制,利用继承可以先创建一个具有广泛意义的类,然 后通过派生创建新类,并添加一些特殊的属性和行为。类的继承是实现代码复用最有效的方 法。本章将以类的继承为基础并逐渐展开,介绍若干类的高级特征: (1)继承的实现。 (2)方法的重写。 (3)抽象类与抽象方法。 (4)接口。 (5)内部类。 (6)Lambda表达式。 (7)泛型。 (8)Java反射机制。 (9)注解。 5.1 继承使用初探 当新建一个类时,也许会发现该类与之前的某个类非常相似,如绝大多数的属性和行为都 相同。这时,可以选择复制原类中的语句,对其部分修改后加入新类中,但这意味着必须同时 维护两个相似的Java程序。 另外一种方法是通过继承,让新类自动获得被继承类中已有的属性和方法,同时添加原类 中没有的属性和方法即可。例如,根据人的特征定义类Person如下: public class Person { private String name; private int age; public void say(){ System.out.println(name+"can say"); } public void setName(String name) { this.name = name; } Java 继承与高级特性 第5章 7 5 public String getName(){ return name; } } 该类对所有人均适用,但如果根据学生的特点需要定义一学生类,则可以肯定的是学生类 中,除了姓名、年龄属性外,还可能有所在学校名称(这是一般人没有的),此外学生的行为中还 包括在校学习(study)这一方法(这也是一般人没有的)。因此,可以通过继承的方式建立类 Student如下: public class Student extends Person{ String schoolname; //增加新的属性schoolname public void study(){ //增加新的方法study System.out.println("I am studying in school"); } public static void main(String[] args) { Student student1 = new Student(); student1.name="MingM";student1.age=10;/ /引用继承自父类的变量 student1.schoolname="CQ"; student1.say(); //调用继承自父类的方法 student1.study(); //调用子类新增加的方法 System.out.println("My name is "+student1.name); System.out.println("My schoolname is "+student1.schoolname); } } 程序运行结果: I can say I am studying in school My name is MingM My schoolname is CQ 分析:通过关键字extends定义了类Person的子类Student,然后添加了只有学生才 有的属性schoolname和方法study()。 在main()方法中,可以看到尽管Student中没有定义变量name、age以及方法say(),但 是子类却可以通过继承的方式自动取得,并像访问自己的成员变量和方法一样引用即可。 5.2 类的继承 5.2.1 继承的实现 继承的实现其实非常简单,其格式如下: class 子类名extends 父类名{ 类体 } extends是关键字,后跟父类的类名,如果没有父类,则缺省父类是java.lang.Object。Java 只支持单继承,即只能有一个父类,但类之间的继承可以具有传递性。 子类可以通过继承自动获得父类中访问权限为public、protected、default的成员变量和方 Java 程序设计(第3 版) 7 6 法,但不能继承权限为private的成员变量和方法。 【例5.1】 类的继承示例。 package code0502; class A { int i; void showi() { System.out.println("i: " + i); } }c lass B extends A { int k; void show() { System.out.println("k: " + k); showi(); } void sum() { System.out.println("i+k: " + (i + k)); } }p ublic class Simple { public static void main(String args[]) { A superOb = new A(); B subOb = new B(); superOb.i = 10; //对父类对象的成员赋值. System.out.println("Contents in 父类: "); superOb.showi(); subOb.i = 7; //对子类中继承得到的变量i 赋值 subOb.k = 9; System.out.println("Contents in 子类: "); subOb.show(); System.out.println("Sum of i and k in 子类:"); subOb.sum(); } } 程序运行结果: Contents in 父类: i: 10 Contents in 子类: k: 9 i: 7 Sum of i and k in 子类: i+k: 16 分析:类B中虽然没有定义变量i和方法showi(),但却可以通过继承关系获得,因此 在类B中直接引用i和方法showi()是正确的。但如果在类A 的语句inti前加上修饰符 private,则上述程序会出现编译错误,其原因在于类B无法继承到private类型的变量i,因此i 对类B是不可见的。 提示:①尽管一个子类可以从父类继承所有允许的方法和变量,但它不能继承构造函 数,掌握这点很重要。一个类要得到构造函数,只有两个办法:重写构造函数;根本不写构造 Java 继承与高级特性 第5章 7 7 函数,这时系统为每个类生成一个缺省构造函数。②为了防止继承被滥用,Java15引入了一 种特殊的密封类(使用sealed修饰class),并通过permits明确写出能够从该class继承的子类 名称,如publicsealedclassShapepermitsCircle{},这表明只允许类Circle继承Shape。 5.2.2 继承与重写 在类的继承过程中,如果子类中新增的变量和方法与父类中原有的数据和方法同名,则会 重写(也称覆盖)从父类继承来的同名变量和方法。重写又分变量重写和方法重写,变量重写 是指父类和子类中的变量名相同,数据类型也相同。方法重写与之前介绍的方法重载相似,但 更严格,不仅要求父类与子类中的方法名称相同,而且参数列表也要相同,只是实现的功能 不同。 【例5.2】 重写父类中的同名方法和变量。 package code0502; class SuperCla { int a = 3, b = 4; void show() { System.out.println("super result=" + (a + b)); } }c lass SubCla extends SuperCla { int a = 10; //重写父类中同名的变量a void show() { //重写父类中同名的方法show int c = a * b; System.out.println("sub result=" + c); } }p ublic class OverrideTest { public static void main(String args[]) { SuperCla sp = new SuperCla(); SubCla sb = new SubCla(); sp.show(); //此处调用的是父类中的方法show System.out.println("In super Class:a=" + sp.a); //此处引用的是父类中的变量a sb.show(); //此时子类对象的show 方法覆盖了父类的同名方法 System.out.println("In sub Class:a=" + sb.a); } } 程序运行结果: super result=7 In super Class:a=3 sub result=40 In sub Class:a=10 分析:子类SubCla中定义有与父类SuperCla同名的变量a和方法show(),因此使用 子类对象sb时访问变量a和方法show()时,引用的是子类中的成员,父类的同名变量被覆 盖,同名的方法被重写。 如果想在子类中访问父类中被覆盖的成员怎么办呢? 这时可以使用关键字super来解决 这一问题,基本格式如下。 访问父类成员: Java 程序设计(第3 版) 7 8 super.成员变量 或 super.成员方法([参数列表]) 访问父类构造方法: super([参数列表]) 【例5.3】 super关键字的使用。 package code0502; //定义员工类 class Employee { private String name; private int salary; public String getDetails() { return "Name:" + name + "\nSalary:" + salary; } Employee() { name = "Tom"; salary = 1234; } } //定义经理类 class Manager extends Employee { public String department; /*重写getDetails 方法*/ public String getDetails() { System.out.println("I am in Manager"); return super.getDetails(); //调用父类的getDetails 方法 } Manager() { super(); //访问父类的无参构造方法,即Employee() department = "sale"; } }p ublic class Inheritance { public static void main(String arg[]) { Manager m = new Manager(); System.out.println(m.getDetails()); System.out.println("department:" + m.department); } } 程序运行结果: I am in Manager Name:Tom Salary:1234 department:sale 分析:程序首先对Manager实例化,并自动调用无参构造方法Manager(),主要完成 两项主要任务:一是通过super()调用父类的无参构造函数,即Employee();二是对子类中新 增变量department初始化。接着,语句m.getDetails()调用子类的同名方法getDetails(),并 Java 继承与高级特性 第5章 7 9 在方法体中通过super.getDetails()实现对父类中getDetails()方法的调用。 注意:生成一个子类的实例时,首先要执行子类的构造方法,但如果该子类继承自某个父 类,则执行子类的构造方法前,系统自动调用父类的无参构造函数。因此,例5.3的方法 Manager()中去掉语句super(),效果是完全相同的。 与super不同,关键字this的主要作用是表示当前对象的引用,当局部变量和类的成员变 量同名时,该局部变量作用区域内成员变量就被隐藏了,必须使用this来指明。 【例5.4】 this关键字的使用。 package code0502; public class ThisTest { public static void main(String[] args) { Local aa = new Local(); } }c lass Local { public int i = 1; //这个i 是成员变量 Local(int i) { //这个i 是局部变量 System.out.println("this.i ="+ this.i); / /t his.i 指的是对象本身的成员变量i System.out.println("i = "+i); //变量i 前没有this,因此是局部变量 } Local(){ this(6); } } 程序运行结果: this.i =1 i = 6 分析:关键字this的主要作用有:①通过它引用成员变量,如上例中this.i即指的是 当前对象中的成员变量i;②通过this调用类的构造方法,如上例中的this(6)将调用对应的 构造方法Local(inti)。 5.2.3 继承与类型转换 和标准类型数据的转换一样,不同类的对象之间也可以相互转换,但前提是源和目标类之 间必须通过继承相联系。转换可分为显式和隐式两种,显式转换格式如下: (类名)对象名 它将对象转换成类名所表示的其他对象。Java支持父类和子类对象之间的类型转换,如果是 子类对象转换为父类,可进行显式转换或隐式转换;如果是父类对象转换成子类,编译器首先 要检查这种转换的可行性,如果可行,则必须进行显式转换。 【例5.5】 对象类型的转换示例。 package code0502; class CA{ String s="class CA"; }class CB extends CA{ String s="class CB"; Java 程序设计(第3 版) 8 0 }c lass Convert{ public static void main(String args[]){ CB bb,b=new CB(); CA a,aa; a=(CA)b; //显式转换 aa=b; //隐式转换 System.out.println(a.s); System.out.println(aa.s); bb=(CB)a; //显式转换 System.out.println(bb.s); } } 程序运行结果: class CA class CA class CB 分析:b是子类CB的实例,将其转换为父类CA 的实例时可以进行显式或隐式转换。 而父类对象a转换为子类CB的对象时,必须进行显式转换。 5.2.4 实用案例 【例5.6】 通过继承定义员工和经理类。 普通员工和经理作为职员有很多共同之处,如都可以取得工资报酬,但经理可能还会获得 额外的奖金,因此在成员属性和方法上可能有特殊之处。可以将NewEmployee类定义为父 类,NewManager类作为子类,子类继承了父类,并添加了一个新的setBonus()方法,用于增加 奖金,最后打印出结果。参考实现代码如下: package code0502; import java.util.*; class NewEmployee { private String name; private double salary; private Date hireDay; public NewEmployee(String n, double s, int year, int month, int day) { name = n; salary = s; GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day); hireDay = calendar.getTime(); } public String getName() { return name; } public double getSalary() { return salary; } public Date getHireDay() { return hireDay; } Java 继承与高级特性 第5章 8 1 public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } } package code0502; class NewManager extends NewEmployee { private double bonus; public NewManager(String n, double s, int year, int month, int day) { super(n, s, year, month, day); bonus = 0; } public double getSalary() { double baseSalary = super.getSalary(); return baseSalary + bonus; } public void setBonus(double b) { bonus = b; } } package code0502; public class ManagerTest { public static void main(String[] args) { NewEmployee e= new NewEmployee("Harry Hacker", 50000, 1989, 10, 1); e.getName(); System.out.println(e.getName()+":"+e.getSalary()); NewManager boss = new NewManager("Carl Cracker", 80000, 1987, 12, 15); boss.setBonus(5000); System.out.println(boss.getName()+":"+boss.getSalary()); } } 程序运行结果: Harry Hacker:50000.0 Carl Cracker:85000.0 5.3 多 态 5.3.1 多态性的概念 简单地说,多态性就是一个名称可以对应多种不同的实现方法。Java语言的多态性体现 在两个方面:编译多态和运行多态。 编译多态是指在程序编译过程中体现出的多态性,如方法重载。尽管方法名相同,但由于 参数不同,在调用时系统根据传递参数的不同确定被调用的方法,这个过程是在编译时完成 的。例如,定义一个作图的类,它有一个draw()方法用于绘图,根据不用的使用情况,可以接 收字符串、矩形、圆形等参数。对于每一种实现,方法名都为draw(),只不过具体实现方式不 同,不用另外重新起名,这样大大简化了方法的实现和调用,程序员无须记住很多的方法名,只 需传递相应的参数即可。 Java 程序设计(第3 版) 8 2 运行多态则是由类的继承和方法重写引起的,由于子类继承了父类的属性和方法,因此, 凡是父类对象可以使用的地方,子类对象也可以使用。如例5.5中的类CA 和CB,可以直接 生成子类CB的对象,并将该引用赋给父类CA 的对象,即 CA a=new CB(); 它等价于下面两个子句: CB b=new CB(); CA a=b; //隐式类型转换 现在有一个问题:如果子类重写了父类的成员方法,调用该方法时,到底应该调用父类中 的方法还是子类中的方法? 这无法在编译时确定,需要系统在运行时根据实际情况来决定,所 以这种由方法重写引起的多态叫运行多态。 Java规定:对重写的方法,Java根据调用该方法的实例的类型来决定选择哪种方法。对 子类的实例,如果子类重写了父类的方法,则调用子类的方法;如果子类没有重写父类的方法, 则调用父类的方法。 【例5.7】 类的多态性示例。 package code0503; class A { void callme() { System.out.println("inside A"); } }c lass B extends A { void callme() { System.out.println("inside B"); } }c lass Poly { public static void main(String args[]) { A a = new A(); B b = new B(); A c = new B(); a.callme(); b.callme(); c.callme(); } } 程序运行结果: inside A inside B inside B 分析:对实例a和b,它们的类型为类A 和B,所以分别调用父类和子类中的方法 callme()即可。对实例c,它被实例化为子类B的一个对象,且子类重写了方法callme(),因此 根据转型规则,(由于调用该方法的实例类型为B)这时应该调用子类B的方法,所以输出为 insideB。 多态性在实际应用中有很多好处。 (1)可替换性:多态对已存在代码具有可替换性。例如,多态对圆Circle类有效,对其他 Java 继承与高级特性 第5章 8 3 任何圆形几何体(如圆环)也同样有效。 (2)可扩充性:多态对代码具有可扩充性。增加新的子类不影响已有类的多态性、继承 性。实际上,新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态 基础上,很容易增添球体类的多态性。 5.3.2 实用案例 【例5.8】 将员工和经理存入同一数组。 数组一般要求存入的数据必须是同一类型,如整形数组要求每个数组成员均为整数。但 之前定义的类NewEmployee和NewManager显然不是同一类型,那么如何将其存入到同一 个数组中呢? 实现代码如下: package code0502; public class EmployeeArray { public static void main(String[] args) { NewEmployee[] staff = new NewEmployee[3]; NewManager boss = new NewManager("Carl", 80000, 1987, 12, 15); boss.setBonus(5000); staff[0] = boss; staff[1] = new NewEmployee("Harry", 50000, 1989, 10, 1); staff[2] = new NewEmployee("Tommy", 40000, 1990, 3, 15); for (NewEmployee e : staff) System.out.println("name=" + e.getName() + ",salary=" + e.getSalary()); } } 程序运行结果: name=Carl,salary=85000.0 name=Harry,salary=50000.0 name=Tommy,salary=40000.0 分析:满足上述需求的关键在于数组类型的定义,如本例中将数组申明为父类 NewEmployee类型,这样将子类对象存入数组时,系统将利用隐式类型转换规则,将其统一到 父类类型中,即可实现将不同类对象存入同一数组的目标。 如何测试一个对象是否为某种类型的实例呢? 如本例中,我们想知道staff[0]是否为 NewManager的实例,可以通过instanceof 运算符来实现,格式为variableinstanceof TypeName,如果variable为TypeName或TypeName父类型的实例,运算结果为true,否则 返回false。因此,表达式staff[0]instanceofNewManager的结果为true,staff[0]instanceof NewEmployee的结果也为true,staff[1]instanceofNewManager的结果则为false。 5.4 抽象类与抽象方法 5.4.1 定义抽象类及实现抽象方法 关键字abstract修饰的类称为抽象类,抽象类是一种没有完全实现的类。不能用它实例 化任何对象,它的主要用途是用来描述一些概念性的内容,然后在子类中具体去实现这些概 念,这样可以提高开发效率,统一用户接口,所以抽象类更多是作为其他类的父类。