java编程思想8.多态

chentnt      2022-04-15     225

关键词:

在面向对象的程序设计语言中,多态是继数据抽象继承之后的第三种基本特征。
多态分离了“做什么”和“怎么做”,让接口和实现分离开,改善了代码的可读性和组织结构,创建了可拓展的程序。

  • 封装,通过合并特征和行为来创建新的数据类型。
  • 实现隐藏,通过将细节“私有化”把接口和实现分离开来。
  • 多态,消除类型之间的耦合联系。多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要他们都是从同一基类导出来的。-->这种区别是根据方法行为的不同而表示出来的,虽然这种方法都可以通过同一个基类来调用。

8.1 再论向上转型

像第七章所说的那样,对象既可以作为他自己本身的类型使用,也可以作为他的基本类型使用,把这种对某个对象的引用视为对其基类类型的引用的做法称之为向上转型(因为在 UML 继承树的画法中,基类是在上方的)。

在发生向上转型的时候,我们可以很清楚的感受到,系统和编写者都在刻意的去忽视传递对象的类型-->导出类的接口向上转型到基类,可能会“缩小”接口,但是一定不会比基类的全部接口更窄。
这意味着在我们编写方法时,只接收基类作为参数,那么这样就可以不必为每一种导出类编写对应的接受参数方法。这就是多态的一种方式。


8.2 转机

下面我们开始更进一步的讨论。

在向上转型的时候,方法接收基类的引用参数。那么编译器是如何得知引用是指向某一个特定导出类的呢。实际上编译器也无法得知,我们需要进一步了解绑定。

  • 将一个方法调用同一个方法主体关联起来被称为绑定
  • 若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),就叫做前期绑定。前期绑定是面向过程的语言中的默认的绑定方式
  • 而在运行时根据对象的类型进行绑定,被称为后期绑定,也叫做动态绑定运行时绑定。一种语言想实现后期绑定,就必须具有一种能在运行时能判断对象的类型从而调用恰当方法的机制。

回到上面的问题,编译器是如何得知引用是指向某一个特定导出类的呢。我们之所以有疑问,就在于如果使用前期绑定,那么当编译器只有一个基类引用的时候,他是不应该知道应该调用哪个方法才对。但是在后期绑定中,编译器其实不知道,也不需要知道导出类对象的类型,方法调用机制能够找到正确的方法体并加以调用。


Java 中,除了 static 方法和 final 方法(private 方法属于 final 方法),其他所有方法,都是后期绑定-->就是说,通常情况下不需要去判定是否应该进行后期绑定(他会自动发生)。
final 方法其实是有效地关闭了动态绑定(后期绑定),或者是告诉了编译器不需要对一个方法做动态绑定。

建立在上面的基础,我们可以很容易的理解:

List<Integer> intList = new ArrayList();
intList.get(0);

看起来 get() 方法调用的是 List 的引用,其实由于后期绑定(多态),调用的是 ArrayList.get() 方法。

编译器不需要获取任何特殊信息就能进行正确的调用,对方法的所有调用都是通过动态绑定进行的


在一个设计良好的 OOP 程序中,大多数或者所有方法,都会遵循特定方法的模型,而且只与基类接口通信,这样的程序就是可拓展的。-->可以从通用的基类继承出新的数据类型,操纵基类接口的方法也不需要任何改动就可以应用于新类。
多态最后的目的,就是希望能修改代码后,不会对程序中其他不受影响的部分受到破坏。


关于多态的缺陷:

  1. ”覆盖“私有方法:非 private 方法才能被覆盖,如果覆盖了 private 方法,那这个方法并不会被在导出类中被重载。因此,导出类中,对基类中的 private 方法最好不要采用相同的名字。
  2. 域与静态方法:其实只有普通的方法调用是多态的。其他情况,例如直接访问某个域,这个访问在编译器就会被解析。
    不过通常我们会将所有的域设置为 private,让他们不能被直接访问。
    另外也不会对基类中的域和导出类中的域赋予相同的名字。
    静态方法的行为不会具有多态性。静态方法是与类关联的,而不是和单个对象关联的。

8.3 构造器和多态

基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接-->每个基类的构造器都能得到调用。
这样做的意义在于:构造器能检查对象是否被正确构造,导出类只能访问自己的成员,基类的成员需要基类自己才能访问。只有在每个构造器都得到调用的情况下,才能保证无论在任何情况都能正确创造出完成的对象。

复杂对象调用构造器的顺序(不严谨的):

  1. 调用基类构造器。这个步骤是递归的,首先是构造这种层次结构的根,然后是下一层导出类,最后知道最低层的导出类。
  2. 按声明顺序调用成员的初始化方法。
  3. 调用导出类构造器主体。

如果不清楚可以按照下面的例子查看。

