第八章 多态
多态(动态绑定、后期绑定或运行时绑定)分离做什么和怎么做,从另外一个角度讲接口和实现分离。
8.1 再论向上转型
将一个对象的引用同时看作其父类的引用的做法称为向上转型。
8.1.1 忘记对象类型
在方法中只接受父类作为参数,则其不同的子类对象也可作为参数传入。
8.2 转机
8.2.1 方法调用绑定
绑定:将一个方法调用同一个方法主体关联起来称为绑定。
前期绑定:若在程序执行前进行绑定(由编译器和连接程序实现),叫做前期绑定。
后期绑定:在运行时根据对象的类型绑定,也称为动态绑定或运行时绑定。
要实现后期绑定,就必须在运行时能够根据对象判断其类型,从而调用恰当的方法。
需要注意的是,后期绑定的前提是具有继承关系,并且在父类中有方法在子类中被重写。
如果某个方法在父类中被声明为final(或者是private),显然不能被子类继承,所以也就不存在后期绑定的说法。
对于static方法而言,虽然子类可以继承父类的静态方法,但是却不能重写,即便子类声明了与父类同名的静态方法,也只不过是将父类的相应静态方法隐藏而已。因此,static方法也不具备后期绑定的特性。
除了static方法和private方法之外,其他方法都是后期绑定的。
8.2.2 产生正确的行为
有了后期绑定之后,就可以只编写与父类打交道的代码了。
8.2.3 可扩展性
不需要改变原有代码,只需要编写新类去继承某一父类,即可用参数为父类引用的方法操作新类对象。
8.2.4 缺陷:”覆盖”私有方法
final方法不具备多态的性质。
public class PrivateOverride {
private void f(){
System.out.println("private f()");
}
public static void main(String[] args){
PrivateOverride po = new Derived();
po.f();
}
}
class Derived extends PrivateOverride{
public void f(){
System.out.println("public f()");
}
}
上面代码输出的是private f()。由于父类的f()方法被声明为private,所以子类Derived不能继承该方法,而子类中的f()方法只是子类新添加的方法,只是方法名相同而已。虽然说是父类引用指向子类对象,但是引用po是父类引用,它只能调用父类的方法,而子类的f()方法,它是不具备的。
针对父类的private方法,子类在新增方法时最好采用不同的名字,以免引起歧义。
8.2.5 缺陷:域与静态方法
针对域(成员变量):
public class FieldAccess {
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());
}
}
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;
}
}
当Sub对象向上转型为Super引用时,对任何域的访问操作都将由编译器解析,因此不具备多态特性。在上述代码中,Super.field和Sub.field分配了不同的存储空间。实际上,Sub包含了两个field,一个是它本身的field,另外一个是从父类继承过来的field,但默认的field还是它自己的field,要想得到从父类继承的field,必须显式地声明super.field。
这种做法一般在实际中不会发生,一方面是因为域一般是声明为private,另一方面也是在子类不会声明与父类名字相同的域。
针对静态方法:
public class StaticPolymorphism {
public static void main(String[] args){
StaticSuper sup = new StaticSub();
System.out.println(sup.staticGet());
System.out.println(sup.dynamicGet());
}
}
class StaticSuper{
public static String staticGet(){
return "Base staticGet()";
}
public String dynamicGet(){
return "Base dynamicGet()";
}
}
class StaticSub extends StaticSuper{
public static String staticGet(){
return "Derived staticGet()";
}
public String dynamicGet(){
return "Derived dynamicGet()";
}
}
静态方法也不具备多态的特性。
8.3 构造器与多态
8.3.1 构造器的调用顺序
在创建子类对象时,总是要先调用父类的构造器(无论是无参构造器还是有参构造器),再调用子类本身的构造器。
8.3.2 继承与清理
在对子类对象进行清理时,如果除了父类的垃圾需要清理外,还需要清理别的垃圾,就必须要覆盖父类的清理方法,并且在方法的末尾显式地调用父类的清理方法。
public class Frog extends Amphibian{
private Characteristic p = new Characteristic("Croaks");
private Description t = new Description("Eats Bugs");
Frog(){
System.out.println("Frog()");
}
protected void dispose(){
System.out.println("Frog dispose");
t.dispose();
p.dispose();
super.dispose();
}
public static void main(String[] args){
Frog frog = new Frog();
System.out.println("Bye!");
frog.dispose();
}
}
class Characteristic{
private String s;
Characteristic(String s){
this.s = s;
System.out.println("Creating Characteristic " + s);
}
protected void dispose(){
System.out.println("disposing Characteristic " + s);
}
}
class Description{
private String s;
Description(String s){
this.s = s;
System.out.println("Creating Description " + s);
}
protected void dispose(){
System.out.println("disposing Description " + s);
}
}
class LivingCreature{
private Characteristic p = new Characteristic("is alive");
private Description t = new Description("Basic Living Creature");
LivingCreature(){
System.out.println("LivingCreature()");
}
protected void dispose(){
System.out.println("LivingCreature dispose");
t.dispose();
p.dispose();
}
}
class Animal extends LivingCreature{
private Characteristic p = new Characteristic("has heart");
private Description t = new Description("Animal not Vegetable");
Animal(){
System.out.println("Animal()");
}
protected void dispose(){
System.out.println("Animal dispose");
t.dispose();
p.dispose();
super.dispose();
}
}
class Amphibian extends Animal{
private Characteristic p = new Characteristic("can live in water");
private Description t = new Description("Both water and land");
Amphibian(){
System.out.println("Amphibian()");
}
protected void dispose(){
System.out.println("Amphibian dispose");
t.dispose();
p.dispose();
super.dispose();
}
}
清理顺序与初始化顺序相反。
在上个例子中,Frog对象拥有自己的成员对象,而且只要Frog存活,只要不显式清除,其成员对象的存活周期应该与Frog一致。但是当多个对象共享同一个成员对象时,情况就变得比较复杂,必须使用引用计数的手段了。
public class ReferenceCounting {
public static void main(String[] args){
Shared shared = new Shared();
Composing[] composings = {
new Composing(shared),
new Composing(shared),
new Composing(shared),
new Composing(shared),
new Composing(shared),
};
for(Composing c : composings){
c.dispose();
}
}
}
class Shared{
private int refcount = 0;
private static long counter = 0;
private final long id = counter++;
public Shared(){
System.out.println("Creatring " + this);
}
public void addRef(){
refcount++;
}
protected void dispose(){
if(--refcount == 0){
System.out.println("Disposing " + this);
}
}
public String toString(){
return "Shared " + id;
}
}
class Composing{
private Shared shared;
private static long counter = 0;
private final long id = counter++;
public Composing(Shared shared){
System.out.println("Creating " + this);
this.shared = shared;
this.shared.addRef();
}
protected void dispose(){
System.out.println("disposing " + this);
shared.dispose();
}
public String toString(){
return "Composing " + id;
}
}
8.3.3 构造器内部的多态方法的行为
如果在父类的构造器中调用了某个非静态非final的方法(f()方法),在其子类中有对这一方法进行覆盖,此时创建子类对象时,情况就变得有点复杂:
public class PolyConstructors {
public static void main(String[] args){
new RoundGlyph(5);
}
}
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.RoundGlyph(), radius = " + radius);
}
void draw(){
System.out.println("RoundGlyph.draw(), radius = " + radius);
}
}
Glyph.draw()方法在子类RoundGlyph中被覆盖。在创建RoundGlyph对象时,在先调用的父类构造器中,其调用的draw()方法就是RoundGlyph类中的draw()方法,但是注意radius的值,此时其值不是1,而是0。我觉得应该是radius的初始化(即将其赋值为1)还没进行,所以其值是0。
一般来说,在构造器中应避免调用其他方法,即便要调用也应该调用final方法。
8.4 协变返回类型
在子类已覆盖的方法中,可以返回父类方法返回类型的子类,这是JDK 5之后加入的特性。
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);
}
}
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();
}
}
8.5 用继承进行设计
优先考虑组合,慎用继承。
状态设计模式:
public class Transmogrify {
public static void main(String[] args){
Stage stage = new Stage();
stage.performPlay();
stage.change();
stage.performPlay();
}
}
class Actor{
public void act(){
}
}
class HappyActor extends Actor{
public void act(){
System.out.println("HappyActor");
}
}
class SadActor extends Actor{
public void act(){
System.out.println("SadActor");
}
}
class Stage{
private Actor actor = new HappyActor();
public void change(){
actor = new SadActor();
}
public void performPlay(){
actor.act();
}
}
用继承表达行为之间的差异,用字段表达状态上的变化。
8.5.1 春继承与扩展
is-a关系:子类继承父类的成员,并不添加新的成员。
is-like-a关系:子类继承父类的成员,并添加新的成员,在此种情况下,当进行向上转型时,新扩展的功能就无法使用父类引用访问。
8.5.2 向下转型与运行时类型识别
向下转型:将父类对象转变成子类对象,这种转型是有风险的。
在Java中,所有的转型都需要检查,以便确保它的确是所需类型,否则会抛出类型转换异常(ClassCastException)。