Java编程思想——多态

8 多态

在面向对象设计语言中,多态是继封装,继承之后的第三种基本特征。多态通过分离做什么和怎么做,从另一个角度将接口和实现分离。

8.1 再论向上转型

已知对象既可以作为它自己本身的类型使用,也可以作为它的基本类型使用。这种把某个对象的引用视为其基类型的引用的做法被称作向上转型。
例如前述的基类Instrument和导出类Wind。如果在Music类中有一个方法表示演奏,假设它的引用是Instrument类,例如tune(Instrument i),则Wind也可以调用这个方法,将自己当做Instrument类。如果这个方法的引用是Wind类,例如tune(Wind i),则如果添加新的乐器进行演奏,例如Stringed类和Brass类,则不能调用这个方法,需要分别添加tune(Stringed i)tune(Brass i)方法。这使得需要大量的工作。

也就是说只写这么一个简单的方法,仅接受基类作为参数,例如tuen(Instrument i),而不是那些特殊的导出类。这样不管导出类是什么,都只要和基类打交道,而这正是多态所允许的。

8.2 转机

例如前述的方法tune(Instrument i)方法,它接受一个一个Instrument引用,如何判断这个引用指向的是一个Wind对象,而不是Brass对象或者Stringed对象?实际上编译器不知道,这里涉及到绑定这个话题

8.2.1方法调用绑定

将一个方法调用同一个方法主体关联起来被称为绑定。在程序执行前进行绑定,叫做前期绑定(面向过程)。这里解决前述问题的方法是后期绑定。

后期绑定的含义是:在运行时根据对象的类型进行绑定。后期绑定也可以叫做动态绑定或者运行时绑定。如果一种语言想要实现后期绑定,就必须实现某种机制,以便在运行时能够判断对象的类型,从而调用恰当的方法。也就是编译器一直不知道对象的类型,但是在对象调用机制能找到正确的方法体,并加以调用。

Java中除了static和final方法,其他所有方法都是后期绑定。**关于多态或者可以这说:**Java中有两种类型:编译时类型和运行时类型。编译时类型由声明该变量时使用的类型决定的,运行时类型是由实际赋给该变量的对象决定的。若编译时类型和运行时类型不一致,就出现多态,调用方法的版本,是实际赋给该变量的对象决定的。
例如:

Person是父类,Man是子类
Person p = new Man();//体现子类对象的多态性父类的引用指向子类对象。

一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法,

    Student m = new Student();
    m.school = “pku”;   //合法,Student类有school成员变量
    Person e = new Student(); 
    e.school = “pku”;   //非法,Person类没有school成员变量

属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误。即调用的方法父类和子类都有,即子类对父类方法的重写。(成员变量不具备多态性

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

例如

//: polymorphism/PrivateOverride.java
// Trying to override a private method.
package polymorphism;
import static net.mindview.util.Print.*;

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

我们期望输出是public f(),但是由于private方法被自动认为是一个final方法,对导出类是屏蔽的,因此这时候,Derived类的f()方法是一个全新的方法。
结论:只有非private方法才可以被覆盖。导出类中,对于基类中private方法,最好采用不同的名字。

8.2.3 缺陷:域与静态方法

关于域,即前述的属性,它是不存在多态性的。例如:

//: polymorphism/FieldAccess.java
// 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
*///:~

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

//: polymorphism/StaticPolymorphism.java
// Static methods are not polymorphic.

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(); // Upcast
    System.out.println(sup.staticGet());
    System.out.println(sup.dynamicGet());
  }
} /* Output:
Base staticGet()
Derived dynamicGet()
*///:~

8.3 构造器和多态

构造器不同于其他种类的方法,不具有多态性。(他们实际上是static方法,不过是隐式声明)。

8.3.1 构造器的调用顺序

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

  1. 调用基类构造器,这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,知道最底层导出类。
  2. 按照声明顺序调用成员的初始化方法。
  3. 调用导出类构造器主体。

例如:

//: 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 Cheese {
  Cheese() { print("Cheese()"); }
}

class Lettuce {
  Lettuce() { print("Lettuce()"); }
}

class Lunch extends Meal {
  Lunch() { print("Lunch()"); }
}

class PortableLunch extends Lunch {
  PortableLunch() { print("PortableLunch()");}
}

public class Sandwich extends PortableLunch {
  private Bread b = new Bread();
  private Cheese c = new Cheese();
  private Lettuce l = new Lettuce();
  public Sandwich() { print("Sandwich()"); }
  public static void main(String[] args) {
    new Sandwich();
  }
} /* Output:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
*///:~

8.3.2 继承与清理

通过组合和继承方法来创建新类,一般不必担心对象的清理问题,子对象通常都会留给垃圾回收器进行处理。如果确实遇到清理的问题,那么必须用心为新类创建dispose()方法(这个名称随意)。由于继承的缘故,如果有其他作为垃圾回收一部分的特殊清理工作,就必须在导出类中覆盖dispose()方法。当覆盖被继承类的dispose()方法,务必调用基类版本的dispose()方法,否则,清理动作就不会发生。
例如:

//: polymorphism/Frog.java
// Cleanup and inheritance.
package polymorphism;
import static net.mindview.util.Print.*;

class Characteristic {
  private String s;
  Characteristic(String s) {
    this.s = s;
    print("Creating Characteristic " + s);
  }
  protected void dispose() {
    print("disposing Characteristic " + s);
  }
}

class Description {
  private String s;
  Description(String s) {
    this.s = s;
    print("Creating Description " + s);
  }
  protected void dispose() {
    print("disposing Description " + s);
  }
}

