【Java编程思想】8.多态

在面向对象的程序设计语言中,多态是继数据抽象继承之后的第三种基本特征。

多态分离了“做什么”和“怎么做”,让接口和实现分离开,改善了代码的可读性和组织结构,创建了可拓展的程序。

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

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)。

原文地址:https://www.cnblogs.com/chentnt/p/9791888.html

时间: 2024-10-11 22:35:08

【Java编程思想】8.多态的相关文章

Java编程思想(五) —— 多态(上)

上一章,Java编程思想(四) -- 复用类里面讲到了向上转型,感觉和多态放在一起写更好. 多态,polymorphism.一个重要的特性,篇幅太长了,分上下两篇写. (1)向上转型 class TV{ public static void show(TV tv){ System.out.println("TV"); } } public class LeTV extends TV{ public static void main(String[] args) { LeTV letv

JAVA编程思想(4) - 多态(一)

多态 在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本类型. 多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来.多态不但能够改善代码的组织结构和可读性,还能够创建可扩展程序. 再论向上转型 代码 //: polymorphism/music/Note.java // Notes to play on musical instruments. package polymorphism.music; public enum Note { MIDDLE_C, C_SHAR

JAVA编程思想(4) - 多态(三)

若干个对象共享 例如Frog对象拥有其自己的对象,并且知道他们的存活多久,因为Frog对象知道何时调用dispose()去释放其对象.然而,如果这些成员对象中存在于其他一个或多个对象共享的情况,问题将不再简单,不再能简单的调用dispose()了.在这种情况下,我们也许需要引用计数来跟踪依旧访问着共享对象的数量. //: polymorphism/ReferenceCounting.java // Cleaning up shared member objects. import static

Java编程思想(五) —— 多态(下)

多态(上)基本讲解了很多多态的特性和问题.下面继续. 1)构造器和多态 这个问题其实前面写过了,构造器实际上是static方法,只不过是隐式声明,所以构造器并没有多态性. 但是需要知道加载的顺序. class GrandFather{ GrandFather(){ print(); } private int print(){ System.out.println("g"); return 1; } } class Father extends GrandFather{ Father(

Java编程思想(四) —— 复用类

看了老罗罗升阳的专访,情不自禁地佩服,很年轻,我之前以为和罗永浩一个级别的年龄,也是见过的不是初高中编程的一位大牛之一,专访之后,发现老罗也是一步一个脚印的人.别说什么难做,做不了,你根本就没去尝试,也没有去坚持. If you can't fly then run,if you can't run then walk, if you can't walk then crawl,but whatever you do,you have to keep moving forward--Martin

Java编程思想之8多态

这一章看下来,感觉比较混乱.乱感觉主要乱在8.4  8.5. 开始读的时候,感觉8.1 8.2 8.3都挺乱的.读了两遍后发现前三节还是比较有条理的. 8.1主要讲了什么是多态,多态的好处. 8.2主要讲了什么情况会发生多态?? 8.3主要讲了构造器内部里面的方法调用会发生多态. 8.4就一页,而且感觉一般用不到.用到了再看也行. 8.5也很简单,相当于一个总结,一个补充(向下转型) 我主要根据书上的内容,总结两个内容: 1.什么是多态,多态的好处: 2.什么情况下会发生多态?为什么这些情况下会

《Java编程思想(第4版)》pdf

下载地址:网盘下载 内容简介 编辑 本书赢得了全球程序员的广泛赞誉,即使是最晦涩的概念,在Bruce Eckel的文字亲和力和小而直接的编程示例面前也会化解于无形.从Java的基础语法到最高级特性(深入的面向对象概念.多线程.自动项目构建.单元测试和调试等),本书都能逐步指导你轻松掌握.[1] 从本书获得的各项大奖以及来自世界各地的读者评论中,不难看出这是一本经典之作.本书的作者拥有多年教学经验,对C.C++以及Java语言都有独到.深入的见解,以通俗易懂及小而直接的示例解释了一个个晦涩抽象的概

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

Java编程思想,Java学习必读经典,不管是初学者还是大牛都值得一读,这里总结书中的重点知识,这些知识不仅经常出现在各大知名公司的笔试面 试过程中,而且在大型项目开发中也是常用的知识,既有简单的概念理解题(比如is-a关系和has-a关系的区别),也有深入的涉及RTTI和JVM底层 反编译知识. 1. Java中的多态性理解(注意与C++区分) Java中除了static方法和final方法(private方法本质上属于final方法,因为不能被子类访问)之外,其它所有的方法都是动态绑定,这意

【java编程思想--学习笔记(四)】对象导论

写这篇博客的前言: 长话短说,我希望通过阅读<java编程思想>来使我的代码 简洁可用 . 目的的层次不同,首先具体的目标是,了解Java的特性和巩固Java的基础. 更抽象的目的如下: 1.期待以巩固基础的方式,使代码优美,简洁,高效. 2.使自己写的模块能够开放适度,好用. 3.形成一种对代码是否优美的审美观. 于是<Java编程思想>第一章 对象导论 由此开始. 1.1 抽象过程 java 相对于命令式语言的优势在于只针对于待解问题建模.后者所做的主要抽象要求所做问题基于计算

java编程思想-基础

interface: 方法默认为public:成员变量默认 static and final 对象数组的定义:理解? 多接口继承:可以多个接口,但只有一个具体类,具体类在前面 自:多接口继承时,来自不同接口的同名方法怎么处理呢? java重载不能依靠返回类型加以区分(C++可以),也不能依靠checked 异常类型区分 变量定义中的系列定义(逗号隔开):变量名 = 值,其它公共 自:类中,自己引用自己的理解(如,链表节点元素).静态看成动态,编译器的本质实现 内部类和普通类区别:内部类可priv