JAVA编程思想学习第八篇の多态

       

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

         

1.再论向上转型

public enum Note{
          MIDDLE_C,C_SHARP,B_FLAT;
}

class Instrument{
         public void play(Note n){
                   print("Instrument.play()");
}

class Wind extends Instument{
         public void play(Note n){
                System.out.println("wind.play()"+n);
         }
}

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);
        }
}/*Output:
wind.play()MIDDLE_C
      从wind向上转型到Instrument可能会“缩小”接口,但不会比Instrument的全部接口更窄。

        

2.转机

        编译器怎么样才能知道这个Instrument引用指向的是wind对象呢?实际上,编译器无法得知。为了深入理解这个问题,有必要研究一下绑定这个话题。

2.1方法调用绑定

        将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前进行绑定,叫做前期绑定
        上述问题的解决方法就是后期绑定,它是在运行时根据对象的类型进行绑定。

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

2.2产生正确的行为

       动态绑定可以通过正确的方法产生正确的调用。


        我们创建Shape s=new Cricle();并且调用s.draw();我们有的是Shape对象,但是为什么没调用Shape的draw()方法呢,由于后期绑定,我们正确的调用了Cricle.draw()方法。

2.3可扩展性

        由于多态机制,我们可以根据自己的需求对系统添加任意多的新类型,而不需更改tune()方法。在一个设计良好的OOP程序中,大多数或者所有方法都会遵循tune()的模型,而且只与基类接口通信。这样的程序是可扩展的,因为可以从通用的基类继承出新的数据类型,从而新添一些功能。那些操纵基类接口的方法不需要任何改动就可以应用于新类。



          我们定义Instrument[] arc={new wind(),new Percussion(),new Stringed(),new Woodwind(),new Brass()};所有元素都可以正确的调用自身的play()方法(子类的子类也可以调用自身的方法)。


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

<span style="font-size:14px;">public class PrivateOverride{
          private void f(){print("print 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()
*///:~</span>

        只有非private方法才可以被覆盖,但是还需要密切注意覆盖private方法的现象,这时虽然编译器不会报错,但是也不会按照我们所期望的来执行。确切的说,在导出类中,对于基类中的private方法,最好采用不同的名字。

2.5缺陷:域与静态方法

            看到这里,认为所有事物都可以多态地发生,这是错误的。只有普通的方法调用可以是多态的。例如,如果你直接访问某个域,这个访问将在编译期进行解析。
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 access{
       public static void main(String[] args){
                 Super sup=new Sub();
                 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
*///:~

        此处,sup.field为0,说明直接访问某个域是没有多态机制的。 静态方法的行为也是不具有多态性的。静态方法是与类,而非与单个对象相关联的。


3.构造器和多态

        构造器并不具有多态性(他们实际是static方法,只不过该static声明是隐式的)。但是还是非常有必要理解构造器怎么样通过多态在复杂的结构中运作。

3.1构造器的调用顺序

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

3.2继承与清理

        通过组合和继承方法来创建新类时,永远不必担心对象的清理问题,子对象通常都会留给垃圾回收器进行处理。如果确实遇到清理问题,那么必须用心为新类创建dispose()方法。当覆盖dispose()方法时,务必记住调用基类版本dispose(),否则,基类的清理动作就不会发生。

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

       如果要调用构造器内部的一个动态方法,就要用到那个方法被覆盖后的定义。然而,这个调用的效果可能相当难于预料,因为被覆盖的方法在对象被完全构造之前就被调用。这可能会造成一些难于发现的隐藏错误。
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(){
                 radius=r;
                 print("RoundGlyph.RoundGlyph().radius="+radius);
         }
       void draw(){
               print("RoundGlyph.draw().radius="+radius);
       }
}

public class Poly{
         public static void main(String[] args){
                  new RoundGlyph(5);
         }
}/*Output:
Glyph() before draw()
RoundGlyph.draw(),radius=0
Glyph() after draw()
RoundGlyph.RoundGlyph(),radius=5
*///:~

       我们发现当Glyph调用draw()方法时,radius不是默认初始值1,而是0。这说明初始化顺序并不完整。

       因此,编写构造器时有一条有效的准则:“用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法。”在构造器中唯一能安全调用的方法是基类的final方法(也适用于private方法,因为他们自动属于final方法)。这些方法不能被覆盖,也就不存在上面的问题。

4.用继承进行设计

       导出类中接口的扩展部分不能被基类访问,因此,一旦向上转型,我们就不能调用那些新方法。

       由于向上转型会丢失具体的类型信息,所以我们就想,通过向下转型,应该能够获取类型信息。但是必须有某种方法来保证向下转型的正确性,使我们不至于贸然转型到一个错误的类型。Java中所有转型都会得到检查。以便保证他的正确性。如果不是,就会返回一个ClassCastException(类转型异常)。这种在运行期间对类型进行检查的行为称为“运行时类型识别”(RTTI)。

class useful{
        public void f(){}
        public void g(){}
}

class MoreUserful extends useful{
         public void f(){}
         public void g(){}
         public void u(){}
}

public class RTTI{
         public static void main(){
                   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
          }
}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值