多态(2):转机

    运行这个程序后,我们便会发现Music.java的难点所在。Wind.play()方法将产生输出结果。这无疑是我们所期望的输出结果,但它看起来似乎又没有什么意义。请观察一个tune()方法:

public static void tune(Instrument i) {
		i.play(Note.MIDDLE_C);
	}

    它接受一个Instrument引用。那么在这种情况下,编译器怎样才能知道这个Instrument引用指向的是Wind对象,而不是Brass对象或Stringed对象呢?实际上,编译器无法得知。为了深入理解这个问题,有必要研究一下绑定这个话题。

一、方法调用绑定

    将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),叫做前期绑定。你可能以前从来没有听过这个术语,因为它是面向过程的语言中不需要选择就默认的绑定方式。例如,C只有一种方法调用,那就是前期绑定。

    上述程序之所以令人迷惑,主要是因为前期绑定。因为,当编译器只有一个Instrument引用时,它无法知道究竟调用哪个方法才对。

    解决的办法就是后期绑定,它的含义就是在运行时根据对象的类型进行绑定。后期绑定也叫做动态绑定或运行时绑定。如果一种语言想实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。后期绑定机制随编程语言的不同而有所不同,但是只要想一下就会得知,不管怎样都必须在对象中安置某种“类型信息”。

    java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。这意味着通常情况下,我们不必判定是否应该进行后期绑定--它会自动发生。

    为什么要将某个方法声明为final呢?它可以防止其他人覆盖该方法。但更重要的一点或许是:这样做可以有效地“关闭”动态绑定,或者说,告诉编译器不需要对其进行动态绑定。这样,编译器就可以为final方法调用生成更有效的代码。然而,大多数情况下,这样做对程序的整体性能不会有什么改观。所以,最好根据设计来决定是否使用final,而不是出于试图提高性能的目的来使用final。

二、产生正确的行为

    一旦知道java中所有方法都是通过动态绑定实现多态这个事实之后,我们就可以编写只与基类打交道的程序代码了,并且这些代码对所有的导出类都可以正确运行。或者换一种说法,发送消息给某个对象,让该对象去断定应该做什么事。

    面向对象程序设计中,有一个经典的例子就是“几何形状”(shape)。因为它很直观,所以经常用到;但不幸的是,他可能使初学者认为面向对象程序设计仅适用于图形化程序设计,实际当然不是这样。

    在“几何形状”这个例子中,有一个基类Shape,以及多个导出类--如Circle、Square、Triangle等。这个例子之所以好用,是因为我们可以说“圆是一种几何形状”,这种说法也很容易被理解。下面继承图展示它们之间的关系:

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

Shape s = new Circle();

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

    假设你调用一个基类方法(它已在导出类中被覆盖):s.draw();你可能再次认为调用的是Shape的draw(),因为这毕竟是一个Shape引用,那么编译器是怎样知道去做其他的事情呢?由于后期绑定(多态),还是正确调用了Circle的draw()方法。

    下面的例子稍微有所不同:

public class Shape {
	public void draw() {
	}

	public void erase() {
	}
}
public class Circle extends Shape {
	public void draw() {
		System.out.println("Circle的draw()");
	}

	public void erase() {
		System.out.println("Circle的erase()");
	}
}
public class Square extends Shape {
	public void draw() {
		System.out.println("Square的draw()");
	}

	public void erase() {
		System.out.println("Square的erase()");
	}
}
public class Triangle extends Shape {
	public void draw() {
		System.out.println("Triangle的draw()");
	}

	public void erase() {
		System.out.println("Triangle的erase()");
	}
}
import java.util.Random;

public class RandomShapeGenerator {
	private Random r = new Random(47);

	public Shape next() {
		switch (r.nextInt(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];
		for (int i = 0; i < s.length; i++) {
			s[i] = gen.next();
		}
		for (Shape shape : s) {
			shape.draw();
		}
	}
}

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

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

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

    随机选择几何形状是为了让大家理解:在编译时,编译器不需要获得任何特殊信息就能进行正确的调用。对draw()方法的所有调用都是通过动态绑定进行的。

三、可扩展性

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

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

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

/**
 * 乐器
 */
class Instrument {
	void play(Note n) {
		System.out.println("Instrument的play()" + n);
	}

	String what() {
		return "Instrument";
	}

	// 调整
	void adjust() {
		System.out.println("Adjusting Instrument");
	}
}

class Wind extends Instrument {
	void play(Note n) {
		System.out.println("Wind的play()" + n);
	}

	String what() {
		return "Wind";
	}

	void adjust() {
		System.out.println("Adjusting Wind");
	}
}

/**
 * 敲打乐器
 */
class Percussion extends Instrument {
	void play(Note n) {
		System.out.println("Percussion的play()" + n);
	}

	String what() {
		return "Percussion";
	}

	void adjust() {
		System.out.println("Adjusting Percussion");
	}
}

/**
 * 弦乐器
 */
class Stringed extends Instrument {
	void play(Note n) {
		System.out.println("Stringed的play()" + n);
	}

	String what() {
		return "Stringed";
	}

	void adjust() {
		System.out.println("Adjusting Stringed");
	}
}

/**
 * 管乐
 */
class Brass extends Wind {
	void play(Note n) {
		System.out.println("Brass的play()" + n);
	}

	void adjust() {
		System.out.println("Adjusting Brass");
	}
}

/**
 * 木管乐器
 */
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 (Instrument instrument : e)
			tune(instrument);
	}

	public static void main(String[] args) {
		Instrument[] orchestra = { new Wind(), new Percussion(), new Stringed(), new Brass(), new Woodwind() };
		tuneAll(orchestra);
	}
}

    新添加的方法what()返回一个带有类描述的String引用;另一个新添加的方法adjust()则是提供每种乐器的调音方法。

    在main()中,当我们将某种引用置入orchestra数组中,就会自动向上转型到Instrument。

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

四、缺陷:“覆盖”私有方法

    我们试图像下面这样做也是无可厚非的:

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

    我们所期望的输出是public f(),但是由于private方法被自动认为是final方法,而且对导出的类是屏蔽的。因此,在这种情况下,Derived类中的f()方法就是一个全新的方法;既然基类中的f()方法在子类Derived中不可见,因此甚至也不能被重载。

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

五、缺陷:域与静态方法

    一旦你了解了多态机制,可能就会开始认为所有事物都可以多态地发生。然而,只有普通的方法调用可以是多态的。例如,如果你直接访问某个域,这个访问就将在编译期进行解析,就像下面的示例所演示的:

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();
		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());
	}
}

    当Sub对象转型为Super引用时,任何域访问操作都将由编译器解析,因此不是多态的。在本例中,为Super.field和Sub.field分配了不同的存储空间。这样,Sub实际上包含两个称为field的域:它自己和它从Super处得到的。然而,在引用Sub中的field时所产生的默认域并非Super版本的field域。因此,为了得到Super.field,必须显式地指明super.field。

    尽管这看起来好像会成为一个容易令人混淆的问题,但是在实践中,它实际上从来不会发生。首先,你通常会将所有的域都设置成private,因此不能直接访问它们,其副作用是只能调用方法来访问。另外,你可能不会对基类中的域和导出类中的域赋予相同的名字,因为这种做法容易令人混淆。

    如果某个方法是静态的,它的行为就不具有多态性:

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();
		System.out.println(sup.staticGet());
		System.out.println(sup.dynamicGet());
	}
}

    静态方法是与类,而并非与单个对象相关联的。

 如果本文对您有很大的帮助,还请点赞关注一下。

发布了100 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_40298351/article/details/104142789