[Java]关于"多态","绑定"等概念的理解的知识点总结(下) - Thinking in Java经典片段之二



<script type="text/javascript"> </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
  • 产生正确的行为

一旦知道Java中所有方法都是通过动态绑定实现多态这个事实之后,我们就可以编写只与基类打交道的程序代码了,并且这些代码对所有的导出类都可以正确运行。或者换种说法,发送消息给某个对象,让该对象去断定应该做什么事。
面向对象程序设计中,有一个最经典的“几何形状(shape)”例子。因为它很容易被可视化,所以经常用到;但不幸的是,它可能使初学者认为面向对象程序设计仅适用于图形化程序设计,实际当然不是这种情形了。
在“几何形状”这个例子中,包含一个Shape基类和多个导出类,如:Circle, Square, Triangle等。这个例子之所以好用,是因为我们可以说“圆是一种形状”,这种说法也很容易被理解。下面的继承图展示了它们之间的关系:


Java 多态示意

向上转型可以像下面这条语句这么简单:

Shape s = new Circle();

这里,创建了一个Circle对象,并把得到的引用立即赋值给Shape,这样做看似错误(将一种类型赋值给另一类型);但实际上是没问题的,因为通过继承, Circle就是一种Shape。因此,编译器认可这条语句,也就不会产生错误信息。
假设我们调用某个基类方法(已被导出类所重载):

s.draw();

同样地,我们可能会认为调用的是shape的draw(),因为这毕竟是一个shape引用,那么编译器是怎样知道去做其他的事情呢?由于后期绑定(多态),程序还是正确调用了Circle.draw( )方法。
下面的例子稍微有所不同:

  1. //: Shapes.java
  2. import java.util.*;  //导入需要用到的生成随机数的类包
  3. class Shape {    // Shape是基类
  4.     void draw() {}
  5.     void erase() {}
  6. }
  7. class Circle extends Shape {  //每个Shape的到处类都重写了方法
  8.     void draw() {
  9.         System.out.println("Circle.draw()");    
  10.     }
  11.     
  12.     void erase() {
  13.         System.out.println("Circle.erase()");
  14.     }
  15.     
  16. }
  17. class Square extends Shape {
  18.     void draw() {
  19.         System.out.println("Square.draw()");
  20.     }
  21.     
  22.     void erase() {
  23.         System.out.println("Square.erase()");
  24.     }
  25. }
  26. class Triangle extends Shape {
  27.     void draw() {
  28.         System.out.println("Triangle.draw()");
  29.     }
  30.     
  31.     void erase() {
  32.         System.out.println("Triangle.erase()");
  33.     }
  34. }
  35. class RandomShapeGenerator {
  36.     private Random rand = new Random();
  37.     public Shape next() {
  38.         switch(rand.nextInt(3)) { //用于返回3种导出类中任意一种的函数
  39.             default:
  40.             case 0return new Circle();
  41.             case 1return new Square();
  42.             case 2return new Triangle();
  43.         }
  44.     }
  45. }

  46. // 下面开始定义公共类
  47. public class Shapes {
  48.     
  49.     private static RandomShapeGenerator gen = new RandomShapeGenerator();
  50.     
  51.     public static void main(String[] args) {
  52.         Shape[] s = new Shape[9];  // 定义一个用于储存Shape类型引用的数组
  53.         for(int i=0; i<s.length; i++)  //将这个引用数组填充满
  54.             s[i] = gen.next(); // 调用gen的next方法来返回一个Shape的对象
  55.             
  56.         for(int i=0; i<s.length; i++) //依次调用数组中各Shape对象的draw()方法
  57.             s[i].draw();  //这里将会调用导出类的draw方法
  58.     }
  59. }

    Shape基类为自它那里继承而来的所有导出类,建立了一个通用接口——也就是说,所有形状都可以描绘和擦除。导出类重载了这些定义,以便为每种特殊类型的几何形状提供独特的行为。

    RandomShapeGenerator是一种“工厂(factory)”,在我们每次调用next()方法时,它可以为随机选择的shape对象产生一个引用。
请注意向上转型是在return语句里发生的。每个return语句取得一个指向某个Circle、Square或者Triangle的句柄,并将其以Shape类型从next()方法中发送出去。 所以无论我们在什么时候调用next()方法时,是绝对没有可能知道它所获的具体类型到底是什么,因为我们总是只能获得一个通用的Shape引用。

    main()包含了Shape句柄的一个数组,通过调用RandomShapeGenerator.next( )来填入数据。此时,我们只知道自己拥有一些Shape,不会知道除此之外的更具体情况(编译器一样不知)。然而,当我们遍历这个数组,并为每个数组元素调用draw()方法时,与各类型有关的专属行为竟会神奇般地正确发生,我们可以从运行该程序时,产生的输出结果中发现这一点。



  • 扩展性

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

