第8章多态

多态

  • 多态也称作动态绑定、后期绑定或运行时绑定
  • 再讨论向上转型:把某个对象的引用视为对其基类的引用的做法称为向上转型
  • 方法绑定:
    ①:将一个方法调用同一个方法主体关联起来被称作绑定.若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),叫做前期绑定.
    ②:后期绑定:运行时根据对象的类型进行绑定,后期绑定也叫做动态绑定或者运行时绑定.java中除了static方法和final方法(private 方法属于final方法)之外,其他所有的方法都是后期绑定,这意味着通常情况下,我们不必判定是否应该进行后期绑定–它会自动发生

  • 为什么要将某个方法声明为final?
    ①:它可以防止其他人覆盖该方法
    ②:最重要的一点这样可以有效的”关闭”动态绑定,或者说,告诉编译器不需要对其进行动态绑定.

  • 多态是一项让程序员”将改变的事物与未来事物分离开来”的重要技术.

  • 缺陷:覆盖私有方法.由于private方法被自动认为是final方法,而且对导出类是屏蔽的,所以不能覆盖.父子类相同的私有方法不是覆盖

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

public class Sup {
    public static String staticGet() {
        return "Base staticGet()";
    }

    public String dynamicGet() {
        return "Base dynamicGet()";
    }
}

public class Sub extends Sup {
    public static String staticGet() {
        return "Derived staticGet()";
    }

    public String dynamicGet() {
        return "Derived dynamicGet()";
    }

    public static void main(String[] args) {
        Sup sup = new Sub();
        System.out.println(sup.staticGet());//Base staticGet()
        System.out.println(sup.dynamicGet());//Derived dynamicGet()
    }
}
  • 构造器和多态
    ①:通常,构造器不同于其他种类的方法,涉及到多态时仍是如此.尽管构造器并不具有多态性(它们实际上是static方法,只不过该static声明是隐式的),但还是非常有必须理解构造器是怎么通过多态在复杂的层次中运作,这一理解将有助于避免一些令人不快的困扰
  • 构造器的调用顺序

基类的构造器总是在导出类的构造过程中被调用,而且是按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用.,这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确的构造.
导出类只能访问它自身的成员,不能够访问基类中的成员(基类成员通常是private类型).只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化,因此必须令所有构造器都得到调用,否则就不可能正确构造完整对象.这正是编译器为什么要强制每个导出类部分都必须调用构造器的原因,在导出类的构造器主体中,如果没有明确指定调用某个基类构造器,它会默默的调用默认构造器.如果不存在默认构造器,编译器就会报错.

public class Meal {
    public Meal() {
        System.out.println("Meal()");
    }
}

public class Bread {
    public Bread() {
        System.out.println("Bread()");
    }
}
public class Cheese {
    public Cheese() {
        System.out.println("Cheese()");
    }
}
public class Lettuce {
    public Lettuce() {
        System.out.println("Lettuce()");
    }
}
public class Lunch extends Meal{
    public Lunch() {
        System.out.println("Lunch()");
    }
}
public class PortableLunch extends Lunch {
    public 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();
    }
}
//结果
//Meal() Lunch() PortableLunch()  Bread() Cheese() Lettuce() Sandwich()

上述代码结果表明
①:调用基类构造器,这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直到最底层的导出类
②:按声明顺序调用成员的初始化方法.
③:调用导出类构造器的主体.

  • 继承与清理
    通过组合和继承来创建新类时,永远不必担心对象的清理问题,子对象通常会留给垃圾回收器进行处理
public class Characteristic {
    private String s;

    public Characteristic(String s) {
        this.s = s;
        System.out.println("Creating Characteristic "+s);
    }

    protected void  dispose(){
        System.out.println("disposing Characteristic "+s);
    }
}

public class Description {
    private String s;

    public Description(String s) {
        this.s = s;
        System.out.println("Creating Description "+s);
    }

    protected void  dispose(){
        System.out.println("disposing Description "+s);
    }
}

public class LivingCreature {
    private Characteristic p = new Characteristic("is alive");
    private Description t = new Description("Base Living Creature");

    public LivingCreature() {
        System.out.println("LivingCreature()");
    }

    protected void  dispose(){
        System.out.println(" LivingCreature dispose ");
        t.dispose();
        p.dispose();
    }
}

public class Animal extends LivingCreature{
    private Characteristic p = new Characteristic("has heart");
    private Description t = new Description("Animal not Vegetable");

    public Animal() {
        System.out.println("Animal()");
    }

    protected void  dispose(){
        System.out.println(" Animal dispose ");
        t.dispose();
        p.dispose();
        super.dispose();
    }

}

public class Amphibian extends Animal {
    private  Characteristic p = new Characteristic("can live in water");
    private Description t = new Description("Both water and land");

    public 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();
    }
}
//结果
Creating Characteristic is alive
Creating Description Base 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 Base Living Creature
disposing Characteristic is alive

①:上述代码层次结构中的每个类都包含Characteristic和Description这两种类型的成员对象,并且他们也必须被销毁,所以万一某个子对象依赖于其他对象,销毁的顺序应该和初始化的顺序相反.对于字段,则意味着与声明的顺序相反(因为字段的初始化是按照声明的顺序进行的).对于基类,应该首先对导出类进行清理,然后才是基类,这是因为导出类的清理可能会调用基类中的某些方法,所以需要使基类中的构件扔起作用而不应该过早的销毁它们.从上述代码中可以看到,Frog对象的所有部分都是按照创建的逆序进行销毁的.
②:从例子中可以看到,尽管通常不必执行清理工作,但是一旦选择要执行,就必须谨慎和小心.
③:当销毁方法被子类覆盖时,一定要调用基类的销毁方法,否则基类的清理动作就不会发生.
③:如果成员对象中存在于其他一个或多个对象共享的情况,问题就变得更加复杂了,就不能简单的销毁了,在这种情况下,也许必要使用”引用计数”来跟踪仍旧访问着共享对象的对象数量了.

