Java Note (4) Polymorphic

多态实现了“是什么”与“怎样做”两个模块的分离,代码的组织以及可读性均能获得改善。此外,还能创建“易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地扩展。
多态旨在消除类型之间的耦合关系。我们知道,通过继承可将一个对象当作它自己的类型或者它自己的基础类型对待。这种能力是十分重要的,因为多个类型(只要从相同的基础类型中衍生出来)可被当作同一种类型对待。因而只需一段代码,即可对所有不同的类型进行同样的处理。而类型之间的区别,是根据方法行为的不同而表现出的,虽然这些方法均可通过对于基类的调用做统一处理。

1. 向上转型与多态

1.1 多态的机理

我们知道,对象可以作为它自己本身的类型使用,也可以作为它的基类使用,而这种把某种对象的引用视为对其基类的引用的做法被称为——向上转型。例如下面的代码, Wind便是 Instrument类继承。
//: polymorphism/music/Instrument.java
package polymorphism.music;
import static net.mindview.util.Print.*;

class Instrument {
  public void play(Note n) {
    print("Instrument.play()");
  }
}
 ///:~
//: polymorphism/music/Wind.java
package polymorphism.music;

// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {
  // Redefine interface method:
  public void play(Note n) {
    System.out.println("Wind.play() " + n);
  }
} ///:~
//: polymorphism/music/Music.java
// Inheritance & upcasting.
package polymorphism.music;

public class Music {
  public static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);
  }
  public static void main(String[] args) {
    Wind flute = new Wind();
    tune(flute); // Upcasting
  }
} /* Output:
Wind.play() MIDDLE_C
*///:~
Wind向上转型可能会缩小接口,但是不会比 Instrument的更窄。一个例子就是:
Shape s = new Circle();
我们假定Circle已经继承了Shape,那么这么写是没有问题的。同时,JVM也会正确的理解为你创建了Circle类型的对象,你运行的是Circle.f(),而不是Shape.f()。
但是有这样一个问题, tune()函数的输入时 Instrument类型,而不是 Wind。如果我们加入了类似 BrassViolin,那么我们需要为这些类型均实现一个不同的方法,这显然比较冗余。
因而我们需要一个简单的方法,它仅仅接受基类作为参数,编写的代码只和基类打交道。这就是多态存在的现实意义。
多态的解决方案:多态解决上述问题的方法是——后期绑定,或者成为“运行时绑定”。Java中出了final和static方法职位,其他的方法默认都是后期绑定,这个与C语言很不一样。而将方法设定为final的意义在于,防止其他人将这个方法覆盖,并关闭后期绑定(可小幅度提升性能)。那么如何实现多态呢?方法很简单,只要在导出类中重写一个实现,将基类中同样的方法覆盖即可。这个不再举例了。
如果新类型可以很好的适应已有的方法,并且只和基类的接口通信,那么我们称这种程序是可扩展的。
Note: 多态的覆盖只能覆盖public方法,而不能覆盖private方法。如果覆盖了,JVM不会报错,但是也不会按照覆盖后的进行运行。

1.2 多态的陷阱

我们可能会认为,所有事物都可以多态的发生。然而, 只有普通的方法调用时可以多态的。例如:
//: polymorphism/FieldAccess.java
package polymorphism; /* Added by Eclipse.py */
// Direct field access is determined at compile time.

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

class Sub extends Super {
  public int field = 1;
  public int getField() { return field; }
  public int getSuperField() { return super.field; }
}

public class FieldAccess {
  public static void main(String[] args) {
    Super sup = new Sub(); // Upcast
    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());
  }
} /* Output:
sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
*///:~
Sub转型为 Super引用时,任何域访问操作都将由编译器解析,因而不是多态的。在上面例子中, Super.fieldSub.field分配了不同的存储空间。这样, Sub实际上包括两个成为field的域。然而,在引用 Sub中field是采用的是默认域。为了得到 Super.field必须使用 super.field(制定基类)。如果某个方法是静态的,那么他的行为就不具有多态性。
同时,我们也应当注意一下编程的习惯:首先将所有的域设置为 private,因而不能够直接访问它们,副作用是只能用对方类中的方法去访问这些域。此外,起名字时不要混淆。

