多态

多态

多态,也称作动态绑定、后期绑定或运行时绑定。
多态的作用是消除 类型 之间的耦合关系。
多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要是从同一父类导出的。区别是根据方法的行为的不同表现出来,虽然只写方法都可以通过同一个父类来调用

“封装” 通过合并特征和行为来创建新的数据类型。
“实现隐藏” 通过将细节 “私有化” 把接口和实现分离开来。
多态通过分离做什么和怎么做,从另一个角度将接口和实现分离。



再论向上转型

  • 把某个对象的引用视为对其基类的引用的做法叫做 向上转型
  • 在参数列表中接受一个父类的引用,同时也能接受任何次父类的子类。在传递参数中不需要进行类型转换。

    因为子类从父类继承而来,父类的接口必定存在子类中。子类向上转型过程中可能会“缩小”接口,不会比父类的全部借口还要小

public class Music {
    public static void tune(Instrument i){//方法接受父类为参数
        i.play(Note.MIDDLE_C);
    }
    public static void main(String[] args) {
        Instrument i = new Wind();
        tune(i);//out:Wind.play MIDDLE_C
    }
}

enum Note{
    MIDDLE_C,C_SHARP,B_FLAT;
}

class Instrument{
    public void play(Note n){
        System.out.println("Instrument.play " + n);
    }
}

class Wind extends Instrument{
    @Override
    public void play(Note n) {
        System.out.println("Wind.play " + n);
    }
}

忘记对象类型

  • 如果方法将子类作为参数,那么如果增加子类就需要为每个子类都编写一个新的方法。

  • 将父类作为参数,编写的代码只与父类打交道,就可以松散与子类的耦合。

    class Wind extends Instrument{
      @Override
      public void play(Note n) {
          System.out.println("Wind.play " + n);
      }
    }
    
    class Brass extends Instrument{
      @Override
      public void play(Note n) {
          System.out.println("Brass.play " + n);
      }
    }
    //如果接受子类为参数,每增加一个子类就需要重载一次方法
    public static void tune(Wind i){
    i.play(Note.MIDDLE_C);
    }
    public static void tune(Brass i){
    i.play(Note.MIDDLE_C);
    }


转机

方法接受的是父类的引用,但是它却正确调用到了子类中重写了父类的方法。编译器如何知道参数引用指向哪一个具体的子类呢?实际上编译器无法得知,这就牵涉到了 绑定 的话题

    public static void tune(Instrument i){//方法接受父类为参数
        i.play(Note.MIDDLE_C);
    }
    public static void main(String[] args) {
        Instrument i = new Wind();
        tune(i);//out:Wind.play MIDDLE_C
    }

方法调用绑定

  • 将一个方法调用同一个方法主体关联起来叫做 绑定

  • 在程序执行前进行绑定,叫做 前期绑定

    可能我们从来没听说过,因为它是面向过程的语言中不需要选择就默认的绑定方式,例如,C只有一种方法调用,就是前期绑定

  • 在运行时根据对象的类型进行绑定,叫做后期绑定运行时绑定动态绑定)。

    实现后期绑定就必须具有某种机制,以便在运行时判断对象类型,从而调用恰当的方法。也就是说,编译器一直不知道对象的类型,但方法调用机制能找到正确的方法体

  • java中除了static方法和final方法外(priavte属于final方法)外,都是后期绑定。


产生正确的行为

Shape s = new Circle();//Circle继承自Shape
s.drow();
//你可能以为调用的是Shape的drow()。因为他是一个Shape引用,由于后期绑定,还是调用了Circle.drow()方法。
  • 父类为所有子类建立公用接口
  • 子类通过导出覆盖(重写)这些定义,为每种特殊类型提供单独的行为。
public class Shape {
    protected void draw(){}
    protected void erase(){}

    public static void main(String[] args) {
        Shape c = new Circle();
        c.draw();
        c.erase();
        Shape s = new Square();
        s.draw();
        s.erase();
      /*out
        Circle draw
        Circle erase
        Square draw
        Square erase
      */
    }
}
class Circle extends Shape{
    @Override
    protected void draw() {
        System.out.println("Circle draw");
    }

    @Override
    protected void erase() {
        System.out.println("Circle erase");
    }
}
class Square extends Shape{
    @Override
    protected void draw() {
        System.out.println("Square draw");
    }

    @Override
    protected void erase() {
        System.out.println("Square erase");
    }
}

