深刻剖析经典面试题之四:OOP的三个核心本质之多态

时间:2022-07-18 21:56:43

   OOP的三个核心本质是什么?

 

    这是道基础中见思想的面试题,面试官爱问这个问题。不过关于OOP三个核心众多书籍似乎莫衷一是。《Java编程思想》第7章谈到多态的开篇语便是:除了数据的抽象化(data abstraction)与继承(Inheritance)以外,面向对象编程语言的第三个核心本质便是多态(ploymorphism)。另有参考书把封装(wrap)、重载(overload)也归为OOP的特性。以上几点点确实都应算做OOP的特性,但是无论怎么组合我想多态是少不了的,如果语言不支持多态,就不能称之为面向对象的。有的语言只支持类而不支持多态,如AdaVB,我们称它们为基于对象的语言。有人总结说:封装是优点,继承是基础,重载是特点,而多态是特征。

 

1)什么是多态

 

多态是一种性质,应为多态性。最直白的理解就是:在程序运行时,根据类的对象来确定调用哪个函数。但是多态的意义不仅限于此,多态的意义更在于:子类型别向上转型为基类型别,从而通过一个公共函数来执行这些类中相同的方法。在C#中通过virtualabstract来实现,在Java中省掉了virtual。下面是引举的《Java编程思想》第7章中关于多态的一个例子:

 

//:Music4.java

 

//Abstract classes and methods

 

Import java.util.*;

 

 

abstract classInstrument4{

 

inti; //storage allocated for each

 

public abstract void play();

 

public String what(){

 

return  "Instrument4";

 

}

 

public abstract void adjust();

 

}

 

 

ClassWind4 extends Instrument4{

 

public void play(){

 

System.out.println("Wind4.play()");

 

}

 

public String what(){return"Wind4";}

 

public void adjust(){}

 

}

 

 

ClassPercussion4 extends Instrument4{

 

public void play(){

 

System.out.println("Percussion4.play()");

 

}

 

public String what(){return"Percussion4";}

 

public void adjust(){}

 

}

 

 

ClassStringed4 extends Instrument4{

 

public void play(){

 

System.out.println("Stringed4.play()");

 

}

 

public String what(){return"Stringed4";}

 

public void adjust(){}

 

}

 

 

ClassBrass4 extendsWind4{

 

public void play(){

 

System.out.println("Brass4.play()");

 

}

 

public void adjust(){

 

System.out.println("Brass4.adjust()");

 

}

 

}

 

 

class Woodwind4 extendsWind4{

 

public void play(){

 

System.out.println("Woodwind4.play()");

 

}

 

public String what(){return"Woodwind4";}

 

}

 

 

public class Music4{

 

//Doesn't care about type,so new types

 

//added to the system still work right:

 

Static void tune(Instrument4i){

 

//...

 

i.play();

 

}

 

static void tuneAll(Instrument4[]e){

 

for(int i=0;itune(e[i]);

 

}

 

public static void main(String[] args){

 

Instrument4[] orchestra = newInstrument4[5];

 

int i = 0;

 

//Upcasting  during addition to the array:

 

orchestra[i++]=new Wind4();

 

orchestra[i++]=new Percussion4();

 

orchestra[i++]=new Stringed4();

 

orchestra[i++]=new Brass4();

 

orchestra[i++]=new Woodwind4();

 

tuneAll(orchestra);

 

}

 

}///:~

 

2)多态的重要性

 

为什么要说“语言不支持多态,就不能称之为面向对象的”。

 

传统的多态实际上就是由虚函数(Virtual Function)利用虚表(Virtual Table)实现的,自然是离不开继承,换句话说多态实际上覆盖了继承。正是由于继承与多态的紧密联系,使得我们很容易张冠李戴,那么如何区别呢?

 

举个常用的例子:

 

Abstract Class Sharp implement IHaveSide {

 

public bool isSharp(){

 

return true;

 

}

 

public abstract int getSides();

 

}

 

 

Class Triangle extends Sharp {

 

public override int getSides() {

 

return 3;

 

}

 

}

 

 

Class Rectangle extends Sharp {

 

pubilc override int getSides() {

 

return 4;

 

}

 

}

 

 

那么这种类的关系叫做继承,下面这种使用方式也是继承所带来的:

 

