3.6 抽象类和方法
在示例 3-4 中,我们把 Circle 类声明为 shapes 包的一部分。假设我们计划实现多个表示形状的类:Rectangle、Square、Ellipse、Triangle 等。我们可以在这些表示形状的类中定义两个基本方法:area() 和 circumference()。那么,为了能方便处理由形状组成的数组,这些表示形状的类最好有个共同的超类 Shape。这样组织类层次结构的话,每个形状对象,不管具体表示的是什么形状,都能赋予类型为 Shape 的变量、字段或数组元素。我们想在 Shape 类中封装所有形状共用的功能(例如,area() 和 circumference() 方法)。但是,通用的 Shape 类不表示任何类型的形状,所以不能为这些方法定义有用的实现。Java 使用抽象方法解决这种问题。
Java 允许使用 abstract 修饰符声明方法,此时只定义方法但不实现方法。abstract 修饰的方法没有主体,只有一个签名和一个分号。2 以下是 abstract 方法和这些方法所在的 abstract 类相关的规则。
2Java 中的抽象方法和 C++ 中的纯虚拟函数(即声明为 = 0 的虚拟函数)有点像。在 C++ 中,包含纯虚拟函数的类是抽象类,不能实例化。包含抽象方法的 Java 类也一样不能实例化。
只要类中有一个
abstract方法,那么这个类本身就自动成为abstract类,而且必须声明为abstract类,否则会导致编译出错。abstract类无法实例化。abstract类的子类必须覆盖超类的每个abstract方法并且把这些方法全部实现(即提供方法主体),才能实例化。这种类一般叫作具体子类(concrete subclass),目的是强调它不是抽象类。如果
abstract类的子类没有实现继承的所有abstract方法,那么这个子类还是抽象类,而且必须使用abstract声明。使用
static、private和final声明的方法不能是抽象方法,因为这三种方法在子类中不能覆盖。类似地,final类中不能有任何abstract方法。就算类中没有
abstract方法,这个类也能声明为abstract。使用这种方式声明的abstract类表明实现的不完整,要交给子类实现。这种类不能实例化。
第 11 章会见到
Classloader类,这个类就没有任何抽象方法。
下面通过一个示例说明这些规则的运作方式。如果定义 Shape 类时把 area() 和 circumference() 声明为 abstract 方法,那么 Shape 的子类必须实现这两个方法才能实例化。也就是说,每个 Shape 对象都要确保实现了这两个方法。示例 3-5 展示了如何编写代码。在这段代码中,定义了一个抽象的 Shape 类和两个具体子类。
示例 3-5:一个抽象类和两个具体子类
public abstract class Shape {public abstract double area(); // 两个抽象方法public abstract double circumference(); // 注意,没有主体,只有分号}class Circle extends Shape {public static final double PI = 3.14159265358979323846;protected double r; // 实例字段public Circle(double r) { this.r = r; } // 构造方法public double getRadius() { return r; } // 访问器public double area() { return PI*r*r; } // 实现超类中的public double circumference() { return 2*PI*r; } // 两个抽象方法}class Rectangle extends Shape {protected double w, h; // 实例字段public Rectangle(double w, double h) { // 构造方法this.w = w; this.h = h;}public double getWidth() { return w; } // 访问器方法public double getHeight() { return h; } // 另一个访问器public double area() { return w*h; } // 实现超类中的public double circumference() { return 2*(w + h); } // 两个抽象方法}
Shape 类中每个抽象方法的括号后面都是分号,没有花括号,也没定义方法的主体。使用示例 3-5 中定义的这几个类可以编写如下的代码:
Shape[] shapes = new Shape[3]; // 创建一个保存形状的数组shapes[0] = new Circle(2.0); // 填充这个数组shapes[1] = new Rectangle(1.0, 3.0);shapes[2] = new Rectangle(4.0, 2.0);double totalArea = 0;for(int i = 0; i < shapes.length; i++)totalArea += shapes[i].area(); // 计算这些形状的面积
有两点要注意。
Shape类的子类对象可以赋值给Shape类型数组中的元素,无需校正。这又是一个放大转换引用类型(第 2 章讨论过)的例子。即便
Shape类没有定义area()和circumference()方法的主体,各个Shape对象还是能调用这两个方法。调用这两个方法时,使用虚拟方法查找技术找到要调用的方法。因此,圆的面积使用Circle类中定义的方法计算,矩形的面积使用Rectangle类中定义的方法计算。
转换引用类型
对象可以在不同的引用类型之间转换。和基本类型一样,引用类型转换可以是放大转换(编译器自动完成),也可以是需要校正的缩小转换(或许运行时还要检查)。要想理解引用类型的转换,必须理解引用类型组成的层次结构,这个体系叫作类层次结构。
每个 Java 引用类型都扩展其他类型,被扩展的类型是这个类型的超类。类型继承超类的字段和方法,然后定义属于自己的一些额外的字段和方法。在 Java 中,类层次结构的根是一个特殊的类,名为 Object。所有 Java 类都直接或间接地扩展 Object 类。Object 类定义了一些特殊的方法,所有对象都能继承(或覆盖)这些方法。
预定义的 String 类和本章前面定义的 Point 类都扩展 Object 类。因此,可以说,所有 String 对象也都是 Object 对象。也可以说,所有 Point 对象都是 Object 对象。但是,反过来说就不对了。我们不能说每个 Object 对象都是 String 对象,因为如前所示,有些 Object 对象是 Point 对象。
简单理解类层次结构之后,我们可以定义引用类型的转换规则了。
对象不能转换成不相关的类型。例如,就算使用校正运算符,Java 编译器也不允许把
String对象转换成Point对象。对象可以转换成超类类型,或者任何祖先类类型。这是放大转换,因此不用校正。例如,
String对象可以赋值给Object类型的变量,或者传入期待Object类型参数的方法。
其实没有执行转换操作,而是直接把对象当成超类的实例。这种行为有时称为里氏替换原则(Liskov substitution principle),以第一个明确表述这种行为的计算机科学家 Barbara Liskov 的名字命名。
对象可以转换成子类类型,但这是缩小转换,需要校正。Java 编译器临时允许执行这种转换,但 Java 解释器在运行时会做检查,确保转换有效。根据程序的逻辑,确认对象的确是子类的实例后才会把对象校正成子类类型。否则,解释器会抛出
ClassCastException异常。例如,如果把一个String对象赋值给Object类型的变量,那么后面可以校正这个变量的值,再变回String类型:Object o = "string"; // 把String对象放大转换成Object类型 String s = (String) o; // 程序后面再把这个Object对象缩小转换成String类型
数组是对象,而且有自己的一套转换规则。首先,任何数组都能放大转换成 Object 对象。带校正的缩小转换能把这个对象转换回数组。下面是一个示例:
// 把数组放大转换成Object对象Object o = new int[] {1,2,3};// 程序后面……int[] a = (int[]) o; // 缩小转换回数组类型
除了能把数组转换成对象之外,如果两个数组的“基类型”是可以相互转换的引用类型,那么数组还能转换成另一个类型的数组。例如:
// 这是一个字符串数组String[] strings = new String[] { "hi", "there" };// 可以放大转换成CharSequence[]类型// 因为String类型可以放大转换成CharSequence类型CharSequence[] sequences = strings;// 缩小转换回String[]类型需要校正strings = (String[]) sequences;// 这是一个由字符串数组组成的数组String[][] s = new String[][] { strings };// 不能转换成CharSequence[]类型,因为String[]类型// 不能转换成CharSequence类型:维数不匹配sequences = s; // 不会编译这行代码// s可以转换成Object类型或Object[]类型,因为所有数组类型// (包括String[]和String[][]类型)都能转换成Object类型Object[] objects = s;
注意,这些数组转换规则只适用于由对象或数组组成的数组。基本类型的数组不能转换为任何其他数组类型,就算基本基类型之间能相互转换也不行:
// 就算int类型能放大转换成double类型// 也不能把int[]类型转换成double[]类型// 这行代码会导致编译出错double[] data = new int[] {1,2,3};// 但是,这行代码是合法的,因为int[]类型能转换成Object类型Object[] objects = new int[][] {{1,2},{3,4}};
第 11 章会见到 