在面向对象的程序设计中,多态是继数据抽象和继承之后的第三种基本特性;多态通过分离做什么(基类对象)和怎么做(导出类对象),从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序—即无论在项目最初创建时还是在需要添加新功能时都可以“生长”的程序。
“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化”把接口和实现分离开来。继承允许将对象视为它自己本身的类型或其基类型(向上转型)来加以处理,多态的作用则是消除类型之间的耦合关系。 多态调用允许一种类型表现出与其他相似类型之间的区别,只要它们都是从同一基类导出来的。这种区别是根据方法行为的不同而表示出来的,虽然这些方法都可以通过同一个基类来调用。
多态
1.1 向上转型
对象即可以作为它自己本身的类型使用,也可以作为它的基类型使用。而这种把对某个对象的引用视为对其基类型的引用的做法
被称为向上转型。但是向上转型时,每个类在调用方法时都会使用父类的对象,而忘记了自己的对象类型。
在上述问题中,我们是否要将会进行向上转型的方法中的参数直接使用成其本身类型的参数呢?这样或许会更加直观。但是那样又会出现一些新的问题:比如说我们要在Instrument(乐器)类型中的每一个子类都编写一个tune()方法,这时当我们以后要在Instrument中添加新的类型时,我们将需要非常多的编程工作,这显然是十分麻烦的。
如果只写这样一个简单方法,它仅接收基类作为参数,而不是那些特殊的导出类。这样情况会变得更好吗?也就是说,如果不管导出类的存在,编写的代码只是与基类打交道,会不会更好?这正是多态所允许的。
多态即是允许一个方法只接收基类作为参数,然后基类的导出类可以通过向上转型方便的将自己的对象当成参数传入该方法中。
1.2 方法调用绑定
说到这里,其实上我们在数据进行向上转型后,对于在方法中如何判断其原本是哪一个对象类型这一问题还没有解决。为了解决这一问题我们将学习方法调用绑定:
将一个方法调用同一个方法主题关联起来被称为绑定。若在程序执行前进行绑定(由编译器和连接程序实现),叫做前期绑定。一个非面向对象编程的编译器产生的函数调用就是使用前期绑定(编译器将产生对一个具体函数名字的调用)。Java中无法使用前期绑定,因为当编译器只有一个Instrument引用时,它无法知道究竟调用哪个方法才对。
而解决这个问题的方法就是后期绑定,它的含义就是在运行时根据对象的类型进行绑定
。后期绑定也叫做动态绑定或运行时绑定。编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。后期绑定根据不同的编程语言有所不同,但是,不管怎样都必须在对象中安置某种 “类型信息”。
Java中所有的方法都是通过动态绑定来实现多态
,我们就可以编写只与基类打交道的程序代码了,并且这些代码对所有的导出类都可以正确运行。换句话说,发送消息给某个对象,让该对象取断定应该做什么事
。
class Shape{
public void draw(){};
public void erase(){};
}
class Circle extends Shape{
public void draw(){
System.out.println("Circle.draw()");
};
public void erase(){
System.out.println("Circle.erase()");
};
}
class Square extends Shape{
public void draw(){
System.out.println("Square.draw()");
};
public void erase(){
System.out.println("Square.erase()");
};
}
class Triangle extends Shape{
public void draw(){
System.out.println("Triangle.draw()");
};
public void erase(){
System.out.println("Triangle.erase()");
};
}
public class Shapes {
public static void main(String[] args) {
Shape shape1=new Circle();
Shape shape2=new Square();
Shape shape3=new Triangle();
shape1.draw();
shape2.draw();
shape3.draw();
}
}
Output:
Circle.draw()
Square.draw()
Triangle.draw()
在“几何形状”例子中,有一个基类Shape,以及多个导出类–如Circle、Square、Triangle等。下面的继承图展示它们之间的关系:
这里,创建了一个Circle对象,并把得到的引用立即赋值给Shape,这样做看似错误(将一种类型赋值给另外一种类型);但实际上是没问题的,因为通过继承,Circle就是一种Shape。因此,编译器认可这条语句。
1.3 缺陷
在“覆盖”私有方时,只有非private方法才可以被覆盖;但是还需要密切注意覆盖private方法的现象。这时虽然编译器不会报错,但是也不会按照我们所期望的来执行。确切地说,在导出类中,对于基类中的private方法,最好采用不同的名字。
域与静态方法不具有多态性;域是在编译期进行解析的;静态方法与类相关联,而非与单个的对象相关联。当使用向上转型调用域属性,会调用父类属性;当使用向上转型调用static方法时,会调用父类方法;调用非static方法时,是调用子类方法的,也就是多态性。
构造器和多态
构造器不同于其他种类的方法,涉及到多态时仍是如此;尽管构造器并不具有多态性(它们实际是static方法,只不过该static声明是隐式的);但是,构造器可通过多态在复杂的层次结构中运作;
2.1 构造器的调用顺序
基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确地构造
;导出类只能给你访问它自己的成员,不能访问基类中的成员。只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化;因此,必须令所有构造器都得到调用,否则就不可能正确构造完整对象
。
class Meal{
Meal(){
System.out.print("Meal()->");
}
}
class Bread{
Bread(){
System.out.print("Bread()->");
}
}
class Cheese{
Cheese(){
System.out.print("Cheese()->");
}
}
class Lettuce{
Lettuce(){
System.out.print("Lettuce()->");
}
}
class Lunch extends Meal{
Lunch(){
System.out.print("Lunch()->");
}
}
class PortableLunch extends Lunch{
PortableLunch(){
System.out.print("PortableLunch()->");
}
}
public class Sandwich extends PortableLunch{
private Bread b=new Bread();
private Cheese c=new Cheese();
private Lettuce l=new Lettuce();
public Sandwich(){
System.out.print("Sandwich()->");
}
public static void main(String[] args) {
new Sandwich();
}
}
Output:
Meal()->Lunch()->PortableLunch()->Bread()->Cheese()->Lettuce()->Sandwich()->
这个例子展示组合、继承以及多态在构建顺序上的作用:
在这里,用其他类创建了一个复杂的类,而且每个类都有一个声明它自己的构造器。其中最重要的Sandwich,它反映了三层继承以及三个成员对象。当在 main()里创建了一个Sandwich对象后,就可以看到输出结果。这里可以反映复杂对象调用构造器的顺序:
1.调用基类构造器
;这个步骤会不断地反复递归下去,从构造这种层次结果的根,直至最底层的导出类。2.按声明顺序调用成员的初始化方法
;(每次进入一个父类的时候,在调用构造器之前,会先初始化成员方法或域)3.调用导出类构造器的主体
构造器的调用顺序是很重要的。当进行继承时,我们已经知道基类的一切,并且可以访问基类中任何声明为public和protected的成员。这意味着在导出类中,必须假定基类的所有成员都是有效的。
2.2 构造器内部的多态方法的行为
构造器调用的层次结构带来了一个有趣的两难问题。如果在构造器的内部调用正在构造的对象的某个动态绑定方法,那会发生什么情况呢?
在一般的方法内部,动态绑定的调用是在运行时才决定的,因为对象无法知道它是属于方法所在的那个类,还是属于那个类的导出类。如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义。然而,这个调用的效果可能相当难于预料,因为被覆盖的方法在对象被完全构造之前就会被调用
。
class Glyph{
void draw(){
System.out.println("Glyph.draw()");
}
Glyph() {
System.out.println("Glyph() before");
draw();
System.out.println("Glyph() after");
}
}
class RoundGlyph extends Glyph{
private int radius =1;
RoundGlyph(int r){
radius =r;
System.out.println("RoundGlyph.RoundGlyph().radius = "+radius);
}
void draw(){
System.out.println("RoundGlyph().draw().radius = " +radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}
Output:
Glyph() before
RoundGlyph().draw().radius = 0
Glyph() after
RoundGlyph.RoundGlyph().radius = 5
Glyph.draw() 方法设计为将要覆盖,这种覆盖是在RoundGlyph中发生的。但是Glyph构造器会调用这个方法,结果导致了对 RoundGlyph.draw() 的调用,这看似使我们的目的。但是从结果看,当Glyph的构造器调用 draw() 方法时,radius不是默认初始值1,而是0(这个时候radius在导出类中还没有被初始化)。
现在可以完善初始化顺序了:
1.在其他事物发生之前,将分配给对象的存储空间初始化成二进制的零
2.如前面所述那样调用基类构造器。此时,调用被覆盖后的draw()方法(要在调用RoundGlyph构造器之前调用),由于步骤一的缘故,此时radius是0
3.按照声明的顺序调用成员的初始化方法
4.调用导出类的构造器主体
因此,编写构造器时有一条有效的准则:“尽可能用简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法”。在构造器内唯一能够安全调用的那些方法是基类中的final方法(也适用与private,它们自动属于final方法)。这些方法不会被覆盖,因此也就不会出现上面的问题。
用继承进行设计
学习了多态之后,似乎所有的东西都可以被继承,但是每当我们使用现成的类来建立新类时,如果首先考虑使用继承技术,反倒会加重我们的设计负担,使事情变得不必要地复杂起来。更好的方式是首先选择“组合”,尤其是不能十分确定应该使用哪一种方式时。组合不会强制我们的程序设计进入继承的层次结构中。而且,组合更加灵活,因为它可以动态选择类型;相反,继承在编译时就需要知道确切类型。在进行程序设计时有一条通用的准则是:“用继承表达行为间的差异,并用字段表达状态上的变化”。
3.1 纯继承与扩展
采用“纯粹”的方式来创建继承层次结构似乎是最好的方式。也就是说,只有在基类中已经建立的方法才可以在导出类中被覆盖,如下图所示:
这被称作纯粹的 “is-a”(是一种)关系,因为一个类的接口已经确定了应该是什么。继承可以确保所有的导出类具有基类的接口,且绝对不会少。按上图那么做,导出类也将具有和基类一样的接口
。
也可以认为这是一种纯替代,因为导出类可以完全代替基类,而在使用它们时,完全不需要知道关于子类的任何额外信息:
也就是说,基类可以接收发送给导出类的任何消息,因为二者有着完全相同的接口。我们只需从导出类向上转型,永远不需知道正在处理的对象的确切类型,所有这一切都是通过多态来处理的。
3.2 向下转型与运行时类别识别
由于向上转型会丢失具体的类型信息,所以,想着通过向下转型(继承层次上向下移动)应该能够获取类型信息。
向上转型是安全的,因为基类不会具有大于导出类的接口,通过基类接口发送的消息保证都能被接受;但是,对于向下转型,比如:我们无法知道一个“几何形状”就是一个“圆”,可以是三角形、正方形或其他一些类型。要解决这个问题,必须有某种方法来确保向下转型的正确性;
在Java中,所有转型都会得到检查,所以即使我们只是进行一次普通的加括号形式的类型转换,在进入运行期时仍会对其进行检查,以确保它的确是我们希望的那种类型。如果不是,便会返回ClassCastException(类转型异常)。这种在运行期间对类型进行检查的行为称作“运行时类型识别”(RRTI)。
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[] uf={new Useful(),new MoreUseful()};
uf[0].f();
uf[1].g();
((MoreUseful)uf[0]).u(); //Exception thrown java.lang.ClassCastException:
((MoreUseful)uf[1]).u();
}
}