8 多态

多态是继继承和封装之后的第三种基本特性。

多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序。

“封装”是通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化”把接口和实现分离开来。而多态的作用则是 消除类型之间的耦合。多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要它们都是从同一基类导出而来的。

再论向上转型

缺陷:“覆盖”私有方法

当基类方法是私有的时候,子类不能重载这个私有的方法。

public class PrivateOverride {
  private void f() { print("private f()"); }
  public static void main(String[] args) {
    PrivateOverride po = new Derived();
    po.f();
  }
}

class Derived extends PrivateOverride {
  public void f() { print("public f()"); }
} /* Output:
private f()
*///:~

缺陷:域和静态方法

只有普通方法调用可以是多态的。如果方法某个域,这个访问就将在编译期进行解析,而不是动态绑定的。

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
*///:~

上面的程序中super.field和sub.field分配了不同的存储空间。因为是编译期解析的,所以不具有多态性。这看起来很容易混淆。但我们通常会将所有的域设置成private,因此不能直接访问它们。

如果某个方法是静态的,它的行为就不具有多态性。

构造器与多态

构造器不具有多态性(不能被覆盖),它们实际是static的,只不过该static是隐式的。

构造器调用顺序

调用顺序:总是先调用基类构造器,而后再调用导出类构造器。如果导出类没有明确调用某个构造器,会调用默认构造器。如果不存在默认构造器,编译器就会报错。

成员访问:导出类只能访问自己的成员,不能访问基类中的成员(一般成员都是设置为private的)

//: polymorphism/Sandwich.java
// Order of constructor calls.
package polymorphism;

import static net.mindview.util.Print.*;
class Meal {
    Meal() {
        print("Meal()");
    }
}
class Bread {
    Bread() {
        print("Bread()");
    }
}
class Lunch extends Meal {
    Lunch() {
        print("Lunch()");
    }
}
class PortableLunch extends Lunch {
    PortableLunch() {
        print("PortableLunch()");
    }
}
public class Sandwich extends PortableLunch {
    private Bread b = new Bread();
    public Sandwich() {
        print("Sandwich()");
    }
    public static void main(String[] args) {
        new Sandwich();
    }
} /* Output:
Meal()
Lunch()
PortableLunch()
Sandwich()
*///:~

上面例子中展现了组合,继承及多态在构建上的顺序。先执行子类构造器,然后按声明顺序初始化导出类成员(执行成员构造器),然后执行导出类构造器主体。

继承和清理

子对象通常会留给垃圾回收器进行处理。如果确实需要清理,通常创建dispose()方法。

如果对象即有组合,又有继承,销毁的顺序应该和初始化的顺序相反:对于字段,则销毁的顺序和声明的顺序相反(字段的初始化是按声明的顺序进行的);对于基类,应该首先对导出类清理,然后清理基类(因为基类的某些方法可能在导出类中会起作用)

通常不必执行清理,但一旦选择,就必须要小心。

如果这些成员对象存在一个或多个对象共享的情况, 可以使用引用计数器来跟踪仍旧访问着的对象数量。如下面例子,Shared中含有final long id这样的成员,那么如果要清理,必须使用引用计数器来销毁对象。像这样:

class Shared {
    private int refcount = 0;
    private static long counter = 0;
    private final long id = counter++;	//共享的成员变量

    public Shared() {
        print("Creating " + this);
    }

    public void addRef() {
        refcount++;
    }

    protected void dispose() {
        if (--refcount == 0)
            print("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) {
        print("Creating " + this);
        this.shared = shared;
        this.shared.addRef();	//调用共享的addRef()计算共享数量
    }

    protected void dispose() {
        print("disposing " + this);
        shared.dispose();
    }

    public String toString() {
        return "Composing " + id;
    }
}

public class ReferenceCounting {
    public static void main(String[] args) {
        Shared shared = new Shared();
        Composing[] composing = {new Composing(shared),
                new Composing(shared), new Composing(shared),
                new Composing(shared), new Composing(shared)};
        for (Composing c : composing)
            c.dispose();
    }
}

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

如果再一个构造器的内部调用正在构造的对象的某个动态绑定方法,会发生什么?

比如:父类A有方法a(),子类B继承父类A,重写了方法a()。构造器的调用顺序还是没有变,当new出的是子类对象B时,从基类开始调用,父类构造器若调用了a()方法,实际调用的还是子类的a()方法。示例如下:

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
*///:~

上面代码中结果radius为0,是因为再基类构造器调用draw()方法时,radius属性还未初始化。

之前所诉的初始化不完整,初始化的实际过程是:

  1. 在其他任何事物发生之前,将分配给对象存储空间初始化成二进制的0
  2. 如前所诉调用基类构造器。(因为此时未对属性初始化,所以radius为0)
  3. 按照声明对成员初始化
  4. 调用导出类构造器主体

这里提一点,无论是导出类还是基类,在构造器调用前,都会先对相同层次的成员进行初始化。

协变返回类型

java se5中添加了协变返回类型,它表示在导出类中的被覆盖方法可以返回基类方法返回类型的某种导出类型。

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);
    }
} /* Output:
Grain
Wheat
*///:~

用继承进行设计

我们首先选择“组合”,尤其是不确定选择哪种方式时。

一条通用的准则是:“用继承表达行为上的差异,并用字段表达状态上的变化”。

纯继承与扩展(is-a和is-like-a)

is-a(是一个) 的基类与导出类的行为(方法)是一摸一样的,二者有着完全相同的接口。在向上转型时,永远不需要知道对象的具体类型。

is-like-a(像一个), 它的导出类的行为除了基类的行为外,还有一些扩展的行为。一旦我们向上转型,将不能调用那些扩展的行为。一般情况下,我们都要重新查明对象的确切行为,以便访问那些扩展的行为。


参考《java编程思想》

posted @ 2019-07-08 08:29  星记事  阅读(240)  评论(0编辑  收藏  举报