第十一章 继承和多态
11.1 引言
面向对象编程支持从已经存在的类中定义新的类,这称为继承。
继承是Java在软件重用方面一个重要且功能强大的特征。
11.2 父类和子类
继承使得你可以定义一个通用的类(即父类),之后继承该类为一个更特定的类(即子类)。
使用类来对同一类型的对象建模。不同的类可能会有一些共同的特征和行为,可以在一个通用类中表达这些共同之处,并被其他类所共享。
考虑一下几何对象。假设要设计类来对像圆和矩形这样的几何对象建模。几何对象有许多共同的属性和行为。它们可以是用某种颜色画出来的,可以填充或者不填充。
在Java术语中,如果类C1继承自另一个类C2,那么就将C1称为子类(subclass),将C2称为超类(superclass)。超类也称为父类(parent class)或基类(base class),子类又称为继承类(extended class)或派生类(derived class)。子类从它的父类中继承可访问的数据域和方法,还可以添加新的数据域和方法。
例如:对于学生类、教师类。它们都具有年龄、性别、身高、体重、姓名等共同特征,那么我们便可以定义一个类,将该类命名为person类,如下:
public class person {
private String name;
private char gender;
private int age;
//空参构造
public person() {
}
//有参构造
public person(String name, char gender, int age) {
this.name = name;
this.gender = gender;
this.age = age;
}
//获取姓名
public String getName() {
return this.name;
}
}
下面我们定义一个学生类,并且继承person类中的内容:
public class student extends person{
// Student 类的属性
private String studentId; // 学生ID
// Student 类的特定方法
public void study() {
System.out.println(name + " is studying.");
}
}
11.3 使用super关键字
关键字 super指代父类,可以用于调用父类中的普通方法和构造方法。
11.3.1 调用父类的构造方法
构造方法用于构建一个类的实例。不同于属性和普通方法,父类的构造方法不会被子类继承。它们只能使用关键字 super从子类的构造方法中调用。
调用父类构造方法的语法是:
super()或者super(arguments);
语句 super() 调用父类的无参构造方法,而语句super(arguments)调用与arguments 匹配的父类的构造方法。语句super()或super(arguments)必须出现在子类构造方法的第一行,这是显式调用父类构造方法的唯一方式。
警告:要调用父类的构造方法就必须使用关键字super,而且这个调用必须是构造方法的第一条语句。在子类中调用父类构造方法的名字会引起一个语法错误。
11.3.2 构造方法链
构造方法可以调用重载的构造方法或父类的构造方法。如果它们都没有被显式地调用编译器就会自动地将super()作为构造方法的第一条语句。例如:
在任何情况下,构造一个类的实例时,将会调用沿着继承链的所有父类的构造方法。当构造一个子类的对象时,子类的构造方法会在完成自己的任务之前,首先调用它的父类的构造方法。如果父类继承自其他类,那么父类的构造方法又会在完成自己的任务之前,调用它自己的父类的构造方法。这个过程持续到沿着这个继承层次结构的最后一个构造方法被调用为止。这就是构造方法链(constructor chaining)。
11.3.3 调用父类的普通方法
关键字super不仅可以引用父类的构造方法,也可以引用父类的方法。所用语法如下:
.super.方法名(参数);
11.4 方法重写
要重写一个方法,需要在子类中使用和父类一样的签名来对该方法进行定义。
子类从父类中继承方法。有时,子类需要修改父类中定义的方法的实现,这称为方法重写(method overriding)。
11.5 方法重写与重载
重载意味着使用同样的名字但是不同的签名来定义多个方法。重写意味着在子类中提供一个对方法的新的实现。
方法重写时需要注意以下几点:
方法重写发生在具有继承关系的不同类中;方法重载可以发生在同一个类中,也可以发生在具有继承关系的不同类中。
方法重写具有同样的签名;方法重载具有同样的名字但是不同的参数列表。为了避免错误,可以使用一种特殊的Java语法,称为重写标注(override annotation),在子类的方法前面放一个**@0verride**。例如:
public class Circle extends GeometricObject{
//Other methods are omitted
@0verride
public String toString(){
return super.toString() + "\nradius is " + radius;
}
}
该标注表示被标注的方法必须重写父类的一个方法。如果具有该标注的方法没有重写父类的方法,编译器将报告一个错误。
11.6 Object类及其toString()方法
Java中的所有类都继承自java.lang.Object 类。
如果在定义一个类时没有指定继承,那么这个类的父类默认是Object。例如,
下面两个类的定义是一样的:
11.7 多态
面向对象程序设计的三大支柱是封装、继承和多态。
继承关系使一个子类能继承父类的特征,并且附加一些新特征。子类是它的父类的特殊化,每个子类的实例都是其父类的实例,但是反过来不成立。
11.8 动态绑定
方法可以在沿着继承链的多个类中实现。JVM决定运行时调用哪个方法。
我们首先介绍两个术语:声明类型和实际类型。一个变量必须被声明为某种类型。变量的这个类型称为它的声明类型(declared type)。这里,o 的声明类型是Object。一个引用类型变量可以是一个null 值或者是一个对声明类型实例的引用。实例可以使用声明类型或它的子类型的构造方法创建。变量的实际类型(actual type)是被变量引用的对象的实际类。这里,o的实际类型是 GeometricObject ,因为 o 引用使用 new GeometricObject() 创建的对象。o 调用哪个 toString() 方法由 o 的实际类型决定。这称为动态绑定(dyamic binding)。
动态绑定工作机制如下:假设对象 o 是类 C 1 C_1 C1, C 2 C_2 C2,…, C n − 1 C_{n-1} Cn−1, C n C_n Cn的实例,其中 $ C_1 $是 C 2 C_2 C2 的子类, C 2 C_2 C2 是 C 3 C_3 C3 的子类,…, C n − 1 C_{n-1} Cn−1 是 C n C_n Cn 的子类,如图 11-2 所示。也就是说, C n C_n Cn 是最通用的类, C 1 C_1 C1 是最特殊的类。在Java中, C n C_n Cn 是 Object 类。如果对象 o 调用一个方法 P,那么JVM会依次在类 C 1 C_1 C1 , C 2 C_2 C2, …, C n − 1 C_{n-1} Cn−1 , C n C_n Cn 中查找方法 p 的实现,直到找到为止。一旦找到一个实现,就停止查找,然后调用这个首先找到的实现。
11.9 对象转换和instanceof 操作符
一个对象的引用可以类型转换为对另外一个对象的引用,这称为对象转换。
语句
m(new Student());
将对象 new student()
赋值给一个 Object
类型的参数。这条语句等价于
Object o = new Student(); //Implicit casting
m(o);
由于 Student
的实例也是 Object
的实例,所以,语句 Object o = new student()
是合法的,它称为隐式转换(implicit casting)。
11.10 Object类的equals方法
如同 toString()
方法,equals(Object)
方法是定义在 Object
类中的另外一个有用的方法。
在Object
类中,equals
是经常使用的方法。它的签名是:
public boolean equals(Object o)
这个方法测试两个对象是否相等。调用它的语法:
object1.equals(object2);
Object
类中 equals
方法的默认实现是:
public boolean equals(Object obj){
return (this == obj);
}
这个实现使用-操作符检测两个引用变量是否指向同一个对象。因此,应该在自己自定义类中重写这个方法,以测试两个不同的对象是否具有相同的内容。
11.11 ArrayList类
ArrayList 对象可以用于存储一个对象列表。
我们可以创建一个数组来存储对象,但是这个数组一旦创建,它的大小就固定了。
而Java中提供了ArrayList
类,可以用来存储不限定个数的对象。下图给出了ArrayList
中的一些方法。
我们可以用如下格式去创建一个ArrayList
对象:
ArrayList<AConcreteType> list = new ArrayList<>();
虽然我们可以像使用数组一样使用ArrayList
对象,但是两者还是有很多不同之处。下表列出了它们的异同点。
11.13 示例学习:自定义栈类
需求:设计一个栈类,用于存放对象。
**思路:**下面是栈类的UML图
具体实现:
import java.uti1.ArrayList;
public class MyStack {
private ArrayList<Object> list = new ArrayList<>();
public boolean isEmpty() {
return list.isEmpty();
}
public int getSize(){
return list.size();
}
public Object peek(){
return list.get(getSize()-1);
}
public Object pop(){
Object o = 1ist.get(getSize()-1);
1ist.remove(getSize()-1);
return o;
}
public void push(Object o){
list.add(o);
}
@Override
public String tostring(){
return "stack:" + list.toString();
}
}
11.14 protected数据和方法
一个类中的受保护成员可以从子类中访问。
至今为止,我们已经用过关键字private
和public
来指定是否可以从类的外部访问数据域和方法。私有成员只能在类内访问,而公共成员可以被任意的其他类访问。
经常需要允许子类访问定义在父类中的数据域或方法,但不允许位于不同包中的非子类的类访问这些数据域和方法。面对这种情景,我们可以使用protected
关键字完成该功能。父类中受保护的数据域或方法可以在它的子类中访问。
修饰符 private
、protected
和 public
都称为可见性修饰符(visibility modifier)或可访问性修饰符(accessibility modifer),因为它们指定如何访问类和类的成员。这些修饰符的可见性按下面的顺序递增:
下表总结了类中成员的可访问性:
11.15 防止继承和重写
一个被 final
修饰的类和方法都不能被继承。被final
修饰的数据域是一个常数。
有时候,可能希望防止类被继承。在这种情况下,使用final
修饰符表明一个类是最终类,是不能作为父类的。Math
类就是一个最终类。String
、StringBuilder
和StringBuffer
类以及所有基本数据类型的包装类也都是最终类。