可扩展性

  • 因为多态的极致,我们可以根据需求添加新的类型(从父类继承而来),而不用更改方法(以父类为参数)

  • 在一个良好的OOP程序中,大多数或所有方法都会遵循这个模型,而且只与父类接口通信。这样的程序就是可扩展的。

    因为可以从通用的父类继承出新的数据类型,从而添加一些新功能。那些操纵父类接口的方法不需要任何改动就可以应用于新类


缺陷:“覆盖”私有方法

  • 只有非private方法才可以被覆盖。
  • static 和 final 修饰的都不能覆盖,当然可以加上private,那就是另个方法了
class A {
    final void f(){}
    static void c(){}
    private void  b(){}
}

class B extends A{
    public void f(){}//error
    void c(){}//error
    public void b(){}
}

缺陷:域与静态方法

  • 只有普通的方法调用可以是多态的

    当子类转换为父类时,任何访问域的操作都将由编译器解析。它将会访问父类中域的值,因此不是多态的。

    父类中的域和子类中的会分配不同的存储空间。子类实际上会包含两个域,自身和父类中的。没有向上转型时,获取就是自身的。

    class A{
      int i = 0;
    }
    class B extends A{
      int i = 1;
    }
    public class FieldAccess {
      public static void main(String[] args) {
          A a  = new B();
          B b = new B();
          System.out.println(a.i);//out:0
          System.out.println(b.i);//out:1
      }
    }

  • 静态方法是与类,而并非与单个的对象相关联的,所以也不具有多态性。

    就是看是索引是父类还是子类啦。

class StaticSuper{
    static String staticGet(){
        return "Super staticGet";
    }
    String dynamicGet(){
        return "Super dynamicGet";
    }
}
class StaticSub extends StaticSuper{
    static String staticGet(){
        return "Sub staticGet";
    }
    String dynamicGet(){
        return "Sub dynamicGet";
    }
}
public class StaticPolymorphism {
    public static void main(String[] args) {
        StaticSuper sup = new StaticSub();
        StaticSub sub = new StaticSub();
        System.out.println(sup.staticGet());//out:Super staticGet
        System.out.println(sub.staticGet());//out:Sub staticGet
        System.out.println(sup.dynamicGet());//out:Sub dynamicGet
    }
}


构造器和多态

构造器不同于其他种类的方法。尽管构造器并不具有多态性(隐式的static声明)但还是要理解构造器怎样通过多态在复杂的层次运作。


构造器的调用顺序

  • 调用父类构造器。这个步骤会反复递归下去,首先是构造这种层次结构的根,然后是下一层子类,最终到最底层的子类

    父类构造器总是在子类构造过程中被调用,按照继承层次逐渐向上链接,使每个父类的构造器都被调用。因为构造器有一项特殊任务,检查对象是否被正确构造。子类只能方法自己的成员,不能访问父类中的成员(父类中通常private类型),只有父类的构造器才具有恰当的知识和权限对自己的元素初始化。必须让所有构造器都得到调用,否则就不可能正确构造完整的对象。这正是编译器为什么强调每个子类必须调用构造器的原因。在子类么有明确指定调用哪个构造器,它会默默调用默认构造器。如果不存在默认构造器,编译器就会报错

  • 按声明顺序调用成员的初始方法。

  • 调用子类构造器的主体。

class Meal{
    Meal(){
        System.out.println("Meal()");
    }
}
class Bread {
    Bread(){
        System.out.println("Bread()");
    }
}
class Lettuce extends Bread{
    Lettuce(){
        System.out.println("Lettuce()");
    }
}
class Lunch extends Meal{
    Lunch(){
        System.out.println("Lunch()");
    }
}
class PortableLunch extends Lunch{
    private Lettuce lettuce = new Lettuce();
    PortableLunch(){
        System.out.println("PortableLunch()");
    }
}
public class Sandwich extends PortableLunch {
    public static void main(String[] args) {
        new Sandwich();
// out:
//        Meal()
//        Lunch()
//        Bread()
//        Lettuce()
//        PortableLunch()
    }
}

继承与清理

  • 通过组合和继承方法来创建新类时,永远不必担心对象的清理问题,子对象通常会留给垃圾回收器处理。如果确实遇到清理的问题,必须为新类创建相应的清理方法,并且如果是继承,必须在子类中也调用父类的清理方法。否则父类的清理就不会发生

    对象的销毁顺序应该和初始化顺序相反。对于字段,意味着与声明的顺序相反(因为字段的初始化是按照申明的顺序进行的),应该首先对子类清理,在对父类,因为子类可能调用父类的某些方法

