第八章 多态(下)
本章多态目前就到此介绍完毕,可能还会有些疑问,不过后面还会有很多设计到多态的地方,通过不断学习,最后一定会将这些知识掌握的。同时这一章所讲解的知识也还是比较详细的,多态基本上都会在我们的代码中体现出来,只是之前不知道,不明白而已。
8.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 grain = m.process();
System.out.println(grain);
m=new WheatMill();
grain=m.process();
System.out.println(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 grain = m.process();
System.out.println(grain);
m=new WheatMill();
grain=m.process();
System.out.println(grain);
}
}
运行结果:
Grain
Wheat
Java SE5与Java较早版本之间的主要差异就是较早的版本将强制process()的覆盖版本必须返回Grain,而不能返回Whear,尽管Wheat是从Grain导出的,因而也应该是一种合法的返回类型。协变返回类型允许返回更具体的Wheat类型。
8.5 用继承进行设计
学习了多态之后,看起来似乎所有的东西都可以被继承,因为多态是一种如此巧妙的工具。事实上,当我们使用现成的类来建立新类时,如果首先考虑使用继承技术,反倒会加重我们的设计负担,使事情变得不必要地复杂起来。
更好的方式是首先选择“组合”,尤其是不能十分确定应该使用哪一种方式时。组合不会强制我们的程序设计进入继承的层次结构中。而且,组合更加灵活,因为它可以动态选择类型(因此也就选择了行为);相反,继承在编译时就需要知道确切的类型。下面举例说明这一点:
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();
}
}
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();
}
}
运行结果:
HappyActor
SadActor
一条通用的准则是:“用继承表达行为间的差异,并用字段表达状态上的变化”。在上述例子中,两者都用到了:通过继承得到了两个不同的类,用于表达act()方法的差异;而Stage通过运用组合使自己的状态发生变化。在这种情况下,这种状态的改变也就产生了行为的改变。
8.5.1 纯继承与扩展
采取“纯粹”的方式来创建继承层次结构似乎是最好的方式。也就是说,只有在基类中已经建立的方法才可以在导出类中被覆盖,如下图所示:
这被称作是纯粹的“is-a”(是一种)关系,因为一个类的接口已经确定了它应该是什么。继承可以确保所有的导出类具有基类的接口,且绝对不会少。按上图那么做,导出类也将具有和基类一样的接口。
也可以认为这是一种纯替代,因为导出类可以完全代替基类,而在使用它们时,完全不需要知道关于子类的任何额外信息。也就是说,基类可以接受发送给导出类的任何消息,因为二者有着完全相同的接口。我们只需从导出类向上转型,永远不需知道正在处理的对象的确切类型。所有这一切,都是通过多态来处理。
8.5.2 向下转型与运动时类型识别
由于向上转型(在继承层次中向上移动)会丢失具体的类型信息,所以我们就想,通过向下转型——也就是在继承层次中向下移动——应该能够获取类型信息。然而,我们知道向上转型是安全的,因为基类不会具有大于导出类的接口。因此,我们通过基类接口发送消息保证都能被接受。但对于向下转型,例如,我们无法知道一个“几何形状”它确实就是一个"圆",它可以是一个三角形、正方形或其他一些类型。
要解决这个问题,必须有某种方法来确保向下转型的正确性,使我们不致于贸然转型到一种错误类型,进而发出该对象无法接受的消息。这样做是及其不安全的。
在某些程序设计语言(如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 class RTTI {
public static void main(String[] args) {
Useful[] x={new Useful(),new MoreUseful()};
x[0].f();
public void g(){}
}
class MoreUseful extends Useful{
public void f(){ }
public void g(){}
public void u(){}
}
public class RTTI {
public static void main(String[] args) {
Useful[] x={new Useful(),new MoreUseful()};
x[0].f();
x[1].g();
// x[1].u(); The method u() is undefined for the type Useful
// (MoreUseful)x[1].u(); Downcast/RTTI
// (MoreUseful)x[0].u(); Exception thrown
}
}
正如前一个示意图中所示,MoreUseful(更有用的)接口扩展了Useful(有用的)接口;但是由于它是继承而来的,所以它也可以向上转型到Useful类型。我们在main()方法中对数组X进行初始化时可以看到这种情况的发生。既然数组中的两个对象属于Useful类,所以我们可以调用f()和g()这两个方法。如果我们试图调用u()方法(它只存在于MoreUseful),就会返回一条编译时出错消息。
如果想访问MoreUseful对象的扩展接口,就可以尝试进行向下转型。如果所转类型是正确的类型,那么转型成功;否则,就会返回一个ClassCastException异常。我们不必为这个异常编写任何特殊的代码,因为它指出的是程序员在程序中任何地方都可能会犯的错误。{Throws Exception}注释标签告知本书的构建系统:在运行该程序时,预期抛出一个异常。