thinkinginjava学习笔记07_多态
在上一节的学习中,强调继承一般在需要向上转型时才有必要上场,否则都应该谨慎使用;
向上转型和绑定
向上转型是指子类向基类转型,由于子类拥有基类中的所有接口,所以向上转型的过程是安全无损的,所有对基类进行的操作都可以同样作用于子类;如示例代码中,Music.tune方法调用时,需要的参数是基类Instrument,而传入一个子类:Wind类的对象时,该方法一样可以被调用,并且play方法执行的是Wind类的对象重载的方法;
在向上转型的设计中,只编写和基类打交道的代码,这样所有的特定子类都可以正确使用该方法,而不用针对每一个子类都编写特定的代码;在一个经典的例子中(示例代码):一个基类Shape派生出很多子类(各种形状),每个子类都对基类的接口进行了重载,工厂类在生成子类时,返回的是基类对象(准确的说是子类对象实体的基类对象引用),而创建的实际对象则是子类对象;由工厂创建的对象调用重载方法时,依然可以正确调用子类重载的方法;这是由于Java中,除了static和final(包括private),所有的对象都是后期绑定,也就是在编译时,执行的方法并不知道执行主体的真正类型,只有当执行的时候才会确定该对象的真正类型,虽然工厂创建的是基类对象引用,当调用该对象的方法时,由于该引用指向的实体子类对象会和该方法完成绑定,并被解释器解释;
缺陷
但是,由于static和final(包括private)方法是前期绑定的,也就是在编译时,方法就和类型进行了绑定,只能调用绑定类型的方法;如在示例代码中,PrivateOverride po = new Derived();虽然子类Derived中有新的方法:f(),但是由于基类中的f()是private,对子类是不可见的,所有子类并没有实现重载,而只是写了一个同名的方法而已;由于PrivateOverride类中,f()方法是private,在编译时,po.f();调用一个基类引用的private方法,编译器执行了基类对象和f()方法的绑定,所以虽然对象实体是Derived对象,但是执行po.f()时,仍然执行的是基类中的方法;
这里就会有一个陷阱,由于private是对使用者不可见的,所以并不能知道继承的基类中是否有某种private方法,如果在子类中实现同名方法,并且使用基类引用来引用子类对象实体的话,子类中实现的同名方法就不会得到正确调用;
除了private,相似的问题出现在对静态方法和域(数据对象)的访问时,如示例代码中,基类引用sup和子类引用sub,虽然对象实体都是Sub对象,但是两个引用访问得到的field却是完全不同的;并且sub引用的对象其实包含了两个成为field的域,但是直接调用时的默认field是Sub版本中的field,如果想要调用Super版本中的field,则必须显式地使用super.field调用;但是域的访问一般不会出错,因为通常都会将域设为private,此时,sup引用的对象实体是Sub,并不能完成对private field的调用,只能通过getField方法进行获取,而由于getField方法并不是final的,此时就避免了该问题;
而静态方法则由于是只和类相关联,所以也并不具备多态性;
构造器和多态
通过一个例子来复习继承中构造器的执行顺序以及潜在的问题:示例代码;输出结果为:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
执行new RoundGlyph(5);时,初始化过程为:
1. 在任何事物发生之前,将分配给对象的存储空间初始化为二进制0;(较之前新增);
2. 加载基类Glyph,并且Glyph并没有其他基类,执行Glyph的static初始化(并没有),执行Glyph构造器;打印
Glyph() before draw()
调用draw()方法,此时由于RoundGlyph中对draw()进行了重载,所以将调用重载的draw()方法,此时RoundGlyph并没有完成初始化,由于第一步分配,radius=0,故打印:
RoundGlyph.draw(), radius = 0
然后打印:
Glyph() after draw()
3. 加载子类RoundGlyph,执行static初始化,radius=1,执行RoundGlyph构造器,并打印
RoundGlyph.RoundGlyph(), radius = 5
在这个过程中,在构造器中添加了后期绑定的方法:draw(),导致执行出现逻辑上的问题,由上例可以总结出构造器的编写准则:
用尽可能简单的方法使对象进入正常状态;如果可以的话,避免使用其他方法;构造器中唯一可以调用的方法是基类中的final方法;
协变返回类型
在Java SE5之后,子类中的重载方法可以返回基类方法的返回类型的某种子类;如:示例代码中:
Mill m = new Mill();
Grain g = m.process();
println(g);
m = new WheatMill();
g = m.process();
println(g);
由于m是一个Mill类的引用,m = new WheatMill()表示一个Mill的引用引用了WheatMill对象(向上转型),而process()方法实现了重载,而在JavaSE5中添加的规则,该方法可以返回为Grain的子类Wheat,因此,m.process()返回的是一个Wheat对象实体,用一个Grain类的引用g来引用该实体;
而在这之前,m.process()的返回值将强制返回为Grain,而不能返回为Wheat;
继承设计
这个问题在前一篇随笔中也有提到,这设计时,应该优先使用组合的方式,而继承应该在需要被向上转型时用到;一条通用准则是:用继承来表达行为之间的差异,并用字段(即组合)来表达状态上的变化;如:示例代码中,Stage类中包含一个基类Actor的引用,并初始化为HappyActor,但是change方法可以改变该引用指向的具体对象实体,比如程序中将其改变为Actor的另外一个子类:SadActor中,此时就完成了状态的变化,Stage类的实例化对象相应的行为都统一做了改变;
(很cool!)
在继承设计时,只继承基类中的已有的方法,这样做可以避免一些继承带来的问题,而把子类看做是基类的一个替代(is-a关系),二者具有完全相同的接口;
但是在实际设计时,扩展接口是难以避免的,此时,子类中不仅仅有基本接口,还有一些额外方法实现的其他特性(is-like-a关系);此时,子类中的扩展部分并不能被基类访问,并且一旦完成向上转型,则不能继续调用扩展部分,如示例代码中,x[1].u()会产生错误;从这里也可以看到,虽然x[1]的对象实体是MoreUseful,并且执行重载方法时,方法是和MoreUseful对象完成后期绑定,但是x[1]仍然是一个Useful的引用,并不能调用任何Useful类接口之外的方法,否则将会产生类转型异常;