Java-多态

一、向上转型

为新的类提供方法并不是继承技术的最重要的方面,其最为重要的方面是用来展现新类和基类之间的关系。即,新类是现有类的一种类型。

class Instrument {
public void play() {}
static void tune(Instrument i) {
i.play();
}
}

public class Wind extends Instrument {
public static void main(String[] args) {
Wind flute = new Wind();
Instrument.tune(flute);
}
}

上面这种将Wind引用转换为Instrument引用的动作,称为向上转型

再看下面的例子:

public static void main(String[] args) {
Wind f = new Wind();
tune(f);
}

public static void tune(Instrument t) {
t.play(Note.MIDDLE_C);
}

enum Note {
MIDDLE_C, C_SHARP, B_FLAT;
}

static class Instrument {
public void play(Note n) {
System.out.println("Instrument.play()");
}

}

static class Wind extends Instrument {
@Override
public void play(Note n) {
System.out.println("Wind.play() " + n);
}
}

这里tune()接收一个Instrument引用,同时也接收任何导出自Instrument的类。从Wind向上转型到Instrument可能会“缩小”接口,但不会比Instrument的全部接口更窄。

二、转机

再来看一下tune()函数:

public static void tune(Instrument t) {
t.play(Note.MIDDLE_C);
}

他接收一个Instrument引用。但是他是如何知道该类型为Wind而不是Winds、Wint。

方法调用绑定

将一个方法调用同一个方法主题关联起来被称为绑定。如果在程序执行前进行绑定,叫做前期绑定。上面代码迷惑的原因就是因为前期绑定,无法确认究竟调用哪个方法。
解决办法就是后期绑定,他在运行时根据对象的类型进行绑定。后期绑定也叫做动态绑定或运行时绑定。虽然编译器不知道是什么类型,但是方法调用机制能找到正确的方法体,并调用。
Java中除了static方法和final方法(private方法属于final方法)之外,所有的方法都是后期绑定。

缺陷1 “覆盖”私有方法

看下面一段代码:

public class PrivateOverride {

public static void main(String[] args) {
PrivateOverride p = new Derived();
p.f();
}

private void f() {
System.out.println("private f()");
}
}
public class Derived extends PrivateOverride {
public void f() {
System.out.println("public f()");
}
}

输出的结果是private f()。由于private方法被自动认为是final方法,因此新类的方法f()是一个新的函数而非继承而来。

缺陷2 域和静态方法

public class Super {
public int field = 0;
public int getField() {
return field;
}

public static void main(String[] args) {
Super sup = new Sub();
System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField());

Sub sub = new Sub();
System.out.println("sub.field = " + sub.field + ", sub.getField() = " + sub.getField() + ", sub.getSuperField() = " + sub.getSuperField());
}
}
public class Sub extends Super {
public int field = 1;

public int getField() {
return field;
}

public int getSuperField() {
return super.field;
}
}
out:
sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0

当Sub对象转型为Super引用时,任何域访问操作都将有编译器解析,因此不在时多态。例子中Sub.field和Super.field分配了不同的存储空间。

同上,如果某个方法设置为static,他的行为也不具有多态性。

三、构造器和多态

尽管构造器并不具备多态性(实际上是static方法,只是该static声明是隐式的),还是非常必要理解构造器怎么通过多态在复杂的层次结构中运作。

构造器的调用顺序

基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。

  • 1.调用基类构造器。这个步骤会不断的反复递归下去。
  • 2.按声明顺序调用成员的初始化方法。
  • 3.调用导出类构造器的主题。

继承与清理

通过组合和继承的方法来创建新类时,永远不用担心对象的清理问题,子对象通常会留给垃圾回收器进行处理。除非需要手动清理的数据,关闭的状态,这些就需要手动添加函数并调用。

构造器内部的多态方法的行为

看看下面的代码:

public class Glyph {

Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}

void draw() {
System.out.println("Glyph.draw()");
}
}
public class RoundGlyph extends Glyph {

private int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph.RoundGlyph(). radius = " + radius);
}

@Override
void draw() {
System.out.println("RoundGlyph.draw(). radius = " + radius);
}

public static void main(String[] args) {
new RoundGlyph(5);
}
}
out:
Glyph() before draw()
RoundGlyph.draw(). radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(). radius = 5

如果在一个构造器的内部调用正在构造的对象的某个动态绑定的方法,会发生什么!
Glyph.draw()方法设计为将要被覆盖,这种覆盖是在RoundGlyph中发生。然后在Glyph构造器中调用这个方法,导致是对RoundGlyph.draw()方法调用。但是结果却不尽如人意,得到的不是初始值1,而是0!

  • 1.在其他任何事情发生之前,将分配给对象的存储空间初始化为二进制的零。
  • 2.如前所述调用基类构造器。调用被覆盖的draw()方法(要在调用RoundGlyph构造器之前调用),由于步骤1,我们会发现值为0。
  • 3.按照声明的顺序调用成员的初始化方法。
  • 4.调用导出类的构造器主体。

四、协变返回函数

JavaSE5中添加了协变返回类型,表示导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型:

		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);
}

static class Grain {
public String toString() {
return "Grain";
}
}

static class Wheat extends Grain {
public String toString() {
return "Wheat";
}
}

static class Mill {
Grain process() {
return new Grain();
}
}

static class WheatMill extends Mill {
Wheat process() {
return new Wheat();
}
}
out:
Grain
Wheat

早期版本不允许返回Wheat,尽管Wheat是Grain导出的。协变返回类型允许了返回更具体的Wheat类型。

五、用继承进行设计

首先考虑使用继承技术,反倒会加重我们的设计负担。更好的方式首先是组合,尤其是不能十分确定应该使用哪一种方式时。

    public static void main(String[] args) {
Stage stage = new Stage();
stage.performPlay();
stage.change();
stage.performPlay();
}

static class Actor {
public void act() {}
}

static class HappyActor extends Actor {
public void act() {
System.out.println("HappyActor");
}
}

static class SadActor extends Actor {
public void act() {
System.out.println("SadActor");
}
}

static class Stage {
private Actor actor = new HappyActor();

public void change() {
actor = new SadActor();
}

public void performPlay() {
actor.act();
}
}
out:
HappyActor
SadActor

在performPlay()方法中,因为引用的类型不同,在运行期间具有了动态灵活性(状态模式)。
用继承表达行为间的差异,并用字段表达状态上的变化。

纯继承与扩展

纯粹的方式创建继承层次结构。只有在基类中已经建立的方法才可以在导出类中被覆盖。
也可以认为这是一种纯代替,因为导出类完全可以代替基类。

这也以为着在导出类中接口的扩展部分不能被基类访问,一旦向上转型,就无法调用这些基类类。而这个问题就需要接口去处理。

向下转型与运行时类型识别

向上转型会丢失具体的类型信息,所以通过向下转型,也就是继承层次向下移动,就能获取类型信息。
但是在实际情况中,向上转型更为安全,因为基类不会具有大于导出类的接口。要解决这个问题,必须有某种方法类确保向下转型的正确性,不会转换到一个错误类型。
在Java中,所有的类型转换都会得到检查,如果发生错误就会返回ClassCastException的异常。这种在运行期间对类型进行检查的行为称为运行时类型识别(RTTI)。