Java编程思想(八、多态)
在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。
多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序。
“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化”把接口和实现分离开来。多态的作用则是消除类型之间和耦合关系。继承允许将对象视为它自己本身的类型或其基类型来处理。这种能力极为重要,因为它允许将多种类型(从同一基类导出的)视为同一类型来处理。而同一份代码也就可以毫无差别地运行在这些不同类型之上了。多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要它们都是从同一基类导出来的。这种区别是根据方法行为的不同而表示出来的,虽然这些方法都可以通过同一个基类来调用。
1、再论向上转型。我们把对某个对象的引用视为对其基类型的引用的做法称为向上转型--因为在继承树的画法中,基类是放置在上方的。为什么我们要向上转型成基类引用呢?主要还是为了复用代码。比如我们有一个car类。car有一个方法是move。不向上转型的话,我们就需要在类开始的地方,写调用各种汽车move的方法。如果可以向上转型,就可以动态绑定方法,我们便无须再写调用各种汽车的方法,只需写调用car的move方法。
class H extends Car{
public void move(){System.out.print("H Move")}; }
class K extends Car{
public void move(){System.out.print("K Move")};
}
public class Dirve{
//如果使用向上转型,这个go方法就可以不用写了,不然的话,以后万一又增加一个新的Car类,比如L,那又得在这里增加一个go(L l)方法。很不方便。
public static void go(H h){
h.move();
}
public static void go(K k){
k.move();
}
public static void main(String[] args){
H h = new H();
K k = new K();
go(h);
go(k);
}
}
2、转机。
1)方法调用绑定。将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),叫做前期绑定。但如果传入的参数一开始编译的时候只有基类的引用,编译器并不知道该去调用哪个方法。所以,解决的办法就是后期绑定。后期绑定的含义就是在运行时根据对象的类型进行绑定。后期绑定也叫做动态绑定或运行时绑定。如果一种语言想实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器一直不知道对象的类型,但方法调用机制能找到正确的方法体,并加以调用。后期绑定机制随编程语言的不同而有所不同,但是只要想一下就会得知,不管怎样都必须在对象中安置某种“类型信息”。前面的例子中,只需要将go方法改成go(Car car)便可。
Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。将方法声明为final后,可以防止其他人覆盖该方法。也可以有效地“关闭”动态绑定。
2)产生正确的行为。一旦知道Java中所有方法都是通过动态绑定实现多态之后,我们就可以编写只与基类打交道的代码了,并且这些代码对所有的导出类都可以正确运行。或者换一种说法,发送消息给某个对象,让该对象去断定应该做什么事。
3)可扩展性。由于有多态机制。我们就可以根据自己的需求对系统添任意多的新类型。而不需增加或更改go()方法。大多数或者所有方法都会遵守go()的模型,而且只与基类接口通信。这样的程序是可扩展的,因为可以从通用的基类继承出新的数据类型,从而新添一些功能。那些操纵基类接口的方法不需要任何改动就可以应用于新类。
4)缺陷:“覆盖”私有方法。
public class PrivateOverride{ private void f(){ System.out.print("private f()");} public static void main(String[] args){ PrivateOverride po = new Derived();
po.f(); } } class Derived extends PrivateOverride{ public void f() { System.out.print("public f()"); } }
/output:
private f()
我们所期望的输出是public f(),但是由于private方法是自动认为是final方法,而且对导出类是屏蔽的。因此,这种情况下。Derived类中的f()方法就是一个全新的方法,基类中的f()方法在子类Derived中不可见,因此也不能被重载。只有非private方法才可以被覆盖。在导出类中,对于基类中的private方法,最好采用不同的名字。
5)缺陷:域与静态方法。只有普通的方法是多态的。
class Super{
public int field = 0;
public int getField(){ return field; }
}
class Sub extends Super{
public int field = 1;
public int getField() { return field; }
public int getSuperField() { return super.field;}
}
public class FieldAccess{
public static void main(String[] args){
Super sup = new Sub();
System.out.println("sup.field = "+sup.field +",sup.getField()="+sup.getField());
Sub sub = new Sub();
System.out.println("sub.field = "+sub.field +",sub.getField()="+sub.getField()+",sub.getSuperField()="+sub.getSuperField());
}
}/*Output:
sup.field = 0 ,sup.getField() = 1
sub.field = 1 ,sub,getField() = 1 , sub.getSuperField() = 0
当Sub对象转型为Super引用时,任何域访问操作都将由编译器解析,因此不是多态的。但这种情况一般都不会发生,因为所有的域一般都是private的,都不能直接访问。只有通过方法才能够访问(例如get方法)。
而且当一个方法是静态的,它的行为也就不具有多态性。静态方法是与类,而并非与单个对象相关联的。
3、构造器和多态。首先,构造器并不具有多态性。(它们实际上是static方法,只不过该static声明是隐式的)。
1)、构造器的调用顺序。1.调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然而是下一层导出类,等等,直到最低层的导出类。2.按声明顺序调用成员的初始化方法。3.调用导出类构造器的主体。
2)、继承与清理。通过组合和继承来创建新类时,永远不必担心对象的清理问题。子对象通常都会留给垃圾回收器进行处理。如果确实遇到问题,那么必须用心为新类创建dispose()方法(名称可以自己定)。并且由于是继承的缘故,如果我们有其他作为垃圾回收一部分的特殊清理动作,就必须在导出类中覆盖dispose()方法。当覆盖被继承类的dispose()方法时,务必记住调用基类的dispose()方法。否则,基类的清理动作就不会发生。
3)、构造器内部的多态方法的行为。在一般的方法内部,动态绑定的调用是在运行时才决定的,因为对象无法知道它是属于方法所在的那个类,还是属于那个类的导出类。如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义。然而这个调用的效果可能相当难于预料。因为被覆盖的方法在对象被完全构造之前就会被调用。这可能会造成一些难于发现的隐藏错误。从概念上讲,构造器的工作实际上是创建对象。在任何构造器的内部,整个对象都可能只是部分形成--我们只知道基类对象已经进行初始化。如果构造器只是在构建对象过程中的一个步骤,并且该对象所属的类是从这个构造器所属的类导出的。那么导出部分在当前构造器正在被调用的时刻仍旧是没有被初始化的。然而,一个动态绑定的方法调用却会向外深入到继承层次结构内部,它可以调用导出类里的方法。如果我们是在构造器内部这样做,那么就有可能调用某个方法,而这个方法所操纵的成员可能还未进行初始化。
class Glyph{
void draw() { System.out.print("Glyph.draw()");}
Glyph(){
System.out.print("Glyph() before draw()");
draw();
System.out.print("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph(){
private int radius = 1;
RoundGlyph(int r){
radius = r;
System.out.print("RoundGlyph.RoundGlyph(),radius = "+radius);
}
void draw(){
System.out.print("RoundGlyph.draw(),radius = "+radius);
}
}
public class PolyConstructors{
public static void main(String[] args){
new RoundGlyph(5);
}
}/Output:
Glyph() before draw
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
我们期望的输出应该是1,但是一开始的输出确是0 正这里我们得更加详细地介绍一下初始化顺序了。
1.在其他任何事物发生之前,将分配给对象的存储空间初始化为二进制的零。
2.调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然而是下一层导出类,等等,直到最低层的导出类
3.按照声明的顺序调用成员的初始化方法。
4.调用导出类的构造器主体。
4、协变返回类型。Java SE5中添加了协变返回类型,它表示在导出类的被覆盖方法可以返回基类方法的返回类型的某种导出类型。
5、用继承进行设计。当在使用现成的类来建立新类时,首先应当还是考虑“组合”。组合不会强制我们的程序设计进入继承的层次结构中,而且,组合更加灵活,因为它可以动态选择类型。相反,继承在编译时就需要知道确切类型。比如我new 一个Car 。我可以在程序运行过程中让它从奔驰变为宝马,也可以变成奥迪(这个也称为状态模式)。但是继承,我一旦new了,奔驰就是奔驰,并不能再变成宝马。我们不能在运行期间决定继承不同的对象。
1)、纯继承与扩展。纯继承:只有在基类中已经建立的方法才可以在导出类中覆盖。就是“is-a”的关系。因为一个类的接口已经确定了它应该是什么。继承可以确保所有的导出类具有基类的接口,且绝对不会少。这就是一种纯替代。因为导出类可以完全替代基类,使用时,完全不需要知道关于子类的任何额外信息。扩展:就是“is-like-a”的关系。因为导出类就像是一个基类--具有相同的基本接口,但是它还具有由额外方法实现的其他特性。虽然这是一种有用的方法,但还是要依赖于具体的情况。它的缺点是,导出类中接口的扩展部分不能被基类访问。因此一旦向上转型,就不能调用那些新方法。
2)、向下转型与运行时类型识别。在Java中,所有转型都会得到检查。这些都是在运行期间对类型进行检查的行为。一般来说,如果你知道这个对象的具体类型,就可以尝试向下转型。如果所转类型是正确的,那么转型成功。如果是错误的,便会返回一个ClassCastException异常。