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

向上转型和绑定

向上转型是指子类向基类转型,由于子类拥有基类中的所有接口,所以向上转型的过程是安全无损的,所有对基类进行的操作都可以同样作用于子类;如示例代码中,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. HDU5983Pocket Cube

    Pocket Cube Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others) Tota ...

  2. python学习之路day1

    学习总结: 变量,字符的由来,python2和python3的区别,控制语句:if,for,while,break,continue用法 学习示例: if用法1:判断年龄 # -*- coding: ...

  3. SQL Server Profiler追踪数据库死锁

  4. day1 python基础知识

    一:python发展 python2.6与python3.0区别: 源码不标准,混乱,重复代码过多 二:python所属类型 (1)编译型:一次性将程序全部编译成二进制 优点:运行速度快 缺点:不能跨 ...

  5. 自己动手写http服务器——处理http连接(二)

    关于http报文格式请看这篇文章 //http_conn.h #ifndef HTTPCONNECTION_H #define HTTPCONNECTION_H #include <unistd ...

  6. 》》ajax加蒙版

    在与后台交互时,用时过长.禁止页面操作等,有提示,增强页面体验: $.ajax({ type:'POST',url:url,data:obj,dataType:'json',beforeSend: f ...

  7. Sqoop2安装记录

    我是採用的源代码编译的包安装的, 主要是考虑到会对部分功能做裁剪或增强, 详细源代码编译方式能够參考另外一篇博文<编译Sqoop2错误解决>.然后从dist/target文件夹下拷贝sqo ...

  8. flask中的session,render_template()第二和参数是字典

    1. 设置一个secret_key 2.验证登入后加上session,这是最简单,不保险 . 3.注意render_template传的参数是字典

  9. cglib动态代理举例

    jdk的动态代理是基于接口的代理,而cglib不要求实现接口,是一种基于继承的代理,使用字节码生成被代理类的子类 public class TestMethodInterceptor implemen ...

  10. C#:MVC引用Log4Net生成错误日志

    第一步:引用log4net配置文件 第二步:在自己项目下新建文件夹LogNet,再在里面建立类Log.cs log.cs内容如下: 第三步:在自己项目下新建Log4Net.config Log4Net ...