文章标题

来源:互联网 发布:安广网络电视 编辑:程序博客网 时间:2024/06/07 14:13

Java编程思想 第8章 多态

标签(空格分隔): JAVA学习


  • Java编程思想 第8章 多态
    • 再论向上转型
      • 11 忘记对象类型
    • 2 转机
      • 21 方法调用绑定
      • 22 产生正确的行为
      • 23 可扩展性
      • 24 缺陷覆盖私有方法
      • 25 缺陷域与静态方法
    • 3 构造器和多态
      • 31 构造器的调用顺序
      • 32 继承和清理
      • 23 构造器内部的多态方法的行为
    • 4 协变返回类型
    • 5 用继承进行设计

再论向上转型

在面向对象的程序设计语言中,多态是继数据抽象继承之后的第三种基本特征。

向上转型:对象既可以作为它自己本身的类型使用,也可以作为它的基类类型使用。而这种把对某个对象的引用视为对其基类型的引用的做法被称作向上转型————因为在继承树画法中,基类是放置在上方的。

enum Note {    MIDDLE_C, C_SHARP, B_FLAT; // Etc.}class Instrument {    public void play(Note n) {        System.out.println("Instrument.play()");    }}class Wind extends Instrument {    public void play(Note n) {        System.out.println("Wind.play()");    }}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()*/

Music.tune()方法接受一个Instrument引用,同时也接受任何导出自Instrument的类。

在main方法中,当一个Wind引用传递到tune()方法时,Wind引用会自动向上转型成Instrument。

8.1.1 忘记对象类型

如果让tune()方法直接接受一个Wind引用作为自己的参数,似乎会更为直观。
但这样引发的一个重要问题是:如果那样做,就需要为系统内Instrument的每种新类型都编写一个新的tune()方法。

如果我们只写这样一个简单方法,它仅接收基类作为参数,而不是那些特殊的导出类,会不会更好呢?
这正是多态所允许的。

8.2 转机

8.2.1 方法调用绑定

前期绑定:在程序执行前进行绑定。
后期绑定(动态绑定或运行时绑定):在运行时根据对象的类型进行绑定。

Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。

8.2.2 产生正确的行为

class Shape {    public void draw() {    }    public void erase() {    }}class Circle extends Shape {    @Override    public void draw() {        System.out.println("Circle.draw()");    }    @Override    public void erase() {        System.out.println("Circle.erase()");    }}class Square extends Shape {    @Override    public void draw() {        System.out.println("Square.draw()");    }    @Override    public void erase() {        System.out.println("Square.erase()");    }}class Triangle extends Shape {    @Override    public void draw() {        System.out.println("Triangle.draw()");    }    @Override    public void erase() {        System.out.println("Triangle.erase()");    }}class RandomShapeGenerator {    private Random rand = new Random(47);    public Shape next() {        switch (rand.nextInt(3)) {            default:            case 0:                return new Circle();            case 1:                return new Square();            case 2:                return new Triangle();        }    }}public class Shapes {    private static RandomShapeGenerator gen = new RandomShapeGenerator();    public static void main(String[] args) {        Shape[] s = new Shape[9];        for (int i = 0; i < s.length; i++) {            s[i] = gen.next();        }        for (Shape shp : s) {            shp.draw();        }    }}/* Output:Triangle.draw()Triangle.draw()Square.draw()Triangle.draw()Square.draw()Triangle.draw()Square.draw()Triangle.draw()Circle.draw()*/

Shape基类创建了公用方法draw()和erase(),导出类可以通过覆盖这些方法,来为每种特殊类型的几何形状提供特定的行为。

RandomShapeGenerator是一种“工厂”(factory),在我们每次调用next()方法时,它可以为随机选择的Shape对象产生一个引用。

随机选择几何形状是为了让大家理解:在编译时,编译器不需要获得任何特殊信息就能进行正确的调用。对draw()方法的所有调用都是通过动态绑定进行的。

8.2.3 可扩展性

8.2.4 缺陷:“覆盖”私有方法

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 {    // 此处f()方法改为private,输出结果一样    public void f() {        System.out.println("public f()");    }}/* Output:private f()*/

我们所期望的输出是public f(),但是由于private方法被自动认为是final方法,而且对导出类是屏蔽的。因此,在这种情况下,Derived类中的f()方法就是一个全新的方法;既然基类中的f()方法在子类Derived中不可见,因此甚至也不能被重载。

结论:只有非private方法才可以被覆盖;在导出类中,对于基类中的private方法,最好采用不同的名字。

8.2.5 缺陷:域与静态方法

只有普通的方法调用可以是多态的。例如,如果你直接访问某个域,这个访问就将在编译期进行解析。

class Super {    public int field = 0;    public int getField() {        return field;    }}class Sub extends Super {    public int field = 1;    @Override    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() = 1sub.field = 1; sub.getField() = 1; sub.getSuperField() = 0*/

当Sub对象转型为Super引用时,任何域访问操作都将由编译器解析,因此不是多态的。为了得到Super.field,必须电显式地指明super.field。

如果某个方法是静态的,它的行为就不具有多态性,静态方法是与类,而并非与单个的对象相关联的。

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()";    }}public class StaticPolymorphism {    public static void main(String[] args) {        StaticSuper sup = new StaticSub();        System.out.println(sup.staticGet());        System.out.println(sup.dynamicGet());    }}/* Output:Base staticGet()Derived dynamicGet()*/

8.3 构造器和多态

构造器并不具有多态性(它们实际上是static方法,只不过该static声明是隐式的)。

8.3.1 构造器的调用顺序

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

让我们来看下面这个例子,它展示组合、继承以及多态在构建顺序上的作用:

package com.java.chapter8;/** * Created by weijie on 17-7-10. */class Meal {    Meal() {        System.out.println("Meal()");    }}class Bread {    Bread() {        System.out.println("Bread()");    }}class Cheese {    Cheese() {        System.out.println("Cheese()");    }}class Lettuce {    Lettuce() {        System.out.println("Lettuce()");    }}class Lunch extends Meal {    Lunch() {        System.out.println("Lunch()");    }}class PortableLunch extends Lunch {    PortableLunch() {        System.out.println("PortableLunch()");    }}public class Sandwich extends PortableLunch {    private Bread b = new Bread();    private Cheese c = new Cheese();    private Lettuce l = new Lettuce();    public Sandwich() {        System.out.println("Sandwich()");    }    public static void main(String[] args) {        new Sandwich();    }}/* Output:Meal()Lunch()PortableLunch()Bread()Cheese()Lettuce()Sandwich()*/

上面这例子表明了调用构造器要遵照下面的顺序:

  1. 调用基类的构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,直到最低层的导出类。对应上面的Meal()->Lunch()->PortableLunch()。
  2. 按声明顺序调用成员的初始化方法。对应上面的Bread()->Cheese()->Lettuce()。
  3. 调用导出类构造器的主体。对应上面的Sandwich()。

8.3.2 继承和清理

通过组合和继承方法来创建新类时,永远不必担心对象的清理问题,子对象通常都会留给垃圾回收器进行处理。
如果确实遇到清理的问题,那么必须用心为新类创建dispose()方法(名称自拟),如下面的例子:

package com.java.chapter8;/** * Created by weijie on 17-7-10. */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();    }}public class Frog extends Amphibian {    private Characteristic p = new Characteristic("Croaks");    private Description t = new Description("Eats Bugs");    public 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();    }}/* Output:Creating Characteristic is aliveCreating Description Basic Living CreatureLivingCreature()Creating Characteristic has heartCreating Description Animal not VegetableAnimal()Creating Characteristic can live in waterCreating Description Both water and landAmphibian()Creating Characteristic CroaksCreating Description Eats BugsFrog()Bye!Frog disposedisposing Description Eats Bugsdisposing Characteristic CroaksAmphibian disposedisposing Description Both water and landdisposing Characteristic can live in waterAnimal disposedisposing Description Animal not Vegetabledisposing Characteristic has heartLivingCreature disposedisposing Description Basic Living Creaturedisposing Characteristic is alive*/