class Meal {
    Meal() { print("Meal()"); // 1}
}
class Bread {
    Bread() { print("Bread()"); }
}
class Cheese {
    Cheese() { print("Cheese()"); }
}
class Lettuce {
    Lettuce() { print("Lettuce()"); }
}
class Lunch extends Meal {
    Lunch() { print("Lunch()"); // 2}
}
class PortableLunch extends Lunch {
    PortableLunch() { print("PortableLunch()"); // 3}
}

public class Sandwich extends PortableLunch {
    private Bread b = new Bread(); // 4
    private Cheese c = new Cheese(); // 5
    private Lettuce l = new Lettuce(); // 6
    public Sandwich() { print("Sandwich()"); }
    public static void main(String[] args) {
        new Sandwich(); // 7
    }

输出如下:

1 Meal()
2 Lunch()
3 PortableLunch()
4 Bread()
5 Cheese()
6 Lettuce()
7 Sandwich()

通过组合和继承来创建新类,不用担心对象的清理问题。
不过加入面临这方面问题时(自定义对象清理)一定要注意,

  • 万一某个子对象要依赖于其他对象,销毁的顺序应该和初始化的顺序相反。
  • 对于字段,则意味着和声明的顺序相反(因为字段的初始化是按照声明的顺序进行的)。
  • 对于基类,应该先对其导出类进行清理,然后才是基类(因为导出类的清理可能会调用积累中的某些方法,要使方法可用则基类不能提前被销毁)。

当成员对象中存在于其他一个或者多个对象共享的情况,或许需要额外的引用计数来跟踪仍旧访问着的共享对象的对象数量。
可以使用由 tatic long 修饰一个静态成员,帮助跟踪所创建的类的实例的数量,还可以为 id 提供数值。


问题:如果在一个构造器内部调用正在构造的对象的某个动态绑定方法,那么对象无法知道自己是属于方法所在的那个类,还是属于那个类的导出类。
举例

class Glyph {
    void draw() { print("Glyph.draw()"); }
    Glyph() {
        print("Glyph() before draw()");
        draw();
        print("Glyph() after draw()");
    }
}   
class RoundGlyph extends Glyph {
    private int radius = 1;
    RoundGlyph(int r) {
        radius = r;
        print("RoundGlyph.RoundGlyph(), radius = " + radius);
    }
    void draw() {
        print("RoundGlyph.draw(), radius = " + radius);
    }
}   

public class PolyConstructors {
    public static void main(String[] args) {
        new RoundGlyph(5);
    }
}

输出

Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5

分析
Glyph.draw() 方法被设计为将要被覆盖,这种覆盖在 RoundGlyph 中发生。但是 Glyph 中调用了这个方法,导致了对 RoundGlyph.draw() 的调用。但是这个调用是有问题的,RoundGlyph 根本没有开始初始化,所以 radius 的值是默认初始值0.
结论
构造器初始化的实际过程是:

  1. 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制零。
  2. 调用基类构造器。
  3. 按照声明的顺序调用成员的初始化方法。
  4. 调用导出类的构造器主体。

为了避免发生上述这类不可控的风险,在编写构造器时,要用尽可能简单的方法使对象进入正常状态;如果可以的话尽量避免其他方法。在构造器中唯一能够安全调用的方法,是基类的 final 方法(也适用于 private 方法),这些方法不能被覆盖,也就不会出现上面的蛋疼问题。


8.4 协变返回类型

Java SE5中添加了协变返回类型,他表示在导出类中的被覆盖方法,可以返回基类方法的返回类型的某种导出类型。
就是说协变返回类型允许重写方法返回更具体的导出类型(如果基类方法返回的是基类的话)。


8.5 用继承进行设计

与继承相比,组合其实要更灵活,因为他可以动态选择类型(也就是选择了行为)。
而继承在编译时就要明确类型,不能再运行期间决定继承不同的对象。

一条通用的准则就是:用继承表达行为间的差异,并用字段表达状态上的变化。


如下图的继承方式,可以说是纯粹的继承层次关系-->只有在基类中已经建立的方法,才可以在导出类被覆盖。
技术分享图片

像这种纯继承模式,也被称作纯粹的“is-a”关系(是一个)-->这个类的接口已经确定了他应该是什么,导出类的接口绝对不会少于基类。
这种设计,保证基类可以介绍发送给导出类的任何消息(因为二者有着完全相同的接口)。只需要进行向上转型,就不需要知道正在处理的对象的确切类型。
技术分享图片

但是实际使用中,我们更倾向于使用“is-like-a”关系(像一个),因为导出类就像一个基类-->有着相同的基本接口,还具有额外的其他方法。但是这么做的话,当使用向上转型时,拓展接口就不能被调用了。
技术分享图片


使用向上转型的时候会丢失具体的类型信息。
使用向下转型可以获取到具体的类型信息。不过,向下转型是不安全的,因为基类接口会小于等于导出类的接口。

为了确保安全,Java 中所有的转型都会得到检查。如果检查未通过,会返回 ClassCastException(类型转换异常)
这种在运行期间对类型进行检查的行为被称为“运行时类型识别”,简称 RTTI(Run-time type information)。























java编程思想(八多态)