class Characteristic{
    private String s;
    Characteristic(String s){
        this.s = s;
        System.out.println("Create Characteristic " + s);
    }
    protected void dispose(){
        System.out.println("dispose Characteristic" + s);
    }
}
class Description{
    private String s;
    Description(String s){
        this.s = s;
        System.out.println("Create Description " + s);
    }
    protected void dispose(){
        System.out.println("dispose 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();
    }
}
public class Frog extends LivingCreature{
    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();
    }
}
/* out
Create Characteristic is alive
Create Description Basic Living Creature
LivingCreature()
Create Characteristic Croaks
Create Description Eats Bugs
Frog()
Bye!
Frog dispose
dispose DescriptionEats Bugs
dispose CharacteristicCroaks
LivingCreature dispose
dispose DescriptionBasic Living Creature
dispose Characteristicis alive
 */
  • 如果成员对象中存在与其他一个或多个对象共享的情况下,就不能简单调用销毁方法。这种情况就必须使用 引用 计数跟踪访问着共享对象的对象数量。
class Shared{
    private int refcount = 0;//被引用计数
    private static long counter = 0;//内存中数据就一份,用来生成id
    private final long id = counter++;//final,生命周期中不会被改变
    public Shared(){
        System.out.println("Createing " + this);
    }
    public void addRef(){//引用加一
        refcount++;
    }
    protected void dispose(){
        //判断引用为0的时候再销毁
        if (--refcount==0){
            System.out.println("Disposing " + this);
        }
    }

    @Override
    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();
    }

    @Override
    public String toString() {
        return "Composing" + id;
    }
}
public class ReferenceCounting {
    public static void main(String[] args) {
        Shared shared = new Shared();
        Composing[] composings = {new Composing(shared),new Composing(shared),new Composing(shared)};
        for (Composing c : composings)
            c.dispose();
    }
}

/* out
Createing Shared 0
Creating Composing0
Creating Composing1
Creating Composing2
disposing Composing0
disposing Composing1
disposing Composing2
Disposing Shared 0
*/

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

一般方法内部,动态绑定的调用实在运行时才决定的,因为对象无法知道,它是属于方法所在的那个类,还是那个类的子类。

如果在构造器内部调用一个动态绑定方法,就要用到那个方法的被覆盖后的定义然而这个结果可能难以预料,因为被覆盖的方法在对象被完全构造之前被调用。

如果A是父类,B是子类,A中构造器会调用一个自身的方法f(),然而这个方法在B类中覆盖了。我们在创建B对象的时候,动态绑定就会在执行A类构造方法时,调用到B类的方法f(),而这个时候B类还没有进行初始化。

java中是如何处理这种情况呢,之前讲述的初始化顺序并不完整,实际过程是。

  • 在其他任何食物发生之前,将分配给对象的存储空间初始化成二进制的0.
  • 如之前所述调用父类构造器。此时,调用覆盖后的draw()方法。(在RoundGlyph构造器之前被调用),由于上一步骤的缘故,发现 radius 被初始化成0。
  • 按照声明的顺序调用成员的初始化方法。
  • 调用子类的构造器主体。
class Glpyh{
    void draw(){
        System.out.println("Glpyh draw");
    }
    Glpyh(){
        System.out.println("Glpyh beforw draw");
        draw();
        System.out.println("Glpyh after draw");
    }
}
class RoundGlyph extends Glpyh{
    private int radius = 1;
    RoundGlyph(int r){
        System.out.println("RoundGlyph.RoundGlyph() radius = " +radius);
        radius = r;
        System.out.println("RoundGlyph.RoundGlyph() radius = " +radius);
    }
    @Override
    void draw() {
        System.out.println("RoundGlyph.draw()  radius = " +radius);
    }
}
public class PolyConstructors {
    public static void main(String[] args) {
        new Glpyh();
        System.out.println("----------");
        new RoundGlyph(2);
    }
}
/*out
Glpyh beforw draw
Glpyh draw
Glpyh after draw
----------
Glpyh beforw draw
RoundGlyph.draw()  radius = 0
Glpyh after draw
RoundGlyph.RoundGlyph() radius = 1
RoundGlyph.RoundGlyph() radius = 2
*/


协变返回类型

  • Java SE5中添加了协变返回类型,它表示在子类中的被覆盖方法可以返回父类方法的返回类型的某种子类。允许返回更具体的类型