销毁的顺序应该和初始化顺序相反。

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

先看个例子:

package com.java.chapter8;/** * Created by weijie on 17-7-10. */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);    }}public class PolyConstructors {    public static void main(String[] args) {        new RoundGlyph(5);    }}/* Output:Glyph() before draw()RoundGlyph.draw(), radius = 0Glyph() after draw()RoundGlyph.RoundGlyph(), radius = 5*/

Glyph.draw()方法设计为将要被覆盖,这种覆盖是在RoundGlyph中发生的。但是Glyph构造器会调用这个方法,结果导致了对RoundGlyph.draw()的调用,这看起来似乎是我们的目的。

但是如果看到输出结果,我们会发现当Glyph的构造器调用draw()方法时,radius不是默认初始值1,而是0。

前一节讲述的初始化顺序并不十分完整,而这正是解决这一谜题的关键所在。初始化的实际过程是:

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

在逻辑方面,该程序没有问题,并且编译器也没有报错,但结果却不可思议的错了。

因此,编写构造器时有一条有效的准则:“用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法”。

在构造器内唯一能够安全调用的那些方法是基类中的final方法(也适用于private方法,它们自动属于final方法)。

8.4 协变返回类型

Java SE5中添加了协变返回类型,它表示在导出类(WheatMill)中的被覆盖方法(process)可以返回基类方法(process)的返回类型(Grain)的某种导出类型(Wheat):

package com.java.chapter8;/** * Created by weijie on 17-7-10. */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:GrainWheat*/

8.5 用继承进行设计

一条通用的准则是:“用继承表达行为间的差异,并用字段表达状态上的变化”。
下面的这个例子,两者都用到了:通过继承得到了两个不同的类,用于表达act()方法的差异;而Stage通过运用组合使自己的状态发生变化。在这种情况下,这种状态的改变也就产生了行为的改变。

package com.java.chapter8;/** * Created by weijie on 17-7-11. */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();    }}public class Transmogrify {    public static void main(String[] args) {        Stage stage = new Stage();        stage.performPlay();        stage.change();        stage.performPlay();    }}/* HappyActorSadActor*/
原创粉丝点击