TJI读书笔记11-多态

body, td {
font-family: 微软雅黑;
font-size: 10pt;
}

多态是数据抽象和继承之后的第三种基本特征. 一句话说,多态分离了做什么怎么做(再次对埃大爷佩服的五体投地,简直精辟啊). 是从另外一个角度将接口和实现分离开来.
封装通过合并特征和行为来创建新的数据对象,通过私有化隐藏细节,把接口和实现分离开来. 多态则是消除类型之间的耦合关系. 继承是允许将对象视为自己本身的类型或者基类型来处理.

再说说向上转型

把某个对象的引用视为对其基类型的引用的做法被称为向上转型. 为什么老是说向上转型呢,因为在UML图中,一般基类都在子类的上面…
多态是个什么样子

class Instrument{    public void paly(Note m){        System.out.println("Instrument play()");    }}class Wind extends Instrument{    public void paly(Note m){        System.out.println("Wind play()");    }}

public class Music {    public static void tune(Instrument i){        i.paly(Note.C_SHARP);    }    public static void main(String[] args) {        Wind flute = new Wind();        tune(flute);    }}

这里针对tune方法,传入的类型是Instrument,而实际执行的时候传入的是Wind,不但没有报错,而且还得到了正确的结果. 这是哪位天使大姐发力了吗? 显然不是的. 这就是多态. 也就是说,我们只需要在基类中定义一个方法,在子类中如果该方法被重写了,那么,在调用相关方法的时候,编译器会自动的去调用子类中重写的方法. 这就是传说中的多态.

没有多态的时候如何实现上面的效果
如果没有多态的特性,我们想达到上面的效果也是有办法的.可以使用方法重载. 比如,可以在music类中定义多个tune方法.public static void tune(wind i),public static void tune(Stringed i),public static void tune(Brass i)等等等. 这样就可以通过方法重载来实现类似这样的效果. 但是,作为一个正常的人类,不觉得这玩意儿有点反人类吗…

多态的原理

为什么会有多态这种现象,这就要聊到程序的方法运行绑定问题了. 绑定就是将一个方法调用与方法主题关联起来的动作. 像C语言这样的程序设计语言都是用一种叫做前期绑定的方法.也就是说在程序执行之前就要做绑定. 一般都是由编译器和连接器实现的. 也就是说程序在运行的时候已经固定了方法的入参类型了. 如果有错的话,就报错呗.

后来啊 ,改革开放过后(这只是一句口头禅,没有具体时间意义),出现了一种叫做后期绑定或者叫动态绑定又或者叫运行时绑定的技术. 也就是说,存在一种在运行的时候去判断对象类型的机制. 编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用. 不同语言的实现不一样.

比如,在子类中的那个基类的子集里的方法调用的指针被更改到了子类中重写的方法. 那在执行tune方法的时候,先讲Wind类型向上转型成Instrument类型,再调用的时候,就会直接去调用Wind类型中定义的方法体了. C++就是这样实现的,每个对象都会维护一张虚表,虚表中的虚函数指针默认初始化为自身(黑色箭头所指)然后通过一个virtual关键字来指定该函数为虚函数,如果出现上述这种调用方式的时候,编译器会讲该虚函数指针指向子类中重写的方法(红色箭头所指). (这是个人的理解啊,我比较菜,还没有看到哪个权威的文档上说java是这样实现的…)

java中static和final的方法(private的方法默认都是final的,所以也算),其他的方法都是动态绑定的. 又说到final方法了,final的方法会被关闭动态绑定. 这样的话,编译器在效率上可能会高一点. 但是不好说. 所以这不能作为使用final修饰方法的一个理由. 还是那句话,通过使用final修饰方法来优化程序是不靠谱的. final使用的唯一考量就是设计需要.

多态带来的可扩展性
有了多态机制之后,带来了一个好处,那就是在使用一个类的时候,可以只与基类的接口通信,这样也可以达到我们想要的结果. 那么程序的可扩展性就大大增强了. 多态带来的思想是,将改变的事物(方法实现)和没有改变的事物(方法签名)分离开来.

哪些方法是可以被覆盖的
首先,private的方法肯定是不可以的. private是被封装在类内部的,无法在类的外部调用. 在实例中都没有办法直接调用,那就没有多态可谈了.
其次,final的方法肯定也是不行的,final的方法不允许被重写.

关于private有一个问题,如果在子类中有一个跟基类的某个private方法名字一毛一样的方法,这算什么?什么也不算,不构成重写,只是看作是基类中的一个普通的方法而已.

