2.3 面向对象的程序设计
如上所述,Hadoop的代码是用Java语言编写的,而Java是一种面向对象的程序设计语言,Hadoop的设计和实现也确实是面向对象的。那么,所谓面向对象,究竟是什么意思?这个概念是怎么来的呢?这个问题,许多天天在用Java编程的人也未必清楚,但是对于我们分析和理解Hadoop的代码却是有意义的,因而值得用一些篇幅做点介绍。
最早的程序是用汇编语言甚至机器语言编写的,程序员可以使用CPU指令系统中的任何指令,那样编程效率既很低,又不安全,程序的可读性就更不用说了。这就迫使人们发展出高级语言,以提高编程效率。但是那时候的程序还都是所谓“spaghetti code”,或者说是“炒面式代码”,乱成一团。
后来荷兰科学家Dijkstra发表了著名的论文“Go To Statement Considered Harmful”,主张在高级语言中去除goto语句。结合着“去goto”,人们又提出“结构化程序设计”的概念和方法,论证只要有if-then-else和while这些程序结构,就完全可以不要goto。
事实上此后的许多高级语言中确实是不提供goto语句的。但是请注意,这只是不让在采用高级语言的应用程序中使用goto语句,而并不限制在采用例如C语言和汇编语言的系统程序中以及经编译产生的可执行代码中使用。其实这就是把使用goto语句或指令的人群范围缩小了,只让比较专业的人员使用,而限制公众使用。这样的情况也发生在别的场合。例如,大家知道Java语言不允许使用指针,但是就有人(当年作者就是其中之一)感到困惑了:不使用指针,那好多功能就无法实现啊?再说,Java中的所谓Reference与指针究竟有什么区别呢?其实Java只是不让Java程序员使用指针,它自己还是用的,Java程序经编译后的代码中当然也用指针,要不然它那些功能怎么实现?它只是不让你们Java程序员用指针而已,这是为你们好。至于Reference,也确实就是指针,只是赋值时它要检查是否真的指向一个所述类型的对象。应该承认,这确实比C语言安全多了,从而程序员的工作效率也可提高,并且还降低了程序员入门的门槛。
与此相平行,Pascal语言之父Nicklaus Wirth提出了“算法+数据结构=程序(Algorithms+Data Structures=Programs)”的命题,提醒人们不要光把眼光盯在算法上,数据结构同样重要。事实上,程序中的许多bug都来自对数据结构的不当访问。这就好比,对于一个设备,哪怕是很简单的物件,要是谁都可以来自助使用,七手八脚,就难免出错。比较好的办法是凡要使用就必须按规定的方法使用,不能让人自作主张。更好的办法是有专人负责,你要怎么样就对他说,由他来帮你操作。
在这样的背景下,Hoare和Brinch Hansen等人提出和发展了“模块化程序设计”、“结构化程序设计”的概念和方法,提出了对象即Obj ect的概念,说Obj ect就是“数据结构,和定义于这个数据结构上的全部操作”。而“类(class)”,则是对具有相同数据结构和相同操作方法集合的那些Obj ect的抽象。
所以,当我们说创建了一个某类的对象时,我们说的是:分配存储空间用作这个类所定义的数据结构,并带有这个类所定义的对于这个数据结构的全部操作(有些操作也可以是超出这个范围的)。这样,我们就把数据“封装”在具体的对象中了。但是这又要靠语言来保证,所谓靠语言保证,其实是靠这个语言的编译器(或解释器)保证。例如,在编译的过程中,如果发现某个类的程序中要自行直接访问另一个类的某个数据成分,而那个数据成分的性质在程序中说明为private,就马上报出错并拒绝继续编译,这样就使该项数据的封装得到了保证。读者也许会问,要是我绕过编译,直接用Java的机器码写个程序来访问这个数据成分呢?答案是:如果你有这个本事,所在的公司或团队又允许你这样做,那当然是挡不住的,但是又有几个人会这样做呢?
不过光是做到了数据封装还不足以让一个语言成为“面向对象”,人们普遍认为需要满足三个要求才算是面向对象,还有两个是“多态(polymorphism)”和“继承(inheritance)”。如果不同时满足这三项要求,就只能说是“基于对象的(Obj ect Based)”,而不能说是“面向对象的(Obj ect Oriented)”。
不过也有的类中并未定义数据成分,而只是定义了一些方法,这样的类实际上就是一个小小的库函数(lib)模块。反过来,如果只定义了数据成分而并未定义任何方法,那就只是一个纯粹的数据结构,但是这样的情况几乎没有。
所谓“多态”,有几方面的意思。首先是同一个函数名可以用于多个不同的函数,只要参数的类型或个数不同,比方说,同样是在BlockManager这个类里面,就定义了两个都叫completeBlock的函数:
completeBlock(final BlockCollection bc, final int blkIndex, boolean force) completeBlock(final BlockCollection bc, final BlockInfo block, boolean force)
二者的区别仅在于第二个参数的类型,一个是int,另一个是BlockInfo。之所以可以如此,是因为Java的编译器会自动把函数的参数表,即所有各个参数的类型,加以排列编码作为后缀拼接在函数名后面。这样,程序员所看到的函数名与编译器所使用的函数名其实是不一样的。这对于程序员确实是提供了方便,因为这二者的功能是一样的,符合思考的习惯。用C语言编程的时候,像这样的情况就得用不同的函数名,例如completeBlock_Index()和completeBlock_Info(),那就要麻烦一些,所以这确实是一项改进。可是这对于程序的阅读分析却未必是好事,因为阅读分析者看到某处调用了completeBlock(),却不知道究竟是其中的哪一个,这时候就得仔细去看调用时的实参分别是什么类型,再去与两个函数的参数表比对,才能知道究竟是调用了哪一个。所以,在阅读分析面向对象的程序代码时,很容易在这样的情况下误入歧途,需要特别注意。
“多态”的另一种意思是,一个类可以有多个不同的“子类(Subclass)”,在程序中可以用“父类”来泛指不同的子类(也许说“大类”、“小类”更符合我们的习惯)。比方说,Hadoop的代码中定义了一个抽象类InputFormat。所谓抽象类,是指这个类声称要提供的函数有些是尚未实现的、是悬空的,有待于它的子类加以落实,因为预知不同的子类应该会有不同的实现;而InputFormat的子类,即扩充了InputFormat的类,事实上就有十多个。然后,程序中可以对InputFormat类进行某些操作,而无须明说这是对于哪个具体的子类。其实这样的“动态绑定”在C程序中也有使用,例如Linux内核中的file_operations数据结构就是用于这种目的,但是在C程序中一般用得不多,而在Hadoop这样的大型Java程序中就比比皆是了。和上述函数名的多态一样,我们在阅读分析Java程序时也很容易因此而误入歧途。
要成为面向对象的程序设计语言,还须满足“继承”的要求。这是说,如果一个类声称扩充(extends)了另一个类,那么后者就是它的父类,于是它就自动继承了父类的所有成分和方法,父类中有什么它就有什么,然后它还可以再补充定义一些成分和方法。这样,当你看着一个类的代码时,一定要意识到这也许不是它的全部,它可能还从父类那里继承了不少内容。另外,当你看到程序中某处调用了这个类的某个方法,可是它的定义中没有这么一个方法的时候,一定要记得去它的父类甚至祖类那里去寻找。
所以,作者的体验是,比之C程序,我们阅读分析Java程序的时候要多长个心眼。