Triangel tri = new Triangle();

 

println("Triangle is a type of sharp? " + tri.isSharp());

 

 

而这种方式则是多态:

 

Sharp sharp = new Rectangle();

 

println("My sharp has " + sharp.getSides() + " sides.");

 

这两者区别在哪?很显然,继承是子类使用父类的方法,而多态则是父类使用子类的方法。其技术上的区别是绑定时期,后期绑定一定是多态(后面将要谈到什么是绑定)。

 

现代软件大量的使用框架、模式(非特指Deisgn Pattern),也就是将软件开发的一些共性进行抽象,提出普遍适用的软件结构。无论是框架还是模式,他们都有一些明显的共同点 — 使用xml配置对象,大量使用接口采用所谓面向接口的方法。

 

3)多态的种类

 

 1、 基类继承多态(Base Class Polymorphism)

 

2、 接口继承多态(Interface Polymorphism

 

    多态并不仅仅局限于传统的虚函数,接口的使用也体现出多态的应用。具体的区分要从设计模式的论域(problem space)的角度来进行区分。我在博客的《从设计模式看抽象类与接口的区别》和《深刻剖析经典面试题之三:关于虚函数》中都有具体的提到。

 

4)多态的运行机制

 

所谓“绑定(binding)”,就是建立method call(函数调用)和method body(函数本体)的关联。如果绑定动作发生于程序执行前(由编译器和连接器完成),称为“先期绑定”。对于面向过程的语言它们没有其他选择,一定是先期绑定。比如C编译器只有一种method call,就是先期绑定。(C++有先期联编和后期联编)

 

当有多态的情况时,解决方法便是所谓的后期绑定(late binding):绑定动作将在执行期才根据对象型别而进行。后期绑定也被称为执行期绑定(run-time binding)或动态绑定(dynamic binding)。程序语言若想实现后期绑定,必须具备“得以在执行期判定对象型别”并“调用其相应之函数”的机制。也就是说,编译器仍然不知道对象的型别,但“method call”机制会找出正确的method body并加以调用。程序语言的后期绑定机制做法因人而异,但是你可以想象,必须有某种“型别信息”被置于对象内。

 

Java的所有函数,除了被声明为final者,都使用后期绑定。从表面上来理解,将函数声明为final型这样是为了防止他人覆写该函数。但是或许更重要的是,这么做可以“关闭”动态绑定。或者说,这么做便是告诉编译器:动态绑定是不需要的。于是编译器可以产生效率较佳的程序代码。这样说来,动态绑定(后期绑定)的效率不如先期绑定了。

 

这让我想起了我被面试官问的最惨的一次------他问我多态的运行机制,也就是说编译器是怎么识别不同的对象调用了不同类里的函数的。我以前并没仔细想过这个问题。然后面试官进一步暗示我说:“为什么我们有时候要把函数声明为final型的?”我答曰:“不想让别人覆写该函数。”面试官然后又不依不饶的追问道:“没有其他的原因了吗?”今天想来,怕是面试官追问的是:如果我们将函数声明为final后,就“关闭”了动态绑定,或许能得到较好的执行效率。但是《Java编程思想》又说了:“这样做并不会为你的程序带来整体效能的提升。所以最好是基于设计上的考量来决定是否使用final,而不要企图藉由它来改善性能。”真是学无止境啊,不将《Java编程思想》烂熟于心,只是找两本Java的教辅来看看的人怎么能应付面试官如此诡异的问题。我想不出意外,有天我也会成为PM级别以上的人,也会去面试新人。我想那时候我会以平常的心态去面对面试者,而不是卖弄自己的小聪明,给他人布下陷阱而显露出自己的高深。这也是我为什么要自己查资料翻阅书籍“剖析”“经典面试题”的原因,一方面是为了批判的继承这些面试题中的精华之处,将基础知识融会贯通。另一方面就是为了去批驳网上满天飞的面试题的不严谨的地方。

 

 

后记:对待一个问题我总希望能追寻到我能触及到的最深处,但是今天发现“昨天的认识”总是要比“今天的认识”肤浅,计算机技术博大精深,而我们绝大多数人却都只是在管中窥豹。今天想想,前几日的心态有点不太端正。我的总结应该是为了提高自己,而不是去征服别人。因为学无止境,今天总要比明天肤浅一些。