Java多态详解
一 . 多态概述
在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序--即无论在项目最初创建时还是在需要添加新功能时都可以“生长”的程序。多态的作用时消除类型之间的耦合关系。它允许将多种类型(从同一基类导出的)视为同一类型来处理,而同一份代码也就可以毫无差别地运行在这些不同类型之上了。多态方法调用允许一种类型表现出与其它相似类型之间的区别,只要它们都是从同一类导出来的。这种区别是根据方法行为的不同而表示出来的,虽然这些方法都不可以通过同一个基类来调用。
Java引用变量有两个类型:一个是编译时类型,它由声明该变量时使用的类型决定。另一个时运行时类型,它由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可以认为是多态出现了。
二 . 引用变量的向上转型
看下面的程序:
class Shape{ public int length=6; public void draw() { System.out.println("父类的普通方法"); } public void erase() { System.out.println("父类中被覆盖的方法"); } } public class Circle extends Shape{ //重新定义一个length实例变量隐藏父类的length实例变量 public int length=8; public void test() { System.out.println("子类的普通方法"); } public void erase() { System.out.println("子类覆盖父类的方法"); } public static void main(String [] arge) { //编译时类型和运行时类型相同,不存在多态 Shape shape=new Shape(); //输出6 System.out.println(shape.length); //输出"父类的普通方法" shape.draw(); //输出"父类中被覆盖的方法" shape.erase(); //编译时类型和运行时类型相同,不存在多态 Circle circle=new Circle(); //输出8 System.out.println(circle.length); //输出"子类的普通方法" circle.test(); //输出"子类覆盖父类的方法" circle.erase(); //编译时类型和运行时类型不相同,多态发生 Shape shapeCircle=new Circle(); //输出6,访问的是父类对象的实例变量 System.out.println(shapeCircle.length); //输出"子类的普通方法",访问的是从父类继承到的 erase() 方法 shapeCircle.erase(); //因为shapeCircle的编译时是Shape,Shape类没有提供test()方法,所以下面代码编译时会报错 //下面是对 shapeCircle 进行强制类型转换为 Circle 类型,才能调用运行时类型的 erase() 方法 ((Circle)shapeCircle).erase(); } }
上面程序的类图如下:
因为子类是一种特殊的父类,因此Java允许把一个子类对象直接赋给一个父类引用变量,无须任何类型转换,或者被称为向上转型,向上转型由系统自动完成。当把一个子类对象直接赋给父类引用变量时,例如上面第一个程序的 Shape shapeCircle=new Circle( ) ; ,这个 shapeCircle 引用变量的编译时类型是 Shape ,而运行时类型是 Circle ,当运行时调用该引用变量时,其方法行为总是表现出子类方法的行为特征,这就是多态。
与方法不同的是,对象的实例变量不具备多态,即通过引用变量来访问其包含的实例变量时,系统总是试图访问它编译时类型所定义的成员变量,而不是运行时类型所定义的成员变量,如上面的 System.out.println(shapeCircle.length) , 输出的时子类的实例变量。接下来再看一个多态的例子。
class Person{ public void work() { System.out.println("Person.work()"); } } class Teacher extends Person{ public void work() { System.out.println("Teacher.work()"); } } public class DemoTest extends Person{ public static void base(Person p) { p.work(); } public static void main(String [] arge) { Teacher teacher=new Teacher(); base(teacher); } }
这也是经常见到的一种向上转型的例子:DemoTest.base( ) 方法接受一个 Person 引用,同时也接受任何导出自 Person 的类。在 main( ) 方法中,当一个 Teacher 引用传递到 base( ) 方法时,就会出现向上转换,而不需要任何类型转换。这样做是自动完成的,因为 Teach 从 Person 继承而来,所以 Person 的接口必定存在于Teacher 中。从 Teacher 向上转型到 Person 可能会“缩小”接口,但不会比 Person 的全部接口更窄。
三 . 引用变量的强制类型转换(向下转型)
运用好强制类型转换,确实能省事不少,但是这种强制类型转换不是盲目的,还是有一些地方需要注意:
- 基本类型之间的转换只能在数值类型之间进行,如:整数型、字符型和浮点型。但数值类型和布尔类型之间不能进行类型转换。
- 引用类型之间的转换只能在具有继承关系的两个类型之间进行,如果是两个没有任何继承关系的类型,则无法进行类型转换,否则编译时就会出现错误。如果试图把一个父类实例转换成子类类型,则这个对象必须实际上是子类实例才行(即编译时类型为父类类型,而运行时类型时子类类型),否则将在运行时引发 ClassCastException 异常。
四 . instanceof 运算符
在进行强制类型转换之前,先用 instanceof 运算符判断是否可以成功转型,从而避免出现 ClassCastException 异常。instance 运算符的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是接口),它用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是,则返回 true ,否则返回 false 。
在使用 instanceof 运算符时需要注意:instanceof 运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引发编译错误。