多态是既抽象和继承之后的第三种基本特征。
多态通过分离做什么和怎么做,从另以角度将接口和实现分离开来。
“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化”把接口和实现分离开来。
而多态的作用则是消除类型之间的耦合关系。
多态(也称作动态绑定、后期绑定或运行时绑定)。
再论向上转型
把对某个对象的引用视为其对基类型的引用的做法被称作向上转型。
从子类向上转型到基类会缩小接口,但不会比基类的全部借口更窄。
忘记对象类型
通俗的说,在方法中,用基类引用作为形参,在使用方法的时候,而不必为每个子类对象分别写重载的方法。
转机
方法调用绑定
将一个方法调用同一个主体关联起来被称作绑定。
- 在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),叫做前期绑定。
- 后期绑定,就是在运行时根据对象的类型进行绑定。也叫做动态绑定或运行时绑定。
后期绑定,编译器一直不知道对象的类型,但是方法调动机制能找到正确的方法体,并加以调用。
后期绑定机制不管怎样都必须在对象中安置某种“类型信息”。
Java 中除了 static 方法和 final 方法(private 方法属于 final 方法)之外,其他所有的方法都是后期绑定。
声明方法为 final 可以防止其他人覆盖该方法,但更重要的一点是,这样做可以有效的“关闭”动态绑定。
缺陷
- 覆盖私有方法
只有非 private 方法才可以被覆盖,但是还需要密切关注 “覆盖” private 的现象,编译器不会报错,但是不会按照我们期望的来执行。确切的说,在导出类中,对与基类的 private 方法,最好采用不同的名字。
- 域与静态方法
并不是所有食物都可以多态地发生,只有普通的方法调用是可以多态的。例如,如果直接访问某个域,这个访问就将在编译器进行解析。
当导出类对象转型为基类对象时,任何域访问操作都将由编译器解析,因此不是多态的。因此,在导出类中,为了得到基类的 field ,必须显示地指明 super.field。
构造器和多态
构造器不具有多态性(它们实际上是 static 方法,只不过该 static 声明是隐式的)。
构造器的调用顺序
基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。这样做有意义,因为构造器有一项特殊任务:检查对象是否被正确的构造。导出类只能访问它自己的成员,不能访问基类中的成员(基类成员通常是 private 类型)。只有基类构造器才具有恰当的权限来对自己的元素进行初始化。因此,必须令所有构造器都能得到调用,否则就不可能正确构造完整对象。在导出类的构造器中,乳沟么有明确指定调用某个基类构造器,它就会默默调用默认构造器,如若不存在默认构造器,编译器会报错。
复杂对象调用构造器遵照下面的顺序:
- 调用基类构造器。
- 按声明顺序调用成员的初始化方法。
- 调用导出类构造器的主题。
继承与清理
当重写了基类的清理方法例如 dispose() 后,要在导出类的 dispose() 方法中,调用基类的 dispose() 方法,而且赢放在最后。为了防止某个子对象要依赖于其他对象,销毁的顺序应该和初始化顺序相反。对于字段,则意味着与声明的顺序相反。对于基类,应该首先对导出类进行清理,然后才是基类。
构造器内部的多态方法的行为
如果在构造器内部调用一个动态绑定方法,就要用到被覆盖后的定义,但这个时候,导出类的对象可能还没被完全构造,这就可能引发错误。通过下面的例子来说明
class Glyph {
void draw() {
System.out.println("Glyph.draw()");
}
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph().radius = " + radius);
}
void draw() {
System.out.println("RoundGlyph().radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}
//output
Glyph() before draw()
RoundGlyph().radius = 0
Glyph() after draw()
RoundGlyph().radius = 5
这里虽然成功的调用了基类的方法,但是可以看到的是 radius 的值是0,而不是指定的1;
初始化的实际过程是:
- 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。
- 如前所述调用基类构造器。此时,调用覆盖后的 draw() 方法(要在调用RoundGlyph 构造器之前调用),会发现 radius 的值为0;
- 按照声明的顺序调用成员的初始化方法。
- 调用导出类的构造器主题。
因此比那些构造器有一条有效的准则:“用尽可能简单的方式使对象进入正常状态;如果也可以的话,避免调用其他方法”。
协变返回类型
表示在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型。
class Grain {
public String toString() {
return "Grain";
}
}
class Wheat extends Grain {
public String toString() {
return "Wheat";
}
}
class Mill {
Grain process() {
return new Grain();
}
}
class WheatMill extends Mill {
Wheat process() {
return new Wheat();
}
}
public class CovariantReturn {
public static void main(String[] args) {
Mill m = new Mill();
Grain g = m.process();
System.out.println(g);
m = new WheatMill();
g = m.process();
System.out.println(g);
}
}
协变返回类型允许返回更具体的 Wheat 类型
用继承进行设计
用继承来表达行为间的差异,并用字段表达状态上的变化
纯继承与扩展
采用“纯粹”的方式来创建继承层级结构似乎是最好的方式。也就是说,只有在基类中已经建立的方法才可以在导出类中覆盖。
这种被称作是纯粹的“is-a”关系,因为一个类的接口已经确定了它应该是什么。
继承可以确保所有的导出类具有基类的接口,且绝对不会少。也可以认为是一种纯替代。
另外一种“is-like-a”,缺点,向上转型,不能调用新方法。
向下转型与运行时类型识别
在 Java 语言中,所有的转型都会得到检查!在进入运行期时会进行检查,以便保证它的确是我们希望的那种类型。这种在运行期进行检查的行为称作“运行时类型识别(RTTI)。
向下转型会添加更多细节,所以要进行判断,否则容易产生 ClassCastException 异常。