class Grain{
    @Override
    public String toString() {
        return "Grain";
    }
}
class Wheat extends Grain{
    @Override
    public String toString() {
        return "Wheat";
    }
}
class Mill{
    Grain process(){
        return new Grain();
    }
}
class WheatMill extends Mill{
    @Override
    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);
    }
}
/*  out
Grain
Wheat
*/


用继承进行设计

虽然多态是一种奇妙的工具,但我们创建新类时,更好的方式应该首先选择 组合 ,尤其是不能十分确定应该使用哪种方式时。组合不会强制我们的 程序设计 进入继承的 层次结构 中。而且组合更加灵活,它可以动态选择类型(选择了行为);相反,继承在编译时就需要知道确切类型。

  • 用继承表达行为间的差异,并用字段表达状态上的变化。

    通过继承得到了两个不同的类,用于表达act()方法的差异;而Stage通过运用组合使自己的状态发生改变。在这种状态也就产生了行为的改变。

    通过继承,我们得到了两个不同的类,这两个类代表不同的状态,类中各自实现了不同状态下的某个方法。在组合中,我们把这个两个类的父类当作是新类的一个子类,并通过方法实例化不同子类,并在组合中去调用子类中的方法。这样,状态类改变了,调用的方法也就改变了。

class Actor{
    void act(){}
}
class HappyActor extends Actor{
    @Override
    void act() {
        System.out.println("HappyActor");
    }
}
class SadActor extends Actor{
    @Override
    void act() {
        System.out.println("SadActor");
    }
}
class Stage{
    //Stage包含一个Actor的引用,actor被初始化为HappyActor对象的引用。这意味着performPlay()会产生某种特殊的行为。既然引用在运行时可以与另一个对象重新绑定,actor可以被替代成SadActor的引用,performPlay()产生的行为也随之改变。这样一来我们在运行时,获得了状态的灵活性。(状态模式)。与此相反,我们不能在运行时间决定继承不同的对象,因为它要求在编译期完全确定下来。
    private Actor actor = new HappyActor();
    void change(){
        actor = new SadActor();
    }
    void performPlay(){
        actor.act();
    }
}
public class Transmogrify {
    public static void main(String[] args) {
        Stage stage = new Stage();
        stage.performPlay();
        stage.change();
        stage.performPlay();
    }
}

纯继承与扩展

is-a:纯粹的继承方式,也可以认为是春替代。

父类已经为子类定义好了所有的接口,子类只有父类中定义好的方法。也就是说,父类可以接收任何发送给子类的消息,因为两者有着完全相同的接口。我们只需从子类向上转型,永远不需要知道正在处理的对象的确切类型。

is-like-a:子类拥有父类相同的基本接口,但它还具有由额外的方法实现的其他特性。

这是一种有用且明智的方法,但它也有缺点。子类中接口的扩展不能被父类访问,因此,向上转型时就不能调用那些方法。


向下转型与运行时类型的识别

由于向上转型会丢失具体的类型信息,所以我们会想,通过向下转型,也就是继承层次中向下移动就能获取具体类的星系。然而我们知道向上转型是安全的,因为父类接口不会大于子类接口,但反过来就不一定了。

我们必须确保向下转型的安全性,在C++中,我们必须执行一个特殊的操作来获得安全的向下转型。但在Java中,所有转型都会得到检查。所以即使我们只进行一次普通的加括弧形式的类型转换,在进入运行期时仍会进行检查,以便保证它的确是我们希望的类型。如果不是就会返回ClassCastException(类型转换异常) 称作运行时类型识别 RTTI

class Useful{
    void f(){}
    void g(){}
}
class MoreUseful extends Useful{
    @Override
    void f() {
    }

    @Override
    void g() {
    }
    void u(){}
    void v(){}
}
public class RTTI {
    public static void main(String[] args) {
        Useful[] x = {new Useful(),new MoreUseful()};
        x[0].f();
        x[1].g();
        ((MoreUseful)x[0]).u();//exception
        ((MoreUseful)x[1]).u();
    }
}


总结

  • 多态意味”不同的形式”。在面向对象的程序设计中,我们持有从基类继承而来的相同接口,以及使用该接口的不同形式:不同版本的动态绑定方法。
  • 多态是一种不能单独来看的特性,相反它只能作为类关系中的一部分,与其他特性协同工作。
  • 在程序中有效运用多态乃至面向对象技术,使其不仅包括个别类的成员和消息,包括类与类之间的共同特性以及他们之间的关系
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值