最后,static的方法,static方法属于类本身. 不存在重写的问题. 也就不存在多态了. 在上面的原理中已经讲的很清楚了,多态发生在实例方法的调用中,对于静态方法是不构成多态的.

简单来说,构成多态需要两个条件.

  • 有继承关系

  • 子类中重写父类的方法.

构造器和多态

构造器是static的方法,虽然是隐式的.

class Meal{    Meal(){System.out.println("Meal()");}}

class Bread{    Bread(){System.out.println("Bread()");}}

class Lettuce{    Lettuce(){System.out.println("Lettuce()");}}class Cheese{    Cheese(){System.out.println("Cheese()");}}class Lunch extends Meal{    Lunch(){System.out.println("Lunch()");}}class ProtableLunch extends Lunch{    public ProtableLunch() {        System.out.println("ProtableLunch()");    }}public class Sandwich extends ProtableLunch{    private Bread b = new Bread();    private Cheese c = new Cheese();    private Lettuce l = new Lettuce();

public Sandwich(){System.out.println("Sandwich()");}

public static void main(String[] args) {        new Sandwich();    }}

一个小例子来完善一下类的初始化过程.

  • 调用基类的构造器,这个过程会一直反复递归下去,显示从根基类开始,一层层的往下找.

  • 按照声明的顺序初始化成员.
  • 调用导出类的构造器主体

继承和清理动作
前面说过,垃圾清理可能不会被调用而且仅仅清理堆上的实例对象. 有时候如果希望更多更及时的清理就需要我们自己书写一个自定义的清理方法. 那么在继承的过程中. 千万不要忘了,如果在子类中重写了这个清理方法的话,在方法内一定要通过super来调用基类的清理方法. 不然,这个方法被覆盖了,基类的清理方法中定义的动作就得不到执行.
还有一点就是,清理过程和初始化过程是相反的. 会先调用自身的清理方法,再调用基类的清理方法.

还有一个问题,如果成员对象中存在于其他一个或者多个对象共享的情况,需要显式的使用引用计数器来跟踪仍旧访问共享对象的对象的数量. 不能随意清理.
来吧,一码解千愁

class Shared{    private int refcount =0;    private static long counter = 0;    private final long id = counter++;    public Shared(){        System.out.println("Ceeating "+this);    }    public void dispose(){        if(--refcount==0){            System.out.println("dispose "+this);        }    }    public void addRef(){        refcount++;    }    public String toString(){        return "shared "+id;    }}

class Composing{    private Shared shared;    private static long counter = 0;    private final long  id = counter++;    public Composing(Shared shared){        System.out.println("Creating "+ this);        this.shared = shared;        this.shared.addRef();    }    protected void disposed(){        System.out.println("disposing "+this);        shared.dispose();    }    public String toString(){        return "Composing "+id;    }

}public class ReferenceCounting {    public static void main(String[] args) {        Shared shared = new Shared();        Composing[] composing = {new Composing(shared),new Composing(shared),new Composing(shared),new Composing(shared)};        for(Composing c:composing){            c.disposed();        }    }}/*out:Ceeating shared 0Creating Composing 0Creating Composing 1Creating Composing 2Creating Composing 3disposing Composing 0disposing Composing 1disposing Composing 2disposing Composing 3dispose shared 0*/

这个例子里使用了引用计数器. 只有当引用计数器归零的时候,才清除shared对象.

构造器内部的多态方法的行为
按道理来说,这种代码是不应该存在的. 构造器用来创建和初始化对象. 而多态的行为是属于实例方法的. 也就是说多态的正常应用场景应该是在对象构建完成之后. 那么,如果在构造器中去调用一个实例方法,在构造的工程中应用多态会发生什么呢?

class Glyph{    void draw(){        System.out.println("Glyph.draw()");    }    Glyph(){        System.out.println("Glyph start");        System.out.println("Glyph() before draw()");        draw();        System.out.println("Glyph() after draw()");        System.out.println("Glyph end");    }}

class RoundGlyph extends Glyph{    private int radius =1;    public RoundGlyph(int r) {        System.out.println("RoundGlyph start");        radius = r;        System.out.println("RoundGlyph.RoundGlyph().radius = "+ radius);        System.out.println("RoundGlyph end");    }    void draw(){        System.out.println("RoundGlyph.draw().radius = "+radius);    }}

public class PolyConstructors {    public static void main(String[] args) {        new RoundGlyph(5);    }}/*out:Glyph startGlyph() before draw()RoundGlyph.draw().radius = 0Glyph() after draw()Glyph endRoundGlyph startRoundGlyph.RoundGlyph().radius = 5RoundGlyph end*/

