think in java 第七章 多形性 第一部分

think in java 第七章的学习 第一部分

上溯造型,深入理解,覆盖与过载

 

“多型性”(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来,亦即实现了“是什么”与“怎样做”两个模块的分离。利用多形性的概念,代码的组织以及可读性均能获得改善。此外,还能创建“易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地“成长”。

1.上溯造型

在第6章,大家已知道可将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用。取得一个对象句柄,并将其作为基础类型句柄使用的行为就叫作“上溯造型”——因为继承树的画法是基础类位于最上方。

我的理解:

上次提到上溯造型是在think in java 第六章 类再生,我认为上溯类型的意思就是:

子类对象同时也是父类对象。需要父类的场合,子类也可以代为实现。

(1)为什么要上溯造型

举了一个上溯造型的例子。

我的理解:

我不希望在类中写很多连续紧密的方法(耦合度很高),让类太过复杂。我希望方法不但能复用,而且易于拓展。

对比下面的写法:

这两种写法的实现效果是一样的,但是第二种写法的耦合性太强了,代码也更加复杂。

正如原文中说的:

第二种写法必须为每种新增的类编写与类紧密相关的方法。这意味着第一次就要求多得多的编程量。以后,假如想添加一个新方法或者一个新类型,仍然需要进行大量编码工作。此外,即使忘记对自己的某个方法进行过载设置,编译器也不会提示任何错误。这样一来,类型的整个操作过程就显得极难管理,有失控的危险。

但假如只写一个方法,将基础类作为自变量或参数使用,而不是使用那些特定的衍生类,岂不是会简单得多?也就是说,如果我们能不顾衍生类,只让自己的代码与基础类打交道,那么省下的工作量将是难以估计的。

这正是“多形性”大显身手的地方。然而,大多数程序员(特别是有程序化编程背景的)对于多形性的工作原理仍然显得有些生疏。

我很少这样使用上溯造型,可以在具体项目中考虑使用的方法。

2.深入理解

现在我们是这样使用上溯造型的:

那么问题就来了,看一下方法public static void my(xie xie){},这个方法只是接收一个xie的句柄而已。

(1)方法调用的绑定

在这种情况下,编译器怎么知道xie句柄指向的是一个god对象呢?为什么不指向godness?编译器怎么进行区分调用的是哪个对象?

答案是通过绑定。将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。

有两种绑定方式(让编译器可以区分参数的调用):

1.早期绑定

若在程序运行以前执行绑定(由编译器和链接程序,如果有的话),就叫作“早期绑定”。大家以前或许从未听说过这个术语,因为它在任何程序化语言里都是不可能的。C编译器只有一种方法调用,那就是“早期绑定”。

但是在这里前期绑定是起不到作用的。上述程序最令人迷惑不解的地方全与早期绑定有关,因为在只有一个xie句柄的前提下,编译器不知道具体该调用哪个方法。

2.后期绑定

解决的方法就是“后期绑定”,它意味着绑定在运行期间进行,以对象的类型为基础。后期绑定也叫作“动态绑定”或“运行期绑定”。

若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。

Java中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final。这意味着我们通常不必决定是否应进行后期绑定——它是自动发生的。

为什么要把一个方法声明成final呢?正如上一章指出的那样,它能防止其他人覆盖那个方法。但也许更重要的一点是,它可有效地“关闭”动态绑定,或者告诉编译器不需要进行动态绑定。这样一来,编译器就可为final方法调用生成效率更高的代码。

我的理解:

这里介绍了java中绑定的知识。绑定用于方法识别参数对象。java方法中都是使用后期绑定。为什么不使用前期绑定?如果使用前期绑定,因为在只有一个句柄的情况下,编译器不知道要去调用哪个方法。

若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。(编译器还是不知道对象的类型,参数对象是由方法调用机制自己去找的)

final不仅可以防止字段和方法被修改,也可以“关闭”编译器对此方法的动态绑定(也就是不使用各种方法调用机制),从而提高效率。(但是这里我有一点不明白,前面提到java中所有方法使用的都是后期绑定,但是final却可以关闭后期绑定,那么要通过什什么方式去识别参数呢?回头再看吧)

(2)产生正确的行为

知道Java里绑定的所有方法都通过后期绑定具有多形性以后,就可以相应地编写自己的代码,令其与基础类沟通。

此时,所有的衍生类都保证能用相同的代码正常地工作。或者换用另一种方法,我们可以“将一条消息发给一个对象,让对象自行判断要做什么事情。”

在面向对象的程序设计中,有一个经典的“形状”例子。由于它很容易用可视化的形式表现出来,所以经常都用它说明问题。

1

上溯造型可用下面这个语句简单地表现出来:Shape s = new Circle();

在这里,我们创建了Circle对象,并将结果句柄立即赋给一个Shape。这表面看起来似乎属于错误操作(将一种类型分配给另一个),但实际是完全可行的——因为按照继承关系,Circle属于Shape的一种。因此编译器认可上述语句,不会向我们提示一条出错消息。

当我们调用其中一个基础类方法时(已在衍生类里覆盖):s.draw();

同样地,大家也许认为会调用Shape的draw(),因为这毕竟是一个Shape句柄。那么编译器怎样才能知道该做其他任何事情呢?但此时实际调用的是Circle.draw(),因为后期绑定已经介入(多形性)。

为了在编译的时候发出正确的调用,编译器毋需获得任何特殊的情报。对draw()的所有调用都是通过动态绑定进行的。

我的理解:

我觉得最后一句话很重要:“为了在编译的时候发出正确的调用,编译器毋需获得任何特殊的情报。对draw()的所有调用都是通过动态绑定进行的。”

我认为,动态绑定和编译器是两个不同的机制。java方法中参数的调用是由动态绑定负责的,与编译器无关。动态绑定是一些列的方法调用机制,功能是在运行期间判断对象的类型,并分别调用适当的方法。(不知道有没有理解错,回头再看)

(3)扩展性

由于存在多形性,所以可根据自己的需要向系统里加入任意多的新类型,同时毋需更改my()方法。

在一个设计良好的OOP程序中,我们的大多数或者所有方法都会遵从my()的模型,而且只与基础类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基础类继承新的数据类型,从而新添一些功能。

如果是为了适应新类的要求,那么对基础类接口进行操纵的方法根本不需要改变。

我们对代码进行修改后,不会对程序中不应受到影响的部分造成影响。此外,我们认为多形性是一种至关重要的技术,它允许程序员“将发生改变的东西同没有发生改变的东西区分开”。

我的理解:

再用这个例子来举例:

因为有上溯造型机制,无论god和godness两个子类中如何修改,my()方法总是不受影响的。如果新加一个子类,也可以适用于my()方法。删除任何子类,也不会有影响。所以这样的设计非常稳定,易于拓展和修改。

3.覆盖与过载

我简化了一下书中的例子。我个人觉得这里算是个比较特别的例子,也许不需要太过在意。

在这里,虽然我传入selectMethod方法的参数是pjl对象,那么selectMethod中应该会执行pjl.write(1),但是因为write()方法的参数为int,所以在这里发生了方法过载,实际执行的却是xie.write(1)。

我的理解:

我觉得这个例子是要告诉我们一件事:从字面上看,应该执行pjl对象中的write(1)方法。但是因为继承实际上是在pjl这个对象中封装了xie对象,也会进行方法过载,根据参数的值去找适合的方法去执行。所以最后的执行的是过载的xie.write()方法。

其实我觉得,稍微注意一下,不要去弄一些这么相近的方法就好了…可以避免预期之外的结果。

4.总结

上溯造型是很重要的特性,后期绑定是jvm中很重要的实现。我需要深刻理解这两个重点的使用思想和实际使用方法。特别是后期绑定,应该深入学习其在jvm中的实现原理。

发表评论

电子邮件地址不会被公开。 必填项已用*标注