OOP的三个核心本质是什么?
这是道基础中见思想的面试题,面试官爱问这个问题。不过关于OOP三个核心众多书籍似乎莫衷一是。《Java编程思想》第7章谈到多态的开篇语便是:除了数据的抽象化(data abstraction)与继承(Inheritance)以外,面向对象编程语言的第三个核心本质便是多态(ploymorphism)。另有参考书把封装(wrap)、重载(overload)也归为OOP的特性。以上几点点确实都应算做OOP的特性,但是无论怎么组合我想多态是少不了的,如果语言不支持多态,就不能称之为面向对象的。有的语言只支持类而不支持多态,如Ada、VB,我们称它们为基于对象的语言。有人总结说:封装是优点,继承是基础,重载是特点,而多态是特征。
(1)什么是多态
多态是一种性质,应为多态性。最直白的理解就是:在程序运行时,根据类的对象来确定调用哪个函数。但是多态的意义不仅限于此,多态的意义更在于:子类型别向上转型为基类型别,从而通过一个公共函数来执行这些类中相同的方法。在C#中通过virtual和abstract来实现,在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级别以上的人,也会去面试新人。我想那时候我会以平常的心态去面对面试者,而不是卖弄自己的小聪明,给他人布下陷阱而显露出自己的高深。这也是我为什么要自己查资料翻阅书籍“剖析”“经典面试题”的原因,一方面是为了批判的继承这些面试题中的精华之处,将基础知识融会贯通。另一方面就是为了去批驳网上满天飞的面试题的不严谨的地方。
后记:对待一个问题我总希望能追寻到我能触及到的最深处,但是今天发现“昨天的认识”总是要比“今天的认识”肤浅,计算机技术博大精深,而我们绝大多数人却都只是在管中窥豹。今天想想,前几日的心态有点不太端正。我的总结应该是为了提高自己,而不是去征服别人。因为学无止境,今天总要比明天肤浅一些。