  在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。  多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程... 查看详情

《java编程思想》学习笔记——第八章多态

    在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征    多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态不但能够方法调用绑定   &nb... 查看详情

java多态向上造型

最近在读java编程思想,在读多态一章时,遇到了一个问题,在此记录一下。1packagemain.demo;23classSuper{4publicintfiled=0;56publicintgetFiled(){7returnfiled;8}9}1011classSubextendsSuper{12publicintfiled=1;1314@Override15publicintgetFiled 查看详情

java编程思想(20170816)

重载与覆写:1.重载(Overloading):Java的方法重载,就是在类中可以创建多个方法,它们具有相同的名字,但具有不同的参数和不同的定义,调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法,这就是多... 查看详情

java面向对象编程三大特征-多态

Java面向对象编程三大特征-多态本文关键字:Java、面向对象、三大特征、多态多态是面向对象编程的三大特征之一,是面向对象思想的终极体现之一。在理解多态之前需要先掌握继承、重写、父类引用指向子类对象的相关概念,... 查看详情

[读书笔记]java编程思想(代码片段)

目录第1章对象导论第2章一切都是对象第3章操作符第4章控制执行流程第5章初始化与清理第6章访问权限控制第7章复用类第8章多态第9章接口第10章内部类第11章持有对象第12章通过异常处理错误第13章字符串第14章类型信息第15章泛... 查看详情

java编程思想

Java编程思想,Java学习必读经典,不管是初学者还是大牛都值得一读,这里总结书中的重点知识,这些知识不仅经常出现在各大知名公司的笔试面试过程中,而且在大型项目开发中也是常用的知识,既有简单的概念理解题(比如i... 查看详情

java面向对象编程思想

丶冯小小 Java面向对象编程思想 面向对象三个特征:封装、继承、多态封装:   语法:属性私有化(private)、提供相对应的get/set的方法进行访问(public)、         在set/get的方法... 查看详情

java面向对象编程思想

面向对象三个特征:封装、继承、多态封装:   语法:属性私有化(private)、提供相对应的get/set的方法进行访问(public)、         在set/get的方法中对属性的数据做相对应的业务逻辑的... 查看详情

java编程思想-目录

....9.1参数化类型  1.10对象的创建和生命周期  1.12并发编程  1. 查看详情

java面向对象编程思想

      面向对象三个特征:封装、继承、多态封装:    语法:属性私有化(private)、提供相对应的get/set的方法进行访问(public)、在set/get的方法中对属性的数据做相对应的业务逻辑的判断 &... 查看详情

java编程思想thinkinginjava

Java编程思想【Thinkinginjava】目录:第1章对象导论1.1抽象过程1.2每个对象都有一个接口1.3每个对象都提供服务1.4被隐藏的具体实现1.5复用具体实现1.6继承1.6.1“是一个”(is-a)与“像是一个”(is-like-a)关系1.7伴随多... 查看详情

java编程思想重点

1.Java中的多态性理解(注意与C++区分)Java中除了static方法和final方法(private方法本质上属于final方法,因为不能被子类访问)之外,其它所有的方法都是动态绑定,这意味着通常情况下,我们不必判定是否应该进行动态绑定&mdash... 查看详情

java编程思想重点笔记(java开发必看)

Java编程思想重点笔记(Java开发必看) Java编程思想,Java学习必读经典,不管是初学者还是大牛都值得一读,这里总结书中的重点知识,这些知识不仅经常出现在各大知名公司的笔试面试过程中,而且在大型项目开发中也是... 查看详情

java编程思想-这一本书

面向对象特性理论(感觉java设计,是为了达到这些目的才把代码写成那样的~)1)万物皆为对象2)程序时对象的集合,他们通过发送消息来告知彼此所要做的3)每个对象都有自己的由其他对象所构成的存储4)每个对象都拥有其... 查看详情

《java编程思想》学习笔记——第十五章泛型

    在面相对象编程中,多态算是一种泛化机制。    泛型实现了参数化类型的概念。    泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确... 查看详情

java基础之java编程思想(1-5)

一、对象导论1:多态的可互换对象  面向对象程序设计语言使用了后期绑定的概念。  当向对象发送消息时,被调用的代码直到运行时才能确定。也叫动态绑定。2:单根继承结构  所有的类最终都继承自单一的基类,这... 查看详情

面向对象编程思想

...态:就是相同的属性和行为却有不同的表现方式面向接口编程接口是为了处理各个对象之间的协作关系,是系统设计的关键部分,主要作用是为了将“定义”与“实现”分离,从而实现系统解耦的目的。在系统设计之初,我们要... 查看详情