class LivingCreature {
  private Characteristic p =
    new Characteristic("is alive");
  private Description t =
    new Description("Basic Living Creature");
  LivingCreature() {
    print("LivingCreature()");
  }
  protected void dispose() {
    print("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() { print("Animal()"); }
  protected void dispose() {
    print("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() {
    print("Amphibian()");
  }
  protected void dispose() {
    print("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() { print("Frog()"); }
  protected void dispose() {
    print("Frog dispose");
    t.dispose();
    p.dispose();
    super.dispose();
  }
  public static void main(String[] args) {
    Frog frog = new Frog();
    print("Bye!");
    frog.dispose();
  }
} /* Output:
Creating Characteristic is alive
Creating Description Basic Living Creature
LivingCreature()
Creating Characteristic has heart
Creating Description Animal not Vegetable
Animal()
Creating Characteristic can live in water
Creating Description Both water and land
Amphibian()
Creating Characteristic Croaks
Creating Description Eats Bugs
Frog()
Bye!
Frog dispose
disposing Description Eats Bugs
disposing Characteristic Croaks
Amphibian dispose
disposing Description Both water and land
disposing Characteristic can live in water
Animal dispose
disposing Description Animal not Vegetable
disposing Characteristic has heart
LivingCreature dispose
disposing Description Basic Living Creature
disposing Characteristic is alive
*///:~

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

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

如果一个构造器内部调用正在构造的对象的某个动态绑定方法,则会怎样?

如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义。然而这个调用的效果可能相当难以预料,因为被覆盖的方法在对象被完全构造之前就会被调用,可能造成一些难以发现的隐藏错误。
构造器的工作是创建对象,在构造器内部,整个对象可能只是部分形成——我们只知道基类对象已经进行初始化。如果我们在构造器中调用某个方法,可能这个方法所操纵的成员还未初始化——这会导致灾难。
例如:

//: polymorphism/PolyConstructors.java
// 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
*///:~

因此,编写构造器的时候有一条有效准则:用尽可能的简单的方法使对象进入正常状态;如果可以,避免调用其他方法。构造器中唯一安全调用的那些方法是基类中的final方法。

8.4 协变返回方法

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

//: polymorphism/CovariantReturn.java

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

Java SE5与Java较早版本之间的区别的主要差异是较早版本将强制process()的覆盖版本必须返回Grain,而不能返回Wheat,尽管Wheat是从Grain导出的,因而也应该是一种合法的返回类型。协变返回类型允许返回更具体的Wheat类型。

8.5 用继承进行设计

在学习多态后,看起来似乎所有的东西都可以被继承,事实上,如果首先考虑使用继承技术,反倒会加重我们的设计负担,使事情变得不必要的复杂起来。更好的方式是首先选择“组合”,尤其是不能够确定使用哪一种方式时。使用组合更灵活,相反,继承在编译时就需要知道确切类型。
例如:

//: polymorphism/Transmogrify.java
// Dynamically changing the behavior of an object
// via composition (the "State" design pattern).
import static net.mindview.util.Print.*;

class Actor {
  public void act() {}
}

class HappyActor extends Actor {
  public void act() { print("HappyActor"); }
}

class SadActor extends Actor {
  public void act() { print("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();
  }
} /* Output:
HappyActor
SadActor
*///:~

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

8.5.1 纯继承与扩展

纯继承就是纯粹的将基类中已经建立的方法才可以在导出类中被覆盖。这被称为纯粹的“is-a”关系。因为一个类的接口已经确定了它应该是什么样,继承可以确保所有导出类具有基类的接口,绝对不会少,导出类也将具有基类一样的接口。可以看作是一个纯粹的替代,导出类完全替代基类。
纯继承

还有一种是他有着和基类相同的接口,但是它还具有额外方法实现其他特性,这个可以称作为“is-like-a”关系。
导出类扩展

对于上述这种方法,有一种缺点就是导出类中扩展的接口不能被基类访问,因此,一旦我们向上转型了,就不能调用那些扩展方法。

8.5.2 向下转型和运行时类型识别

向上转型会丢失具体类型信息,可以通过向下转型——基类到导出类——获取类型信息。向上转型是安全的,因为基类不会具有大于导出类的接口,因此通过基类接口发送的消息保证都能被接受,但是对于向下转型,例如无法知道一个“几何形状”它确实是一个“圆”,它可以是“正方形”,“三角形”等等。

要解决这个问题,必须有某种方法来保证向下转型的安全性,不至于转型到一种错误类型,进而发出对象无法接受的消息。
为了解决这个问题,java中提供了instacneof操作符,用来确保向下转型的正确性。
x instanceof A:检验x是否为类A的对象,返回值为boolean型。
即对象a instanceof 类A 判断对象a是否是类A的一个实例,是的话返回true。
 要求x所属的类与类A必须是子类和父类的关系,否则编译错误。
 如果x属于类A的子类B,x instanceof A值也为true。
例如:

public class Main{
    int ss=1;
    public static void main(String[] args) {
        Main m=new Subclass();
        if(m instanceof Subbclass){
            Subclass t = (Subclass)m;
            t.getinfor();

        }else if(m instanceof Subbclass){
            Subbclass t=(Subbclass)m;
            t.getinfor();
        }

    }
    void getinfor(){
        System.out.println("Main");
    }
}
class Subclass extends Main{

    Subclass(){
        super();

    }
    void getinfor(){
        System.out.println("Subclass");
    }

}
class Subbclass extends Main{

    Subbclass(){
        super();

    }
    void getinfor(){
        System.out.println("Subbclass");
    }

}/*output:
Subbclass
*/

从父类到子类的类型转换必须通过强制类型转换实现,无继承关系的引用类型间的转换是非法的。
子父类类型转换

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值