首先在基类的构造方法中调用了draw()方法,那么在new RoundGlyph实例的时候,RoundGlyph的构造器会先执行基类的构造器. 所以Glyph()被调用了.而在Glyph()里调用的drwa()方法,那么由于多态,会执行RoundGlyph类中的draw,而这个时候,RoundGlyph实例并没有构造完成. 所以得到的值与期望值不一样. 那为什么这里会是0呢?
记得之前在初始化那一节的时候说过初始化的顺序:

  • 分配内存空间并初始化为二进制的0

  • 调用基类的构造器
  • 按照声明顺序调用成员的初始化方法
  • 调用子类的构造器.

那么,在Glyph()被调用的时候,第一步的初始化已经完成了. 所以,我们看到的是个0;这么做有个好处,在任何方法(包括构造方法)执行前,所有东西都已经被初始化为”空”了. 至少保证是有初始化状态的,如果出了问题,无论是报异常还是结果出错都相对来说都比较方便定位问题.

上面这个例子可能很多人都不会想到,因为大家一直都默认一条规定:构造器中使用尽可能简单的方法完成初始化,让对象进入正常状态就可以了. 如果有可能,避免在构造器中调用其他方法.

协变返回类型

之前说的多态都只解决了一个问题,对于子类中被覆盖的基类方法,入参类型不同可以构成多态. 那多态中的方法的返回值会有什么改变吗?

class Grain{    public String toString(){        return "Grain";    }}

class Wheat extends Grain{    public String toString(){        return "Wheat";    }}

class Mill{    Grain process(){        return new Grain();    }}

class WheatMill extends Mill{    Wheat process(){        return new Wheat();    }}public class ConvarianReturn {    public static void main(String[] args) {        Mill m = new Mill();        Grain g = new Grain();        System.out.println(g);        m = new WheatMill();        g = m.process();        System.out.println(g);    }}/*out:GrainWheat*/

在java SE1.5之前,重写基类方法的时候 返回值必须跟基类中方法的一致. 1.5之后,放宽了这个限制. 返回值可以是基类中被覆盖方法的返回值的子类. 也就是说可以是更具体的类型了.

使用继承进行设计

之前已经说过,在设计的时候,组合的方式是首选的考量. 组合更加灵活,而且还有多态的特性可以为它动态的选择类型.而继承必须在编译是就知道确切的类型. 一般来说的准则是用继承来表达行为见的差异,并且用字段来表达状态上的变化.

状态模式

class Leg{    public void walk(){} }

class SoberLeg extends Leg{    public void walk(){        System.out.println("walk in line");    }}

class DrunkenLeg extends Leg{    public void walk(){        System.out.println("zzz...");    }}

class Person{    private Leg  legs= new SoberLeg();    public void change() {legs = new DrunkenLeg();}    public void walkWithLegs(){legs.walk();}}public class PersonTest {    public static void main(String[] args) {        Person p = new Person();        p.walkWithLegs();        p.change();        p.walkWithLegs();    }}

这个例子的设计不太合理,但是能表示那个意思. 就是使用组合的时候,完全可以在运行时,把一个对象的引用指向另外一个对象,然后时其行为发生改变.

纯继承,扩展和向下转型
最理想的继承方式应该是,子类和基类有一毛一样的接口. 也就是说是一种纯粹的”is-a”的关系. 也可以认为这是一种纯替代.
在这种状态下多态可以处理一切的事情.
但是理想永远是人们的一种美好的可望不可及的期待,java中继承的关键字是extends,这也说明,继承更多时候需要的是扩展基类接口. 也就是说子类是基类的一个超集. 这种关系可以说是一种 “is-like-a”的关系. 在这种关系里,向上转型的话,那子类中扩展的方法是无法使用的.同时带来了向下转型的风险. 使用”()”的强转方式依旧可以向下转型. java在运行时会做类型检查,如果不是我们期望的类型,会报ClassCastException.

时间: 2024-08-23 13:24:14

TJI读书笔记11-多态的相关文章

TJI读书笔记12-接口

body, td { font-family: 微软雅黑; font-size: 10pt; } TJI读书笔记12-接口 抽象类和抽象方法 接口 完全解耦和策略模式 接口间的继承关系 工厂模式 乱七八糟不知道怎么归类的知识点 接口和抽象类为我们提供了更强又有力的接口和实现分离的方法. 抽象类和抽象方法 抽象类的目的是为了给子类提供一个通用接口,通过这样一个通用的接口,可以操纵一系列的子类. 也就是说,只需要通过相同的接口调用就可以操作不用的实现. 这也就是所谓的接口和实现的分离. 抽象类简单来

TJI读书笔记15-持有对象

