zoukankan      html  css  js  c++  java
  • Java多态

    上溯造型

      类可以被当做他的基类来使用,这种将对象当做基类进行引用并且进行使用被称之为Upcasting,因为他是沿着对象继成树向上走的。我们先看一下下面的例子:

    复制代码
    //: polymorphism/music/Note.java
    // Notes to play on musical instruments.
    package polymorphism.music;
    public enum Note {
      MIDDLE_C, C_SHARP, B_FLAT; // Etc.
    } ///:~
    
    //: polymorphism/music/Instrument.java
    package polymorphism.music;
    import static net.mindview.util.Print.*;
    class Instrument {
      public void play(Note n) {
        print("Instrument.play()");
      }
    }
    ///:~
    //: polymorphism/music/Wind.java
    package polymorphism.music;
    // Wind objects are instruments
    // because they have the same interface:
    public class Wind extends Instrument {
    // Redefine interface method:
      public void play(Note n) {
        System.out.println("Wind.play() " + n);
      }
    } ///:~
    //: polymorphism/music/Music.java
    // Inheritance & upcasting.
    package polymorphism.music;
    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() MIDDLE_C
    *///:~
    复制代码

      在Music的tune中应该接收的是Instrument引用,但是其实传递任何继承自Instrument的对象都可以,而且不需要进行类型转换。Wind中包含了Instrument的借口,虽然从Wind到Instrument的转换会缩小Wind的借口,但是对Instrument的使用并不会有影响。

      那么我们为什么不将tune方法的参数设置为Wind类型的呢?在tune方法仅仅是需要Instrument的接口的情况下,如果将参数的类型设置为具体的Wind类型,那么当我们新添加一个乐器的时候,就需要多书写一个以这个具体乐器类型为参数的tune方法。这无疑增加了代码的耦合性和复杂性。而这也正是多态解决的问题。

    捻度控制

      在Music中的tune方法中调用的是Instrument的play方法,而多态需要控制的一点就是根据传入的子类型的来调用对应派生类型的play方法,编译器是如何做到这一点的呢?编译器是做不到这一点的,为了理解如何实现这一点,我们需要了解一下对象绑定的概念

      在调用方法的时候涉及到一个改变,叫做绑定。有两种形式的方法绑定,预绑定和即使绑定,预绑定就是在编译的时候进行方法的绑定,如C语言中只有预绑定,但是即时绑定是在运行的时候,根据对象的类型,来动态的决定应该绑定的方法,不同的语言有不同的动态绑定方法的机制,但是有一点是肯定的,那就是对象中肯定存在一些信息用来帮助在运行的时候确定对象的类型。

      在Java中,出了static和final修饰的方法之外,其余的都是使用即时绑定的方法来调用方法。因此你不用手动使用即时绑定,它会自动为你实现。正式因为Java的这种即时绑定的机制,允许了我们直接与基类进行交互,而编译器会根据传入的派生类的具体类型来执行正确的方法。

      这样的一种设计方法使得程序具有很好的扩展性,因为随着从基类派生出更多的类实现更多功能的时候,与基类进行交互的代码几乎是不需要进行任何修改的。

      但是下面的代码需要注意:

    复制代码
    //: 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()
    *///:~
    复制代码

      这里的基类中的f方法并没有在派生类中重写,因为他是私有的,对派生类是不可见的,所以派生类中的f是一个全新的方法。这里在这种情况下需要将这两个方法设置为不同的名称。

      需要记住的一点是,多态只能针对一般的方法进行使用,如果使用多态去读取字段的话,那么读到的将会是基类的字段,如下面的代码:

    复制代码
    //: 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()
    *///:~
    复制代码

    构造器和多态

      通常来说,构造器也是不具有多态特性的,他们实际上是静态方法,只不过没有明确使用static关键字而已。下面我们来看一下构造器在多太中起到的作用。

      在构造派生类的时候总是需要在派生类的构造器中调用基类的构造器,这样才能够保证类的正确构建。因为派生类无法读取基类中的私有字段,只有在基类的构造函数中才能对基类中的私有字段进行正确的初始化。如果在派生类的构造器中没有显示调用基类中的构造器,那么将会调用默认的构造器,如果基类没有默认的构造器,那么编译器将会报错。下面的代码中可以看到复杂的层级关系中的实例化顺序:

    复制代码
    //: 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()
    *///:~
    复制代码

      由上面的代码中可以看到派生类的构造顺序是首先为要生成的类进行内存分配,然后先调用基类的构造函数,然后是调用派生类中成员的构造函数,这个顺序是按照成员声明的顺序,最后是调用派生类的构造函数。知道这个顺序是非常重要的,这样在派生类的构造器中你就可以基类中的成员,因为这个时候基类已经初始化完毕了,而且在派生类的构造函数中开始执行的时候,成员也已经初始化结束了。

       与初始化相反的是对象的清理工作,虽然可以留给垃圾收集器进行清理,但是有些涉及到非内存的清理工作仍然需要手动进行,可以在基类中添加一个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
    Polymorphism 207
    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
    *///:~
    复制代码

      但是需要注意到另外一个问题,那就是如果某个实例的引用被多个对象引用的时候,那么就不能直接对其清理,要确定没有其他类型的实例对他还有引用之后才能进行清理,下面的例子中添加了一个专门用来计数对象实例化个数,用id进行表示,以及被引用个数的成员refcounter,在确定没有对象对其实例有引用之后才对其进行清理:

    复制代码
    //: polymorphism/ReferenceCounting.java
    // Cleaning up shared member objects.
    import static net.mindview.util.Print.*;
    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();
      }
      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();
      }
    } /* Output:
    Creating Shared 0
    Creating Composing 0
    Creating Composing 1
    Creating Composing 2
    Creating Composing 3
    Creating Composing 4
    disposing Composing 0
    disposing Composing 1
    disposing Composing 2
    disposing Composing 3
    disposing Composing 4
    Disposing Shared 0
    *///:~
    复制代码

      但是这里还有一个问题,就是如果在派生类的构造函数中调用派生类的多态函数呢?来看下面的代码先:

    复制代码
    //: 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
    *///:~
    复制代码

       这里我们可以看到在派生类的构造函数中调用多态函数的时候确实是会执行派生类中的函数,但是由于派生类还没有完全构造完毕,因此非常容易产生不是我们需要的效果。

    协变返回类型

      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之前的版本中,虽然Wheat是Grain的派生类型,但是在重写process的时候会强制返回Grain类型,而且这样的返回时合法的。协变返回类型允许在重写函数中返回更加具体的类型

    下塑造性

        多态中的上溯造型能够保证类型的安装转换,因为任何可以对基类调用的方法都可以对其派生类进行使用,但是如果在上溯之后我们需要还原到起始的类型的时候,就需要进行强制类型转换,而在强制类型转换的过程中有可能出现错误,因为一个基类可以派生出多个基类,而上塑之后我们就不知道当初他的具体类型了,因此我们需要进行判断,如下面的代码:

    复制代码
    //: polymorphism/RTTI.java
    // Downcasting & Runtime type information (RTTI).
    // {ThrowsException}
    class Useful {
      public void f() {}
      public void g() {}
    }
    class MoreUseful extends Useful {
      public void f() {}
      public void g() {}
      public void u() {}
      public void v() {}
      public void w() {}
    }
    public class RTTI {
      public static void main(String[] args) {
        Useful[] x = {new Useful(), new MoreUseful()};
        x[0].f();
        x[1].g();
        // Compile time: method not found in Useful:
        //! x[1].u();
        ((MoreUseful)x[1]).u(); // Downcast/RTTI
        ((MoreUseful)x[0]).u(); // Exception thrown
      }
    } ///:~
    复制代码

      在运行的时候确定对象的具体类型称之为运行时类型识别runtime type identification,RTTI,关于RTTI我们还有比强制类型转换更多的方法,我们可以在下塑之前就获得对象的具体类型的信息,这点留到以后再讲

     
    分类: Java
  • 相关阅读:
    tomcat部署web应用的4种方法以及部署多个应用
    LeetCode算法题目解答汇总(转自四火的唠叨)
    16、手把手教你Extjs5(十六)Grid金额字段单位MVVM方式的选择
    15、手把手教你Extjs5(十五)各种Grid列的自定义渲染
    14、手把手教你Extjs5(十四)模块字段和Grid列的定义[2]
    13、手把手教你Extjs5(十三)模块字段和Grid列的定义[1]
    12、手把手教你Extjs5(十二)执行菜单命令在tabPanel中显示模块
    11、手把手教你Extjs5(十一)模块界面的总体设计
    10、手把手教你Extjs5(十)自定义模块的设计
    9、手把手教你Extjs5(九)使用MVVM特性控制菜单样式
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/2742187.html
Copyright © 2011-2022 走看看