在前面的示例中,我们把 Circle 类声明为 shapes 包的一部分。假设我们计划实现多个表示形状的类:Rectangle、Square、Ellipse、Triangle 等。我们可以在这些表示形状的类中定义两个基本方法:area() 和 circumference()。那么,为了能方便处理由形状组成的数组,这些表示形状的类最好有个共同的超类 Shape。这样组织类层次结构的话,每个形状对象,不管具体表示的是什么形状,都能赋予类型为 Shape 的变量、字段或数组元素。我们想在 Shape 类中封装所有形状共用的功能(例如,area() 和 circumference() 方法)。但是,通用的 Shape 类不表示任何类型的形状,所以不能为这些方法定义有用的实现。Java使用抽象方法解决这种问题。
Java 允许使用 abstract 修饰符声明方法,此时只定义方法但不实现方法。abstract 修饰的方法没有主体,只有一个签名和一个分号。以下是 abstract 方法和这些方法所在的abstract 类相关的规则:
1 只要类中有一个 abstract 方法,那么这个类本身就自动成为 abstract 类,而且必须声明为 abstract 类,否则会导致编译出错。
2 abstract 类无法实例化。
3 abstract 类的子类必须覆盖超类的每个 abstract 方法并且把这些方法全部实现(即提供方法主体),才能实例化。这种类一般叫作具体子类(concrete subclass),目的是强调它不是抽象类。
4 如果 abstract 类的子类没有实现继承的所有 abstract 方法,那么这个子类还是抽象类,而且必须使用 abstract 声明。
5 使用 static、private 和 final 声明的方法不能是抽象方法,因为这三种方法在子类中
6 不能覆盖。类似地,final 类中不能有任何 abstract 方法。就算类中没有 abstract 方法,这个类也能声明为 abstract。使用这种方式声明的abstract 类表明实现的不完整,要交给子类实现。这种类不能实例化。
比如Classloader 类,这个类就没有任何抽象方法。
下面通过一个示例说明这些规则的运作方式。如果定义 Shape 类时把 area() 和circumference() 声明为 abstract 方法,那么 Shape 的子类必须实现这两个方法才能实例化。也就是说,每个 Shape 对象都要确保实现了这两个方法。下图示例展示了如何编写代码。在这段代码中,定义了一个抽象的 Shape 类和两个具体子类:
Shape 类中每个抽象方法的括号后面都是分号,没有花括号,也没定义方法的主体。使用示例中定义的这几个类可以编写如下的代码:
有两点要注意:
1 Shape 类的子类对象可以赋值给 Shape 类型数组中的元素,无需校正。这又是一个放大转换引用类型(第 2 章讨论过)的例子。
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 对象。带校正的缩小转换能把这个对象转换回数组。下面是一个示例:
除了能把数组转换成对象之外,如果两个数组的“基类型”是可以相互转换的引用类型,那么数组还能转换成另一个类型的数组。例如:
注意,这些数组转换规则只适用于由对象或数组组成的数组。基本类型的数组不能转换为任何其他数组类型,就算基本基类型之间能相互转换也不行:
// 就算int类型能放大转换成double类型
// 也不能把int[]类型转换成double[]类型
// 这行代码会导致编译出错
double[] data = new int[] {1,2,3};
//但是,这行代码是合法的,因为int[]类型能转换成
Object类型Object[] objects = new int[][] {{1,2},{3,4}};