body, td { font-family: 微软雅黑; font-size: 10pt; } TJI读书笔记15-持有对象 总览 类型安全和泛型 Collection接口 添加元素 List 迭代器 LinkedList 栈 Set Map Queue Collection和Iterator Foreach与迭代器 总结 总览 It's a fairly simple program that only has a fixed quantity of objects with known l

TJI读书笔记16-异常处理

TJI读书笔记16-异常处理 概念 基本异常情形 异常的捕获 自定义异常 异常说明 捕获所有异常 栈轨迹 重新抛出异常 Java标准异常 使用finally 异常的限制 构造器 异常的匹配 其他乱七八糟 概念 在早期没有专门的异常处理机制的时候,比如C语言,会通过一些约定俗成的东西来处理异常. 比如让程序返回某个特殊的值或者设置某个标记. 然后对返回值进行检查以判断程序是否出错. 还记得以前C语言的时候,return 0和return -1对异常处理的实现可以追溯到BASIC中的on error

TJI读书笔记07-初始化

body, td { font-family: 微软雅黑; font-size: 10pt; } TJI读书笔记07-初始化 成员初始化 构造方法初始化 初始化块 初始化的顺序 成员初始化 java尽量去保证每个变量在使用前都会得到初始化. 对于方法局部变量,java不会自动初始化他们,如果没有显式的初始化,编译器会报错. 对于类的数据成员,java会自动初始化成一个"空""的值.简单来说,这个空的值对于基本数据类型就是,0,false,和空格. 对于引用类型就是null.

TJI读书笔记09-访问控制权限

body, td { font-family: 微软雅黑; font-size: 10pt; } TJI读书笔记09-访问控制权限 包,package和import 权限修饰符 接口和实现 类的访问权限控制 首先问一个问题,为什么要有访问控制权限? 安全,这当然是一个很重要的原因. 让类库的使用者只能接触他做需要接触的东西. 另外一方面,当我们去重构和修改代码的时候,如何不影响其他的代码和功能?权限访问控制是可以很好的将"变动的事物"和"不变的事物"区分开来.比如一

TJI读书笔记10-复用类

body, td { font-family: 微软雅黑; font-size: 10pt; } TJI读书笔记10-复用类 组合语法 继承语法 代理 final关键字 final的数据 final的参数 final的方法 final的类 初始化和类的加载 乱七八糟不知道怎么归类的知识点 代码复用是java众多牛逼哄哄的功能之一(好像OOP语言都可以呢-),代码复用的理想状态是,使用类但是又不破坏现有代码. 当然住了复制粘贴以外还有其他的方法. 一个叫组合,一个叫继承. 组合语法 其实刚开始我非

<C和指针---读书笔记11>

对于构成字符串的,string.h封装了许多现成的函数以供使用. 字符串,不论是常量字符串还是借用数组尾缀'\0'.  这个strings.h里面的函数都是针对的字符串. 即以'\0'做为截至. 如果你不幸的对 char数组操作,可能得到的结果并不是你想要的结果. strlen函数  unsigned int  strlen (char const *string) 参数列表: 是一个指针或者数组名即可. 返回值:  返回一个无符号数.  如果两个返回值进行比较大小,可以使用  strlen(

分布式系统的烦恼------《Designing Data-Intensive Applications》读书笔记11

使用分布式系统与在单机系统中处理问题有很大的区别,分布式系统带来了更大的处理能力和存储容量之后,也带来了很多新的"烦恼".在这一篇之中,我们将看看分布式系统带给我们新的挑战. 1.故障 当我们在使用单机系统时,它通常以一种相当可预测的方式工作:要么它正常工作,要么不工作. 而当我们在使用分布式系统时,情况就不同了.在分布式系统中,系统的某些部分可能以某种不可预知的方式被破坏,即使系统的其他部分工作正常.这种故障通常是不确定的:如果你想做涉及多个节点和网络的东西,可能甚至不知道某个消息是

存储器的保护(一)——《x86汇编语言:从实模式到保护模式》读书笔记18

本文是原书第12章的学习笔记. 说句题外话,这篇博文是补写的,因为让我误删了,可恶的是CSDN的回收站里找不到! 好吧,那就再写一遍,我有坚强的意志.司马迁曰:“文王拘而演<周易>:仲尼厄而作<春秋>:屈原放逐,乃赋<离骚>:左丘失明,厥有<国语>:孙子膑脚,<兵法>修列:不韦迁蜀,世传<吕览>……”好了,不煽情了,进入正题. 第12章的代码如下. 1 ;代码清单12-1 2 ;文件名:c12_mbr.asm 3 ;文件说明:硬盘主引