<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等。这个例子之所以好用,是因为我们可以说“圆是一种形状”,这种说法也很容易被理解。下面的继承图展示了它们之间的关系:
Shape s = new Circle();
这里,创建了一个Circle对象,并把得到的引用立即赋值给Shape,这样做看似错误(将一种类型赋值给另一类型);但实际上是没问题的,因为通过继承, Circle就是一种Shape。因此,编译器认可这条语句,也就不会产生错误信息。
假设我们调用某个基类方法(已被导出类所重载):
s.draw();
同样地,我们可能会认为调用的是shape的draw(),因为这毕竟是一个shape引用,那么编译器是怎样知道去做其他的事情呢?由于后期绑定(多态),程序还是正确调用了Circle.draw( )方法。
下面的例子稍微有所不同:
- //: Shapes.java
- import java.util.*; //导入需要用到的生成随机数的类包
- class Shape { // Shape是基类
- void draw() {}
- void erase() {}
- }
- class Circle extends Shape { //每个Shape的到处类都重写了方法
- void draw() {
- System.out.println("Circle.draw()");
- }
- void erase() {
- System.out.println("Circle.erase()");
- }
- }
- class Square extends Shape {
- void draw() {
- System.out.println("Square.draw()");
- }
- void erase() {
- System.out.println("Square.erase()");
- }
- }
- class Triangle extends Shape {
- void draw() {
- System.out.println("Triangle.draw()");
- }
- void erase() {
- System.out.println("Triangle.erase()");
- }
- }
- class RandomShapeGenerator {
- private Random rand = new Random();
- public Shape next() {
- switch(rand.nextInt(3)) { //用于返回3种导出类中任意一种的函数
- default:
- case 0: return new Circle();
- case 1: return new Square();
- case 2: return new Triangle();
- }
- }
- }
- // 下面开始定义公共类
- public class Shapes {
- private static RandomShapeGenerator gen = new RandomShapeGenerator();
- public static void main(String[] args) {
- Shape[] s = new Shape[9]; // 定义一个用于储存Shape类型引用的数组
- for(int i=0; i<s.length; i++) //将这个引用数组填充满
- s[i] = gen.next(); // 调用gen的next方法来返回一个Shape的对象
- for(int i=0; i<s.length; i++) //依次调用数组中各Shape对象的draw()方法
- s[i].draw(); //这里将会调用导出类的draw方法
- }
- }
Shape基类为自它那里继承而来的所有导出类,建立了一个通用接口——也就是说,所有形状都可以描绘和擦除。导出类重载了这些定义,以便为每种特殊类型的几何形状提供独特的行为。
RandomShapeGenerator是一种“工厂(factory)”,在我们每次调用next()方法时,它可以为随机选择的shape对象产生一个引用。 请注意向上转型是在return语句里发生的。每个return语句取得一个指向某个Circle、Square或者Triangle的句柄,并将其以Shape类型从next()方法中发送出去。 所以无论我们在什么时候调用next()方法时,是绝对没有可能知道它所获的具体类型到底是什么,因为我们总是只能获得一个通用的Shape引用。
main()包含了Shape句柄的一个数组,通过调用RandomShapeGenerator.next( )来填入数据。此时,我们只知道自己拥有一些Shape,不会知道除此之外的更具体情况(编译器一样不知)。然而,当我们遍历这个数组,并为每个数组元素调用draw()方法时,与各类型有关的专属行为竟会神奇般地正确发生,我们可以从运行该程序时,产生的输出结果中发现这一点。
- 扩展性
现在,让我们返回到乐器(Instrument)示例。由于有多态机制,我们可根据自己的需求向系统中添加任意多的新类型,而不需更修改true()方法。在一个设计良好的OOP程序中,我们的大多数或者所有方法都会遵循tune()的模型,而且只与基类接口通信。我们说这样的程序是“可扩展的”,因为我们可以从通用的基类继承出新的数据类型,从而新添一些功能。那些操纵基类接口的如方法不需要任何改动就可以应用于新类。
考虑一下:对于乐器例子,如果我们向基类中添加更多的方法,并加入一些新类,将会出现什么情况呢?如下图所示:
事实上,不需要改动tune()方法,所有的新类都能与原有类一起正确运行。即使tune()方法是存放在某个单独文件中,并且在Instrument接口中还添加了其他的新方法,tune()也不需再编译就仍能正确运行。下面是上述示意图的具体实现:
- //: Music3.java
- class Instrument {
- void play(Note n) {
- System.out.println("Instrument.play()" + n);
- }
- String what() { return "Instrument"; }
- void adjust() {}
- }
- class Wind extends Instrument {
- void play(Note n) {
- System.out.println("Wind.play()" + n);
- }
- String what() {return "Wind";}
- void adjust() {}
- }
- class Percussion extends Instrument {
- void play(Note n) {
- System.out.println("Percussion.play()" + n);
- }
- String what() { return "Percussion";}
- void adjust() {}
- }
- class Stringed extends Instrument {
- void play(Note n) {
- System.out.println("Stringed.play()" + n);
- }
- String what() { return "Stringed";}
- void adjust() {}
- }
- class Brass extends Wind {
- void play(Note n) {
- System.out.println("Brass.play()" + n);
- }
- void adjust() {
- System.out.println("Brass.adjust()");
- }
- }
- class Woodwind extends Wind {
- void play(Note n) {
- System.out.println("Woodwind.play()" + n);
- }
- String what() { return "Woodwind"; }
- }
- //开始公共类
- public class Music3 {
- public static void tune(Instrument i) {
- // ...
- i.play(Note.MIDDLE_C);
- }
- public static void tuneAll(Instrument[] e) {
- for(int i = 0; i < e.length; i++)
- tune(e[i]);
- }
- public static void main(String[] args) {
- // Upcasting during addition to the array:
- Instrument[] orchestra = {
- new Wind(),
- new Percussion(),
- new Stringed(),
- new Brass(),
- new Woodwind()
- };
- tuneAll(orchestra);
- }
- } ///:~
- class Note {
- private String noteName;
- private Note(String noteName) {
- this.noteName = noteName;
- }
- public String toString() { return noteName; }
- public static final Note
- MIDDLE_C = new Note("Middle C"),
- C_SHARP = new Note("C Sharp"),
- B_FLAT = new Note("B Flat");
- // Etc.
- } ///:~
新添加的方法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>
我们试图这样做也是无可厚非的:
我们所期望的输出是“public f( )”,但是
由于private方法被自动认为就是final方法,而且对导出类是屏蔽的。因此,在这种情况下,Derived类中的f( ) 方法就是一个全新的方法;既然基类中f( ) 方法在子类Derived中不可见,因此也就没有被重载。
结论就是:只有非private方法才可以被重载;但是我们还需要密切注意重载private方法的现象,虽然编译不会出错,但是不会按照我们所期望的来执行。明白地说,在导出类中,对于基类中的private方法,我们最好用一个不同的名字。
请继续支持下一篇文章: 关于抽象类和抽象方法
在main()中, 当我们将某种引用置入orchestra数组中,就会自动向上转型到Instrument。
可以看到,tune()方法完全可以忽略它周围代码所发生的全部变化,依旧正常运行。这正是期望多态所具有的特性。我们所作的代码修改,不会对程序中其他不应受到影响的部分产生破坏。从另一方面说就是, 多态是一项让程序员“将改变的事物与未变的事物分离开来”的重要技术。
- 缺陷:“重载”私有方法
<script type="text/javascript"> </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
我们试图这样做也是无可厚非的:
- //: PrivateOverride.java
- public class PrivateOverride {
- private void f() {
- System.out.println("private f()");
- }
- public static void main(String[] args) {
- PrivateOverride po = new Derived();
- po.f();
- }
- }
- class Derived extends PrivateOverride {
- public void f() {
- System.out.println("public f()");
- }
- } ///:~
结论就是:只有非private方法才可以被重载;但是我们还需要密切注意重载private方法的现象,虽然编译不会出错,但是不会按照我们所期望的来执行。明白地说,在导出类中,对于基类中的private方法,我们最好用一个不同的名字。
请继续支持下一篇文章: 关于抽象类和抽象方法