考虑一下:对于乐器例子,如果我们向基类中添加更多的方法,并加入一些新类,将会出现什么情况呢?如下图所示:


Java 多态示意图

事实上,不需要改动tune()方法,所有的新类都能与原有类一起正确运行。即使tune()方法是存放在某个单独文件中,并且在Instrument接口中还添加了其他的新方法,tune()也不需再编译就仍能正确运行。下面是上述示意图的具体实现:

  1. //: Music3.java
  2. class Instrument {
  3.     void play(Note n) {
  4.         System.out.println("Instrument.play()" + n);
  5.     }   
  6.     String what() { return "Instrument"; }  
  7.     void adjust() {}
  8. }
  9. class Wind extends Instrument {
  10.     void play(Note n) {
  11.         System.out.println("Wind.play()" + n);
  12.     }   
  13.     String what() {return "Wind";}  
  14.     void adjust() {}
  15. }
  16. class Percussion extends Instrument {
  17.     void play(Note n) {
  18.         System.out.println("Percussion.play()" + n);
  19.     }   
  20.     String what() { return "Percussion";}   
  21.     void adjust() {}
  22. }
  23. class Stringed extends Instrument {
  24.     void play(Note n) {
  25.         System.out.println("Stringed.play()" + n);
  26.     }   
  27.     String what() { return "Stringed";} 
  28.     void adjust() {}
  29. }
  30. class Brass extends Wind {
  31.     void play(Note n) {
  32.         System.out.println("Brass.play()" + n);
  33.     }
  34.     void adjust() {
  35.         System.out.println("Brass.adjust()");
  36.     }
  37. }
  38. class Woodwind extends Wind {
  39.     void play(Note n) {
  40.         System.out.println("Woodwind.play()" + n);
  41.     }
  42.     String what() { return "Woodwind"; }
  43. }
  44. //开始公共类
  45. public class Music3 {
  46.   public static void tune(Instrument i) {
  47.     // ...
  48.     i.play(Note.MIDDLE_C);
  49.   }
  50.   public static void tuneAll(Instrument[] e) {
  51.     for(int i = 0; i < e.length; i++)
  52.       tune(e[i]);
  53.   }
  54.   public static void main(String[] args) {
  55.     // Upcasting during addition to the array:
  56.     Instrument[] orchestra = {
  57.       new Wind(),
  58.       new Percussion(),
  59.       new Stringed(),
  60.       new Brass(),
  61.       new Woodwind()
  62.     };
  63.     tuneAll(orchestra);
  64.   }
  65. ///:~
  66. class Note {
  67.   private String noteName;
  68.   private Note(String noteName) {
  69.     this.noteName = noteName;
  70.   }
  71.   public String toString() { return noteName; }
  72.   public static final Note
  73.     MIDDLE_C = new Note("Middle C"),
  74.     C_SHARP  = new Note("C Sharp"),
  75.     B_FLAT   = new Note("B Flat");
  76.     // Etc.
  77. ///:~

新添加的方法what()返回一个String引用及类的描述说明;另一个新添加的方法adjust()则提供每种乐器的调音方法。
在main()中,
当我们将某种引用置入orchestra数组中,就会自动向上转型到Instrument。

可以看到,tune()方法完全可以忽略它周围代码所发生的全部变化,依旧正常运行。这正是期望多态所具有的特性。我们所作的代码修改,不会对程序中其他不应受到影响的部分产生破坏。从另一方面说就是, 多态是一项让程序员“将改变的事物与未变的事物分离开来”的重要技术。


  • 缺陷:“重载”私有方法


<script type="text/javascript"> </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
我们试图这样做也是无可厚非的:


  1. //: PrivateOverride.java
  2. public class PrivateOverride {
  3.     private void f() {
  4.         System.out.println("private f()");
  5.     }
  6.     public static void main(String[] args) {
  7.     PrivateOverride po = new Derived();
  8.     po.f();
  9.     }
  10. }
  11. class Derived extends PrivateOverride {
  12.     public void f() {
  13.         System.out.println("public f()");
  14.     }
  15. ///:~
我们所期望的输出是“public f( )”,但是 由于private方法被自动认为就是final方法,而且对导出类是屏蔽的。因此,在这种情况下,Derived类中的f( ) 方法就是一个全新的方法;既然基类中f( ) 方法在子类Derived中不可见,因此也就没有被重载。
结论就是:只有非private方法才可以被重载;但是我们还需要密切注意重载private方法的现象,虽然编译不会出错,但是不会按照我们所期望的来执行。明白地说,在导出类中,对于基类中的private方法,我们最好用一个不同的名字。


请继续支持下一篇文章: 关于抽象类和抽象方法

<script type="text/javascript"> </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值