6.6 多态
在第1章的1.3.9节(多态、动态绑定)已经解释了多态的实质,它是指当系统A访问系统B的服务时,系统B可以通过多种实现方式来提供服务,而这一切对系统A是透明的。比如动物园的饲养员能够给各种各样的动物喂食。图6-3显示了饲养员Feeder、食物Food和动物Animal及它的子类的类框图。
图6-3 饲养员Feeder、食物Food和动物Animal及它的子类的类框图
可以把Feeder、Animal和Food都看成独立的子系统。Feeder类的定义如下:
public class Feeder{
public void feed(Animal animal,Food food){
animal.eat(food);
}
}
以下程序演示了一个饲养员分别给一只狗喂肉骨头,给一只猫喂鱼。
Feeder feeder=new Feeder();
Animal animal=new Dog();
Food food=new Bone();
feeder.feed(animal,food); //给狗喂肉骨头
animal=new Cat();
food=new Fish();
feeder.feed(animal,food); //给猫喂鱼
以上animal变量被定义为Animal类型,但实际上有可能引用Dog或Cat的实例。在Feeder类的feed()方法中调用animal.eat()方法,Java虚拟机会执行animal变量所引用的实例的eat()方法。可见animal变量有多种状态,一会儿变成猫,一会儿变成狗,这是多态的字面含义。
Java语言允许某个类型的引用变量引用子类的实例,而且可以对这个引用变量进行类型转换。
Animal animal=new Dog();
Dog dog=(Dog)animal; //向下转型,把Animal类型转换为Dog类型
Creature creature=animal; //向上转型,把Animal类型转换为Creature类型
图6-4 类型转换
如图6-4所示,如果把引用变量转换为子类类型,则称为向下转型,如果把引用变量转换为父类类型,则称为向上转型。在进行引用变量的类型转换时,会受到各种限制。而且在通过引用变量访问它所引用的实例的静态属性、静态方法、实例属性、实例方法,以及从父类中继承的方法和属性时,Java虚拟机会采用不同的绑定机制。
下面通过具体的例子来演示多态的各种特性。在例程6-4中,父类Base和子类Sub中都定义了实例变量var、实例方法method()、静态变量staticVar和静态方法staticMethod(),此外,在Sub类中还定义了实例变量subVar和subMethod()。
例程6-4 Sub.java
package poly;
class Base{
String var="BaseVar"; //实例变量
static String staticVar="StaticBaseVar"; //静态变量
void method(){ //实例方法
System.out.println("Base method");
}
static void staticMethod(){ //静态方法
System.out.println("Static Base method");
}
}
public class Sub extends Base{
String var="SubVar"; //实例变量
static String staticVar="StaticSubVar"; //静态变量
void method(){ //覆盖父类的method()方法
System.out.println("Sub method");
}
static void staticMethod(){ //隐藏父类的staticMethod()方法
System.out.println("Static Sub method");
}
String subVar="Var only belonging to Sub";
void subMethod(){
System.out.println("Method only belonging to Sub");
}
public static void main(String args[]){
Base who=new Sub(); //who被声明为Base类型,引用Sub实例
System.out.println("who.var="+who.var); //打印Base类的var变量
System.out.println("who.staticVar="+who.staticVar); //打印Base类的staticVar变量
who.method(); //打印Sub实例的method()方法
who.staticMethod(); //打印Base类的staticMethod()方法
}
}
(1)对于一个引用类型的变量,Java编译器按照它声明的类型来处理。例如在以下代码中,编译器认为who是Base类型的引用变量,不存在subVar成员变量和subMethod()方法,所以编译出错。
Base who=new Sub(); //who是Base类型
who.subVar="123"; //编译出错,提示在Base类中没有subVar属性
who.subMethod(); //编译出错,提示在Base类中没有subMethod()方法
如果要访问Sub类的成员,必须通过强制类型的转换。
Base who=new Sub(); //who是Base类型
((Sub)who).subVar="123"; //编译成功,把Base引用类型强制转换为Sub引用类型
((Sub)who).subMethod(); //编译成功,把Base引用类型强制转换为Sub引用类型
Java编译器允许在具有直接或间接继承关系的类之间进行类型转换,对于向上转型,不必使用强制类型转换,因为子类的对象肯定也可看做父类的对象。例如一个Dog对象是一个Animal对象,也是一个Creature对象,也是一个Object对象。
Dog dog=new Dog();
Creature creature=dog; //编译成功,把Dog引用类型直接转换为Creature引用类型
Object object=dog; //编译成功,把Dog引用类型直接转换为Object引用类型
对于向下转型,必须进行强制类型转换。
Creature creature=new Cat();
Animal animal=(Animal)creature; //编译成功,把Creature引用类型强制转换为Animal引用类型
Cat cat=(Cat)creature; //编译成功,把Creature引用类型强制转换为Cat引用类型
Dog dog=(Dog)creature; //编译成功,把Creature引用类型强制转换为Dog引用类型
假如两种类型之间没有继承关系,即不在继承树的同一个继承分支上,那么Java编译器不允许进行类型转换。例如:
Dog dog=new Dog();
Cat cat=(Cat)dog; //编译出错,不允许把Dog引用类型转换为Cat引用类型
(2)对于一个引用类型的变量,运行时Java虚拟机按照它实际引用的对象来处理。例如以下代码虽然编译可以通过,但运行时会抛出ClassCastException运行时异常。
Base who=new Base(); //who引用Base类的实例
Sub s=(Sub)who; //运行时抛出ClassCastException
在运行时,子类的对象可以转换为父类类型,而父类的对象实际上无法转换为子类类型。因为通俗地讲,父类拥有的成员子类肯定也有,而子类拥有的成员父类不一定有。假定Java虚拟机能够把子类对象转换为父类类型,那么以下代码中的sub.subMethod()方法无法执行。
Base who=new Base(); //who引用Base类的实例
Sub sub=(Sub)who; //假定运行时未出错
sub.subMethod(); //sub引用变量实际上引用Base实例,而Base实例没有subMethod()方法
sub引用变量实际上引用的是Base类的实例,而Base实例没有subMethod()方法。由此可见,在运行时,Java虚拟机无法把子类对象转换为父类类型。以下代码尽管能够编译成功,但在运行时,creature变量引用的Cat对象无法转换为Dog类型,因此会抛出ClassCastException。
Creature creature=new Cat();
Animal animal=(Animal)creature; //运行正常,Cat对象可转换为Animal类型
Cat cat=(Cat)creature; //运行正常,Cat对象可以被Cat类型的引用变量引用
Dog dog=(Dog)creature; //运行时抛出ClassCastException,Cat对象不可转换为Dog类型
(3)在运行时环境中,通过引用类型变量来访问所引用对象的方法和属性时,Java虚拟机采用以下绑定规则。
实例方法与引用变量实际引用的对象的方法绑定,这种绑定属于动态绑定,因为是在运行时由Java虚拟机动态决定的。
静态方法与引用变量所声明的类型的方法绑定,这种绑定属于静态绑定,因为实际上是在编译阶段就已经做了绑定。
成员变量(包括静态变量和实例变量)与引用变量所声明的类型的成员变量绑定,这种绑定属于静态绑定,因为实际上是在编译阶段就已经做了绑定。
例如,对于以下这段代码:
Base who=new Sub(); //who被声明为Base类型,引用Sub实例
System.out.println("who.var="+who.var); //打印Base类的var变量
System.out.println("who.staticVar="+who.staticVar); //打印Base类的staticVar变量
who.method(); //打印Sub实例的method()方法
who.staticMethod(); //打印Base类的staticMethod()方法
运行时将会输出如下结果:
who.var=BaseVar
who.staticVar=StaticBaseVar
Sub method
Static Base method
再看一个例子:
public abstract class A{
abstract void method();
void test(){
method(); //到底调用哪个类的mehtod()方法?
}
}
public class B extends A{
void method(){ //覆盖父类的method()方法
System.out.println("Sub");
}
public static void main(String args[]){
new B().test();
}
}
运行类B的main()方法将打印“Sub”。方法test()在父类A中定义,它调用了方法method()。虽然方法method()在类A中被定义成抽象的,它仍然可以被调用,因为在运行时环境中,Java虚拟机会执行类B的实例的method()方法。一个实例所属的类肯定实现了父类中所有的抽象方法(否则这个类不能被实例化)。
再看一个例子:
public class A{
void method(){System.out.println("Base");}
void test(){method();}
}
public class B extends A{
void method(){System.out.println("Sub");}
public static void main(String args[]){
new A().test(); //调用类A的method()方法
new B().test(); //调用类B的method()方法
}
}
运行这段代码将打印:
Base
Sub
test()方法在父类A中定义,它调用了method()方法,和上面一个例子的区别是父类A的method()方法不是抽象的。但是通过new B().test()调用method()方法,执行的仍然是子类B的method()方法。由此可以更深入地体会动态绑定的思想:在运行时环境中,当通过B类的实例去调用一系列的实例方法(包括一个方法调用的另一个方法),将优先和B类本身包含的实例方法动态绑定,如果B类没有定义这个实例方法,才会与从父类A中继承来的实例方法动态绑定。