public class Shared {
    private int refcount = 0;
    private static long counter = 0;
    private final long id = counter++;

    public Shared() {
        System.out.println("Creating  " + this);
    }

    public void addRef() {
        refcount++;
    }

    protected void dispose() {
        if (--refcount == 0) {
            System.out.println("Disposing  " + this);
        }
    }

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

public 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("dispose  "+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();
         }
    }
}

//结果
Creating  Shared0
Creating  Composing  0
Creating  Composing  1
Creating  Composing  2
Creating  Composing  3
Creating  Composing  4
dispose  Composing  0
dispose  Composing  1
dispose  Composing  2
dispose  Composing  3
dispose  Composing  4
Disposing  Shared0

static long counter跟踪所创建的Shared的实例数量,还可以为id提供数值.counter的类型是long而不是int,这样可以防止溢出(这是一个良好实践,这种计数器不可能发生溢出).id是final的,因为我们不希望它的值在对象生命周期中被改变.在将一个共享对象附着到类上时,必须记住调用addRef(),但是dispose()方法将跟踪引用数,并决定何时执行清理.使用这种技巧需要倍加细心,但是如果你正在共享需要清理的对象,那么你没有太多的选择余地.

  • 构造器内部的多态方法的行为
    在一般的方法内部,动态绑定的调用是运行时才决定的,因为对象无法知道它是属于方法的哪个类,还是属于那个类的导出类.如果要调用构造器内部的一个动态绑定方法,就会用到那个方法的被覆盖后的定义.然而,这个调用的效果可能相当难以预料,因为被覆盖的方法在对象被完全构造之前就会被调用,这可能会造成一些难以发现的隐藏错误.
    从概念上讲,构造器的工作实际上是创建对象,在任何构造器内部,整个对象可能只是部分形式–我们知道基类对象已经进行初始化.如果构造器只是在构建对象过程中的一个步骤,并且该对象所属的类是从这个构造器所属的类导出的,那么导出部分在当前构造器正在被调用的时刻仍旧没有被初始化.然而,一个动态绑定的方法调用却会向外深入到继承层次结构内部,它可以调用导出类的方法,如果我们在构造器内部这样做,那么就可能会调用某个方法,而这个方法所操纵的成员可能还未进行初始化—这肯定会导致灾难,下面例子,看问题所在
public class Glyph {
    void draw(){
        System.out.println("Glyph.draw()");
    }

    public Glyph() {
        System.out.println("Glyph()  before draw()");
        draw();
        System.out.println("Glyph()  after draw()");
    }
}

public 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);
    }
}
//结果
Glyph()  before draw()
RoundGlyph.draw().radius= 0
Glyph()  after draw()
RoundGlyph.RoundGlyph().radius= 5

前一节讲述的初始化顺序并不十分完整,而这时解决这一谜题的关键所在,初始化的实际过程是
①:在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制零
②:如前所述那样调用基类构造器,此时,调用覆盖后的draw()方法(要在调用RoundGlyph构造器之前调用),由于步骤1的缘故,我们此时会发现redius的值为0
③:按照声明的顺序调用成员的初始化方法.
④:调用导出类的构造器主体.
注意:编写构造器时有一条准则:用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其它方法.在构造器内唯一能够安全调用的那些方法是基类的final方法(也适用于private方法,它们自动属于final方法),因为这些方法不能被覆盖,不会出现上述问题

  • 协变返回类型:在导出类中的被覆盖的方法可以返回基类方法的返回类型的某种导出类型(适用于父子类)
public class Grain {
    public String toString() {
        return "Grain";
    }
}
public class Wheat extends Grain{
    public String toString() {
        return "Wheat";
    }
}
public class Mill {
    Grain process(){
        return new Grain();
    }
}
public 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);
    }
}
//结果 Grain   Wheat
  • 用继承进行设计:
    学习了多态之后,看起来似乎所有东西都可以被继承,因为多态是一种如此巧妙的工具。事实上,当我们用现成的类来建立新类时,如果首先考虑使用继承技术,反倒会加重我们的设计负担,使事情变得不必要地复杂起来。
    更好的方法是首先选择“组合”,尤其是不能十分确定应该使用哪一种方式时。组合不会强制我们的程序设计进入继承的层次结构中。而且,组合更加灵活,因为它可以动态选择类型(因此也就选择了行为);相反,继承在编译时就需要知道确切类型。下面举例说明这一点:
package package4;
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();  
    }    
}  
//结果    HappyActor   SadActor  

在这里,Stage对象包含一个对Actor的引用,而Actor被初始化为HappyActor对象。这意味着performPlay()会产生某种特殊行为。既然引用在运行时可以与另一个不同的对象重新绑定起来,所以SadActor对象的引用可以在actor中被替代,然后由performPlay()产生的行为也随之改变。这样一来,我们在运行期间获得了动态灵活性(这也称作状态模式)。与此相反,我们不能在运行期间决定继承不同的对象,因为它要求在编译期间完全确定下来。
一条通用的准则是:“用继承表达行为间的差异,并用字段表达状态上的变化”。在上述例子中,两者都用到了:通过继承得到了两个不同的类,用于表达act()方法的差异:而Stage通过运用组合使自己的状态发生了变化。在这种情况下,这种状态的改变也就产生了行为的改变。

  • 纯继承与 扩展
    ①:继承is-a(是一个)是父子类的方法一样.
    ②:扩展is-like-a(像一个) 子类比父类拥有更多的方法. 缺点:导出类中接口的扩展不能被基类访问.一旦向上转型,就不能调用扩展方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值