目录
- 面向对象编程
- 两种范式
- 抽象
- OOP 三原则
- 封装
- 继承
- 多态
- 多态、封装与继承协同工作
面向对象编程
面向对象编程(Object-Oriented Programming,OOP)在Java中核心地位。几乎所有的Java程序至少在某种程度上都是面向对象的。OOP与java是密不可分的。下面说一下OOP的理论部分。
两种范式
所有计算机程序都包含两种元素:代码和数据。而且从概念上讲,程序可以围绕代码或数据进行组织。也就是说,某些程序是围绕“正在发生什么”进行编写的,其他一些程序则是围绕“将影响谁”进行编写的。这是控制程序如何构造的两种范式。第一种方式被称为面向过程模型(process-oriented model)。这种方式将程序描述为一系列线性步骤(即代码)。面向过程模型可以被认为是代码作用于数据。例如,C 这类过程化语言采用这种模型是相当成功的。但是,正如在第1章提到的,随着程序规模和复杂性的不断增长,这种方式带来的问题会逐渐显现出来。
为了管理日益增长的复杂性,发明了第二种方式,称为面向对象编程(object-oriented programming)。面向对象编程围绕数据(即对象)以及一套为数据精心定义的接口组织程序**。面向对象编程的特点是数据控制对代码的访问。正如将会看到的,通过将数据作为控制实体,可以得到组织结构方面的诸多好处。
抽象
面向对象编程的本质元素之一是抽象(abstraction)。人们通过抽象管理复杂性。例如,人们不会将一辆汽车想象成一系列相互独立的部分,而是将它想象成一个定义良好的具有自己独特行为的对象。通过这种抽象人们可以驾驶汽车到杂货店,而不会因为汽车零部件的复杂性而不知所措。人们可以忽略引擎、传动以及刹车系统的工作细节。相反,可以自由地作为一个整体使用这个对象。
使用层次化分类是管理抽象的一种强有力方式。这种方式允许对复杂系统的语义进行分层,将它们分解为多个更易于管理的部分。从外部看,汽车是单个对象。而从内部看,汽车是由几个子系统构成的:驾驶系统、制动系统、音响系统、安全带、加热系统、移动电话,等等。如果继续细分,每个子系统是由更多特定的单元组成的。例如,音响系统是由收音机、CD 播放器和/或磁带或 MP3 播放器组成的。关键的一点是,通过层次化抽象管理汽车(或所有其他复杂系统)的复杂性。
复杂系统的层次化抽象也可以应用于计算机程序。来自传统面向过程程序的数据,通过抽象可以转换成程序的组件对象。程序中的一系列处理步骤,何以变成这些对象之间的消息集合。因此,每个对象描述了它自己的独特行为。可以将这些对象当作响应消息的具体实体,消息告诉对象做什么事情。这就是面向对象编程的本质。
面向对象的概念形成了 Java的核心,就如同它们形成了人类理解事物的基础一样。理解这些概念是如何被迁移到程序中的,这一点很重要。将会看到,对于创建在项目生命周期过程中不可避免地要发生变化的程序来说,面向对象编程是一种强大且自然的范式,所有比较大的软件项目都要经历如下生命周期:概念提出、成长和衰老。例如,如果具有定义良好的对象,并且这些对象的接口清晰可靠,那么就可以优美地废除或替换旧系统的某些部分,而不用担心发生问题。
OOP 三原则
所有面向对象编程语言都提供了用于帮助实现面向对象模型的机制,这些机制是封装(encapsulation)、继承(inheritance)和多态(polymorphism)。现在让我们看一看这些概念。
封装
封装是将代码及其操作的数据绑定到一起的机制,并且保证代码和数据既不会受到外部干扰,也不会被误用。理解封装的一种方法是将它想象成一个保护性的包装盒,可以阻止在盒子外部定义的代码随意访问内部的代码和数据。对盒子内代码和数据的访问是通过精心定义的接口严格控制的。为了将封装联系到现实世界,考虑汽车上的自动传动装置,其中封装了引擎的数百位信息,例如当前的加速度、路面的坡度以及目前的挡位等。作为用户,您只有一个方法可以影响这个复杂的封装:换挡。例如,不能通过使用转弯信号或雨刷器来影响传动。因此,挡位是定义良好的(实际上也是唯一的)传动系统接口。此外,在传动系统内部发生的操作不会影响到外部对象。例如,挡位不会开启前灯!因为自动传动装置被封装了起来,所以任何一家汽车制造商都可以选择他们喜欢的方式实现它。但是,从驾驶员的角度看,它们的作用是相同的。相同的思想可以应用于编程。封装代码的优点是每个人都知道如何访问,因此可以随意访问而不必考虑实现细节,也不必担心会带来意外的负面影响。
在Java中,封装的基础是类。类(class)定义了一组对象共享的结构和行为(数据和代码)。给定类的每个对象都包含该类定义的结构和行为,就好像它们是从同一个类的模子中铸造出来的一样。由于这个原因,有时将对象称作类的实例(instance ofclass)。因此,类是一种逻辑结构,而对象是物理实体。
当创建类时,需要指定构成类的代码和数据。笼统地讲,这些元素称为类的成员(member)。特别地,类定义的数据被称为成员变量(member variable)或实例变量(instance variable)。操作数据的代码称为成员方法(member method),或简称为方法(Java 程序员所说的方法,实际上就是CIC+程序员所说的函数,如果您熟悉 CIC++的话,了解这一点是有帮助的)。在正确编写的Java 程序中,方法定义了使用成员变量的方式。这意味着类的行为和接口是由操作实例数据的方法定义的。
既然类的目的是封装复杂性,那么在类的内部就存在隐藏实现复杂性的机制。类中的每个方法或变量可以被标识为私有的或公有的。类的公有(public)接口表示类的外部用户需要知道或可以知道的所有内容。私有(private)方法和数据只能由类的成员代码访问,所有不是类成员的其他代码都不能访问私有的方法或变量。因为只能通过类的公有方法访问类的私有成员,所以可以确保不会发生不正确的行为。当然,这意味着必须仔细地设计公有接口,不要过多地暴露类的内部工作情况。
继承
继承是一个对象获得另一个对象的属性的过程。继承很重要,因为它支持层次化分类的概念。在前面提到过,通过层次化分类(即从上向下),大多数知识都将可管理。例如,金毛猎犬是狗类的一部分,狗又是哺乳动物类的一部分,哺乳动物又是更大的动物类的一部分。如果不使用层次化分类,每个对象都将需要显式定义自身的所有特征。而通过使用继承,对象只需要定义自己在所属类中独有的那些属性,可以从父类继承通用的属性。因此,继承机制使得对象成为更一般情况的特殊实例成为可能。下面进一步分析这个过程。
大多数人很自然地将世界看作由各种以层次化方式相互关联的对象(例如动物、哺乳动物和狗)构成的。如果希望以抽象的方式描述动物,那么会说它们具有某些属性,例如体型、智力、骨骼系统的类型等。动物还具有特定的行为,它们需要进食、呼吸以及睡觉。对属性和行为的这一描述就是动物类的定义。
如果希望描述更具体的动物类,例如哺乳动物,那么它们会有更特殊的属性,比如牙齿类型、乳腺类型等。这就是所谓的动物类的子类(subclass),而动物类被称作哺乳动物类的超类(superclass)。
既然哺乳动物只不过是定义更加具体的动物,它们当然可以从动物类继承所有属性。深度继承的子类会继承整个类层次(class hierarchy)中每个祖先的所有属性。
继承还与封装相互作用。如果一个给定的类封装了某些属性,那么它的任何子类除了具有这些属性之外,还会添加自己特有的属性(见图2-2)。这是一个关键概念,它使面向对象程序的复杂性呈线性增长而非几何性增长。新的子类继承所有祖先的所有属性,它不会与系统中的大部分其他代码进行不可预料的交互。
多态
多态(来自希腊语,表示“多种形态”)是允许将一个接口用于一类通用动作的特性。具体使用哪个动作与应用场合有关。考虑堆栈(一种后进先出的数据结构),可能有一个程序需要三种类型的堆栈,一种用于整数值,另一种用于浮点值,第三种用于字符。尽管存储的数据不同,但是实现每种堆栈的算法是相同的。如果使用非面向对象的语言,需要创建三套不同的堆栈例程,每套例程使用不同的名称。但是,由于支持多态,因此使用 Java 可以指定一套通用的堆栈例程,所有这些例程共享相同的名称。
更一般的情况是,多态的概念经常被表达为“一个接口,多种方法”。这意味着可以为一组相关的动作设计一个通用接口。多态允许使用相同的接口指定通用类动作(general class of action),从而有助于降低复杂性。选择应用于每种情形的特定动作(即方法)是编译器的任务,程序员不需要手动进行选择,只需要记住并使用通用接口即可。
再次以狗作为例子,狗的嗅觉是多态的“如果狗闻到猫的气味,就会吠叫并且追着猫跑;如果狗闻到了食物的气味,就会分泌睡液并跑向盛着食物的碗。在这两种情况下,是相同的嗅觉在工作,区别是闻到的气味,也就是作用于狗鼻子的数据的类型!当将多态应用于 Java 程序中的方法时,也可以采用相同的通用概念来实现。
多态、封装与继承协同工作
如果应用得当,由多态、封装和继承联合组成的编程环境与面向过程模型环境相比,能支持开发更健壮、扩展性更好的程序。精心设计的类层次结构是重用代码的基础,在这个过程中,需要投入时间和精力进行开发和测试。通过封装可以随着时间迁移您的实现,而不会破坏那些依赖于类的公有接口的代码。通过多态可以创建出清晰、易懂、可读和灵活的代码。
对于前面两个真实的例子,汽车更全面地演示了面向对象设计的功能。狗的例子对于思考继承则很有趣,但是汽车更像程序。依靠继承,所有驾驶员能够驾驶不同类型的车辆(子类)。不管是校车、奔驰、保时捷,还是家用货车,驾驶员大体上都能找到并操作方向盘、制动闸和油门踏板。经过一段时间的腾合,大部分人甚至能够知道手动挡与自动挡之间的差别,因为他们从根本上理解了手动挡与自动挡的共同超类——传动。
人们总是与已经封装好的汽车特性进行交互。刹车和油门踏板隐藏了不可思议的复杂性,但是接口却非常简单,使用脚就可以操作它们。而引擎的实现、制动踏板的样式以及轮胎的大小,对于如何与踏板的类定义进行交互则没有影响。
汽车制造商为基本相同的车辆提供多种选项的能力,清晰地反映了最后一个特性——多态。例如,刹车系统有正锁和反锁之分,方向盘有带助力和不带助力之分,引擎有 4 缸、6缸或8缸之分。不管采用哪种方式,仍然是通过踩下刹车踏板停车、转动方向盘改变方向、踩下油门踏板开动车辆。相同的接口可以用于控制大量不同的实现。
可以看出,正是通过应用封装、继承和多态,将各个独立的部分变换成了所谓的汽车对象。对于计算机程序也一样。通过应用面向对象原则,可以将复杂系统的各个部分组合到一起,形成健壮、可维护的整体。
在本节开头提到过,每个 Java 程序都是面向对象的。或者更准确地说,每个 Java 程序都涉及封装、继承和多态。将会看到,Java 提供的大部分特性都是内置类库的一部分,这些类库大量使用了封装、继承和多态。