多态(4):协变返回类型、用继承进行设计

一、协变返回类型

    java SE5中添加了协变返回类型,它表示在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型:

/**
 * 粮食
 */
class Grain {
	public String toString() {
		return "Grain";
	}
}

/**
 * 小麦
 */
class Wheat extends Grain {
	public String toString() {
		return "Wheat";
	}
}

/**
 * 磨坊
 */
class Mill {
	Grain process() {
		return new Grain();
	}
}

/**
 * 小麦磨坊
 */
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);
	}
}

    java SE5与java早期版本之间的主要差异就是较早的版本将强制process()的覆盖版本必须返回Grain,而不能返回Wheat,尽管Wheat是从Grain导出的,因而也应该是一种合法的返回类型。协变返回类型允许返回更具体的Wheat类型。

二、用继承进行设计

    学习了多态之后,看起来似乎所有东西都可以被继承,因为多态是一种如此巧妙的工具。事实上,当我们使用现成的类来建立新类时,如果首先考虑使用继承技术,反倒会加重我们设计负担,使事情变得不必要地复杂起来。

/**
 * 男演员
 */
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();
	}
}

    在这里,Stage对象包含一个对Actor的引用,而Actor被初始化为HappyActor对象。这意味着performPlay()会产生某种特殊行为。既然引用在运行时可以与另一个不同的对象重新绑定起来,所以SadActor对象的引用可以在actor中被替代,然后由performPlay()产生的行为也随之改变。这样一来,我们在运行期间获得了动态灵活性。与此相反,我们不能在运行期间决定继承不同的对象,因为它要求在编译期间完全确定下来。

    一条通用的准则是:“用继承表达行为间的差异,并用字段表达状态上的变化”。在上述例子中,两者都用到了:通过继承得到了两个不同的类,用于表达act()方法的差异;而Stage通过运用组合使自己的状态发生变化。在这种情况下,这种状态的改变也就产生了行为的改变。

三、纯继承与扩展

    采用“纯粹”的方式来创建继承层次结构似乎是最好的方式。也就是说,只有在基类中已经建立的方法才可以在导出类中被覆盖,如下图所示:

    这被称作是纯粹的“is-a”(是一种)关系,因为一个类的接口已经确定了它应该是什么。继承可以确保所有的导出类具有基类的接口,且绝对不会少。按上图那么做,导出类也将具有和基类一样的接口。

    也可以认为这是一种纯替代,因为导出类可以完全代替基类,而在使用它们时,完全不需要知道关于子类的任何额外信息:

    也即是说,基类可以接收发送给导出类的任何消息,因为二者有着完全相同的接口。我们只需从导出类向上转型,永远不需知道正在处理的对象的确切类型。所有这一切,都是通过多态来处理的。

    按这种方式考虑,似乎只有纯粹的is-a关系才是唯一明智的做法,而所有其他的设计都只会导致混乱和注定会失败。这其实也是一个陷阱,因为只要开始考虑,就会转向,并发现扩展接口(遗憾的是,extends关键字似乎在怂恿我们这样做)才是解决特定问题的完美方案。这可以称为“is-like-a”(像一个)关系,因为导出类就像是一个基类--它有着相同的基本接口,但是它还具有由额外方法实现的其他特性。

    虽然这是一种有用且明智的方法(依赖于具体情况),但是它也有缺点。导出类中接口的扩展部分不能被基类访问,因此,一旦我们向上转型,就不能调用那些新方法:

    在这种情况下,如果我们不进行向上转型,这样的问题也就不会出现。但是通常情况下,我们需要重新查明对象的确切类型,以便能够访问该类型所扩充的方法。下一节将说明如何做到这一点。

四、向下转型与运行时类型识别

    由于向上转型(在继承层次中向上移动)会丢失具体的类型信息,所以我们就想,通过向下转型--也就是在继承层次中向下移动--应该能够获取类型信息。然而,我们知道向上转型是安全的,因为基类不会具有大于导出类的接口。因此,我们通过基类接口发送的消息保证都能被接受。但是对于向下转型,例如,我们无法知道一个“几何图形”它确实就是一个“圆”,它可以是一个三角形、正方形或其他一些类型。

    要解决这个问题,必须有某种方法来确保向下转型的正确性,使我们不至于贸然转型到一种错误类型,进而发出该对象无法接受的消息。这样做是及其不安全的。

    在某些程序设计语言(如C++)中,我们必须执行一个特殊的操作来获得安全的向下转型。但是在java语言中,所有转型都会得到检查。所以即使我们只是进行一次普通的加括弧形式的类型转换,在进入运行期时仍然会对其进行检查,以便保证它的确是我们希望的那种类型。如果不是,就会返回一个ClassCastException(类转换异常)。这种在运行期间对类型进行检查的行为称作“运行时类型识别”(RTTI)。下面的例子说明RTTI的行为:

class Useful {
	public void f() {
	}

	public void g() {
	}
}

class MoreUseful extends Useful {
	public void f() {
	}

	public void g() {
	}

	public void u() {
	}

	public void v() {
	}

	public void w() {
	}
}

public class RTTI {
	public static void main(String[] args) {
		Useful[] x = { new Useful(), new MoreUseful() };
		x[0].f();
		x[1].g();
		// x[1].u();
		((MoreUseful) x[1]).u();
		((MoreUseful) x[0]).u();
	}
}

    正如前一个示意图中所示,MoreUseful(更有用的)接口扩展了Useful(有用的)接口;但是由于它是继承而来的,所以它也可以向上转型到Useful类型。我们在main()方法中对数组x进行初始化时可以看到这种情况的发生。既然数组中的两个对象都属于Useful类,所以我们可以调用f()和g()这两个方法。如果我们试图调用u()方法(它只存在于MoreUseful),就会返回一条编译时出错消息。

    如果想访问MoreUseful对象的扩展接口,就可以尝试进行向下转型。如果所转类型是正确的类型,那么转型成功;否则,就会返回一个ClassCastException异常。我们不必为这个异常编写任何特殊的代码,因为它指出的是程序员在程序中任何地方都可能会犯的错误。

    RTTI的内容不仅仅包括转型处理。例如它还提供一种方法,使你可以在试图向下转型之前,查看你所要处理的类型。

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

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

猜你喜欢

转载自blog.csdn.net/qq_40298351/article/details/104178691
今日推荐