在面向对象的程序设计(OOP)语言中,多态(又称动态绑定、后期绑定或运行时绑定)与继承是两个基本的特征。继承允许将对象当作它自己的类型或其基类来进行处理,而多态则可以消除不同类型之间的耦合关系,多态方法调用允许一种类型表现出与其他从同一基类导出类型之间的区别。
一、动态绑定的理解
在java中,将一个方法调用与一个方法主体关联起来叫做绑定,当程序运行之前就进行绑定的行为叫前期绑定;然而,在程序运行之前无法确定方法调用的具体类型,需要程序运行之后才能确定其类型的情况被称作后期绑定,也即是多态。需要注意的是:java中除了static和final(private隐式地属于final)方法外,其余的方法都是后期绑定的。当所有方法都是通过动态绑定时,编写代码时就只需与基类进行通信,因为这些代码对基类的导出类都是可以正确运行的。对多态粗糙的理解就是:在基类和导出类中定义了带相同参数和返回值而函数体不同的函数,尽管调用方法在程序运行前不知道具体的类对象类型,但是运行时程序能识别出具体的类型,从而决定是调用基类或某一导出类中对应的调用方法。比如说:
// Shapes.java
import java.util.*;
class Shape
{
public void Draw(){/…;*/}
public void Earse(){/*…*/}
}
class Circle extends Shape
{
public void Draw(){System.out.println("Circle.Draw() !");}
public void Earse(){System.out.println("Circle.Earse() !");}
}
class Square extends Shape
{
public void Draw(){System.out.println("Square.Draw() !");}
public void Earse(){System.out.println("Square.Earse() !");}
}
class Triangle extends Shape
{
public void Draw(){System.out.println("Triangle.Draw() !");}
public void Earse(){System.out.println("Triangle.Earse() !");}
}
/*public*/ class RandomGen
{
private Random rand=new Random(47);
public Shape next()
{
switch(rand.nextInt(3))
{
default:
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
}
public class Shapes
{
private static RandomGen gen=new RandomGen();
public static void main(String[] args)
{
Shape[] a=new Shape[9];//向上转型为Shape类型
for(int i=0;i<a.length;i++)
a[i]=gen.next();
for(Shape s:a)
s.Draw();//多态
}
}
[分析]:上述程序段定义了一个基类Shape以及由此导出的几个子类Circle、Square、Triangle等,通过类RandomGen的Shape next()函数得到随机数控制返回的子类类对象,由此验证多态的后期绑定机制,因为程序运行之前编译器是不知道返回的具体类型的。
Main()函数中定义的数组Shape[ ] a保存着根据随机数控制返回的、自动向上转型的类对象。由于基类及导出类中都定义有相同的Draw()函数(只是函数体不同),因此,s.Draw()会根据返回类对象的具体类型调用基类或导出类中的Draw()函数。
二、private方法的屏蔽效应
当基类中的某个方法retntype f(argstype)被设置成private(被自动认为是一个final方法)时,会对导出类产生屏蔽效应,且导出类中retntype f(argstype)就会是一个全新的方法,基类中的方法retntype f(argstype)在导出类中不可见。比如说:
//Base.java
class Base
{
private void f(){System.out.println("Base f() !");}
public static void main(String[] args)
{
Base b=new Derived();
b.f();//Base f() !,导出类中的f()被当成是新方法,因此此处调用基类的f()
}
}
class Derived extends Base
{
public void f(){System.out.println("Derived f() !");}
}
三、多态与域、静态方法之间的关系
当试图直接访问某个域时(当然是具有访问权限的域,如public域),此域访问操作由编译器解析,因此,它不存在多态的情况。当基类和导出类中含有相同的域时,如果存在向上转型类对象访问域的情况,则该访问域是基类中的域;同理,导出类对象访问域则是导出类本身的域,试图访问基类中的域则需要加关键字super实现。比如说:
// FieldAcess.java
class Base
{
public int value=10;//public可访问域
public int getValue(){System.out.println("Base value !");return value;}
}
class Derived extends Base
{
public int value=100; //public可访问域
public int getValue(){System.out.println("Derived value !");return value;}
public int getBaseValue()
{
System.out.println("Derived super Base value !");
return super.value;//访问基类中的域需要关键字super
}
}
public class FieldAcess
{
public static void main(String[] args)
{
Base b=new Derived();
System.out.println("b.value="+b.value);//直接访问基类的域,不存在多态
System.out.println("b.getValue()="+b.getValue());//方法则是多态的
Derived d=new Derived();
System.out.println("d.value="+d.value); //直接访问导出类域,无多态
System.out.println("d.getValue()="+d.getValue());//方法多态
System.out.println("d.getBaseValue()="+d.getBaseValue());
}
}
[分析]:根据上述原则,程序的运行结果应该是:
b.value=10
Derived value !
b.getValue()=100
d.value=100
Derived value !
d.getValue()=100
Derived super Base value !
d.getBaseValue()=10
另外一方面,如果调用方法是static的,则此调用方法仍然是不具有多态的,如下面情况:
//StaticPoly.java
class StaticBase
{
public static String staticRetn(){return "Base staticRetn()!";}
public String dynamicRetn(){return "Base dynamicRetn()";}
}
class StaticDerived extends StaticBase
{
public static String staticRetn(){return "Derived staticRetn()!";}
public String dynamicRetn(){return "Derived dynamicRetn()";}
}
public class StaticPoly
{
public static void main(String[] args)
{
StaticBase sb=new StaticDerived();
System.out.println(sb.staticRetn());//static方法没有多态,调用基类方法
// Base staticRetn()!
System.out.println(sb.dynamicRetn());//非static方法多态,调用导出类
// Derived dynamicRetn()
}
}
注:静态方法是与类而非类与单个对象相关联的。
四、构造器与多态
在导出类中,构造器应该按先基类再导出类的顺序进行调用。导出类中方法调用的执行顺序应该是:
<1>.调用基类构造器
<2>.按声明的顺序进行成员初始化
<3>.调用该导出类构造器
例如:
// ConstrPoly.java
class PolyBase
{
PolyBase(){System.out.println("Base() Constructor !");}
}
class PolyDerived extends PolyBase
{
PolyDerived(){System.out.println("PolyDerived() Constructor !");}
}
public class ConstrPoly extends PolyDerived
{ /* Base() Constructor !
PolyDerived() Constructor ! */
PolyBase pb=new PolyBase();//Base() Constructor !
PolyDerived pd=new PolyDerived();
/* Base() Constructor !
PolyDerived() Constructor ! */
ConstrPoly(){System.out.println("ConstrPoly() Constructor !");}
// ConstrPoly() Constructor !
public static void main(String[] args)
{
new ConstrPoly();
}
}
[分析]:
[1]. main()函数所在的类ConstrPoly是一个导出类,应该先调用其基类PolyDerived的构造器,而PolyDerived又是导出于PolyBase类的,因此调用基类构造器的顺序就是PolyBase()->PolyDerived()。
[2]. 按声明的顺序进行成员初始化.由[1]可知,此步骤进行的顺序是:PolyBase()->PolyBase()->PolyDerived()
[3]. 调用该导出类构造器ConstrPoly()。
五、协变返回类型
协变返回类型,即在导出类中被覆盖的方法可以返回基类方法返回类型的某一导出类型,即:
class Shape
{
public Shape Draw(){return new Shape();}
}
class Circle extends Shape
{
public Triangle Draw(){System.out.println("Circle Draw() !");return new Triangle();}
}
class Triangle extends Shape{}
public class CovRetn
{
public static void main(String[] args)
{
Circle c=new Circle();
Triangle t=new Triangle();
t=c.Draw();
}
}
Reference:Bruce Eckel <<Thinking in java>>4th Edition