2. 构造器与多态

2.1 使用导出类时的构造顺序

通常,构造器不同于其地种类的方法。涉及到多态时仍然是如此。尽管构造器并不具有多态性(它们实际上是static方法,只不过该static声明是隐式的),但还是非常有必要理解构造器怎样通过多态在复杂的层次结构中运作。
基类的构造器总是在导出类的构造过程中被调用,面且按继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。这样做是有意义的,器为构造器具有一特殊任务:检查对象是否被正确地构造。导出类只能访问它自己的成员,不能访问基类中的成员(基类成员通常是private类型)。只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化。因此,必须使得所有的构造器都得到调用,否则就不可能正确构造完整对象。这正是编译器为什么要强制每个导出类部分都必须调用构造器的原因。在导出类的构造器主体中,如果没有明确指定某种基类构造器,它就会自动调用默认构造器。若不存在默认构造器,编译器就会报错(若某个类没有构造器,编译器会自动合成出一个默认构造器)。
复杂对象调用构造器一定要遵照下面的顺序:
  1. 调用默认构造器。这个步骤会不断地反复递归下去。
  2. 按声明顺序调用成员的初始化方法。
  3. 调用导出类构造器的主体。
这种调用顺序重要的。当进行继承时,我们己经知道基类的一切,并且可以访问基类中任向声明为public或protected的成员。这意味着在导出类中,必须假定基类的所有成员都是有效的。一种标准方法是,构造动作一经发生,那么对象所有部分的全体成员都会得到构建。然而,在构造器内部,我们必须确保所要使用的成员都己经构建完毕。为确保这一目的,唯一的办法就是首先调用基类构造器。那么在进入导出类构造器时,在基类中可供我们访问的成员都已得到初始化。此外,知道构造器中的所有成员都有效也是因为,当成员对象在类内进行定义的时候,只要有可能,就应该对它们进行初始化。
若遵循这一规则,那么就能保证所有基类成员说及当前对象的成员对象都被初始化了。但这种捷径并不适用于所有情况。

2.2 清理

通过组合和继承的方法来创建新的类的时候,通常不需要考虑对象的清理的问题。

2.3 构造器内部的多态方法

考虑一下一段的代码:
//: polymorphism/PolyConstructors.java
package polymorphism; /* Added by Eclipse.py */
// Constructors and polymorphism
// don't produce what you might expect.
import static net.mindview.util.Print.*;

class Glyph {
  void draw() { print("Glyph.draw()"); }
  Glyph() {
    print("Glyph() before draw()");
    draw();
    print("Glyph() after draw()");
  }
}	

class RoundGlyph extends Glyph {
  private int radius = 1;
  RoundGlyph(int r) {
    radius = r;
    print("RoundGlyph.RoundGlyph(), radius = " + radius);
  }
  void draw() {
    print("RoundGlyph.draw(), radius = " + radius);
  }
}	

public class PolyConstructors {
  public static void main(String[] args) {
    new RoundGlyph(5);
  }
} /* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*///:~
通过这个程序,我们可以更加精细的了解初始化的顺序,初始化的实际顺序为:
  1. 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零。
  2. 如前所述那样调用基类构造器。此时,调用被覆盖后的draw()方法。
  3. 按照声明的顺序调用成员的初始化方法。
  4. 调用导出类的构造器变体。
因此,编写构造器时有一条有效的准则“用尽可能简单的方法使得对象进入正常状态。如果可以的话,避免调用其他方法。”在构造器内唯一能移安全调用的方法是通过基类中的final方法。(亦适用于private,它们自动属于final方法)。这些方法是不能被覆盖。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值