上一节的学习中,强调继承一般在需要向上转型时才有必要上场,否则都应该谨慎使用;

向上转型和绑定

向上转型是指子类向基类转型,由于子类拥有基类中的所有接口,所以向上转型的过程是安全无损的,所有对基类进行的操作都可以同样作用于子类;如示例代码中,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类接口之外的方法,否则将会产生类转型异常;

thinkinginjava学习笔记07_多态的更多相关文章

  1. SQL反模式学习笔记7 多态关联

    目标:引用多个父表 反模式:使用多用途外键.这种设计也叫做多态关联,或者杂乱关联. 多态关联和EAV有着相似的特征:元数据对象的名字是存储在字符串中的. 在多态关联中,父表的名字是存储在Issue_T ...

  2. No2_3.接口继承多态_Java学习笔记_多态

    ***多态***1.多态性:通常使用方法的重载(Overloading)和重写(Overriding)实现类的多态:2.重写之所以具有多态性,是因为父类的方法在子类中被重写,方法名相同,实现功能不同. ...

  3. thinkinginjava学习笔记01_导论

    初学java,希望旅途愉快  :) 类型决定对象的接口,(有人认为类是类型的特定实现),接口确定对象所能发出的请求(消息),满足请求的代码和隐藏的数据一起构成实现: 对象设计时,应该很好地完成一项任务 ...

  4. 1.12(java学习笔记)多态及向上、向下转型

    一.多态 多态是指同一个方法被调用,由于对象不同导致行为不同. 例如调用自由活动方法,张三喜欢玩耍,那么他就会去玩耍. 李四喜欢学习,那么他可能去学习.调用方法因对象的不同 而产生了不同的行为. 形成 ...

  5. Thinking in java学习笔记之多态

    多态是一种将改变的事物和未变的事物分离开来的重要技术.

  6. Java学习笔记之多态

    1.父类型的引用可以指向子类型的对象: Parent p = new Child(); 2.当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误:如果有,再去调用子类的该同名方法 ...

  7. thinkinginjava学习笔记06_复用类

    MarsEdit粘代码好麻烦,所有代码交给github:https://github.com/lozybean/MyJavaLearning 复用一个类常用的两种方式:组合.继承: 组合 将对象引用置 ...

  8. thinkinginjava学习笔记04_初始化与清理

    java沿用了c++的构造器,使用一个和类名完全一样的方法作为类的构造器,可以有多个构造器来通过不同的参数进行构造,称为重载:不仅是构造器可以重载,其他方法也一样通过不同的形参以及不同的返回值来实现重 ...

  9. C++学习笔记:多态篇之虚析构函数

    动态多态中存在的问题:可能会产生内存泄漏! 以下通过一个例子向大家说明问什么会产生内存泄漏: class Shape//形状类 { public: Shape(); virtual double ca ...

随机推荐

  1. java把html标签字符转普通字符(反转换成html标签)(摘抄)

    下面是java把html标签字符转换,我用了spring 包中的 org.springframework.web.util.HtmlUtils 了解了源代码并且进步了使用,发现写得真不错...同时也可 ...

  2. UWP 常用文件夹

    ①KnownFolders KnownFolders.PicturesLibrary 等等列举 ②ApplicationData.Current ApplicationData.Current.Loc ...

  3. Date( )方法 章节中,你可以查看更多关于日期转换为字符串的函数

    在 Date 方法 章节中,你可以查看更多关于日期转换为字符串的函数: 方法 描述 getDate() 从 Date 对象返回一个月中的某一天 (1 ~ 31). getDay() 从 Date 对象 ...

  4. C#判断ListBox是否显示了水平滚动条/横向滚动条

    参看: Windows消息定义网址:http://wenku.baidu.com/link?url=9fesYjbLSDx9_TsLgSZSVoR7ELal-60x2p-lua_iPR44Xfekz0 ...

  5. 【NOIP2016提高组】愤怒的小鸟

    https://www.luogu.org/problem/show?pid=2831 BFS 看到N这么小就可以想到搜索,求最少步数显然应该用BFS. 在这题中过两猪可以唯一确定一条抛物线,每一步可 ...

  6. 【NOIP2015提高组】子串

    https://daniu.luogu.org/problem/show?pid=2679 看到方案数问题直觉就能想到DP,考虑用f(i,j,k)表示A[1...i]取k个子串组成B[1...j]的方 ...

  7. Linux定义变量的脚本

    现有两段基本一样的代码,只是变量进行改变,其他都没有变化,但是执行过程中出现了不一样的结果 代码一: vi back.sh #backup import file,such as /etc/rc.lo ...

  8. C#复习资料

    C#期末考试复习题 一.单项选择题(每小题2分,共20分) 1.在类作用域中能够通过直接使用该类的(   )成员名进行访问. A. 私有      B. 公用      C. 保护      D. 任 ...

  9. Linux禁用显示“缓冲调整”

    Linux禁用显示"缓冲调整" youhaidong@youhaidong-ThinkPad-Edge-E545:~$ free -o total used free shared ...

  10. Android自己定义组件之日历控件-精美日历实现(内容、样式可扩展)

    需求 我们知道.Android系统本身有自带的日历控件,网络上也有非常多开源的日历控件资源.可是这些日历控件往往样式较单一.API较多.不易于在实际项目中扩展并实现出符合详细样式风格的,内容可定制的效 ...