”咱们之因此将天然界分解,组织成各类概念,并按其含义分类,主要是由于咱们是整个口语交流社会共同遵照的协定的参与者,这个协定以语言的形式固定下来······除非同意这个协定中规定的有关语言信息的组织和分类,不然咱们根本没法交谈。“java
本章将向读者介绍包括开发方法概述在内的OOP的基本概念。程序员
1.1 抽象过程算法
全部编程语言都提供抽象机制。能够认为,人们所可以解决的问题的复杂性直接取决于抽象的类型和质量。编程
所谓的”类型“是指”所要抽象的是什么?“数组
汇编语言是对底层机器的轻微抽象。接着出现的许多所谓”命令式“语言(如FORTRAN、BASIC、C等)都是对汇编语言的抽象。这些语言在汇编语言基础上有了大幅的改进,可是它们所作的主要抽象仍要求在解决问题时要基于计算机的结构,而不是基于所要解决的问题的结构来考虑。程序员必须创建起在机器模型(位于”解空间“内,这是你对问题建模的地方,例如计算机)和实际待解问题的模型(位于”问题空间“内,这是问题存在的地方,例如一项业务)之间的关联。创建这种映射是费力的,并且这不属于编程语言固有的功能,这使得程序难以编写,而且维护代价高昂,同事也产生了做为副产物的整个”编程方法“行业。安全
另外一种对机器建模的方式就是只针对待解问题建模。早期的编程语言,如LISP和APL,都是 选择考虑世界的某些特定视图(分别对应于“全部问题最终都是列表”或者"全部问题都是算法形式的”)。PROLOG则将全部问题都转化成决策链。此外还产生了基于约束条件编程的语言和专门经过对图形符号操做来实现编程的语言(后者被证实限制性太强)。编程语言
面向对象方式经过向程序员提供表示问题空间中的元素的工具而更进了一步。这种表示方法很是通用,使得程序员不会受限于任何特定类型的问题。咱们将问题空间中的元素及其在解空间中表示称为”对象“。(你还须要一些没法类比为问题空间元素的对象。)这种思想的实质是:程序能够经过添加新类型的对象使自身使用于某个特定问题。所以,当你在阅读描述解决方案的代码的同时,也是在阅读问题的表述。相比之前咱们所使用的语言,这是一种更灵活和更强有力的语言抽象。因此,OOP容许根据问题来描述问题,而不是根据运行解决方案的计算机来描述问题。可是它仍然与计算机有联系:每一个对象看起来都有点像一台微型计算机------它具备状态,还具备操做,用户能够要求对象执行这些操做。若是要对现实世界中的对象做类比,那么说它们都具备特性和行为彷佛不错。函数
(我的理解)C语言就是编写一堆无组织的操做(方法或者说函数);而java的改变就是用对象来组织C中的那些无序的方法。工具
纯粹面向对象程序设计方式:布局
1)万物皆为对象。
将对象视为奇特的变量,它能够存储数据,除此以外,你还能够要求它在自身上执行操做。理论上讲你能够抽取待求解问题的任何概念化构件(狗、建筑物、服务等),将其表示为程序中的对象。
2)程序视对象的集合,它们经过发送消息来告知彼此所要作的。要想请求一个对象,就必须对该对象发送一条消息。更具体地说,能够把消息想象为对某个特定对象的方法的调用请求。
3)每一个对象都有本身的由其余对象所构成的存储。换句话说,能够经过建立包含现有对象的包的方式来建立新类型的对象。所以,能够在程序中构建复杂的体系,同时将其复杂性隐藏在对象的简单性背后。
4)每一个对象都拥有其类型。按照通用的说法,”每一个对象都是某个类(class)的一个实例(instance)“,整个”类“就是”类型“的同义词。每一个类最重要的区别于其余类的特性就是”能够发送什么样的消息给它“。
5)某一特定类型的全部对象均可以接收一样的消息。这是一句意味深长的表达,你在稍后便会看到。由于”圆形“类型的对象同时也是”几何形“类型的对象,因此一个”圆形“对象一定可以接受发送给”几何形“对象的信息。这意味着能够编写与”几何形“交互而且自动处理全部与几何形性质相关的事物的代码。这种可替代性(substitutability)是OOP中最强有力的概念之一。
Booch对对象提出了一个更加简洁的描述:
对象具备状态、行为和标识。这意味着每个对象均可以拥有内部数据(它们给出了该对象的状态)和方法(它们产生行为),而且每个对象均可以拥有内部数据(它们给出了该对象的状态)和方法(它们产生行为),而且每个对象均可以惟一地与其余对象区分开来,具体说来,就是每个对象在内存中都有一个惟一的地址。
1.2 每一个对象都有一个接口
亚里士多德大概是第一位深刻研究类型(type)的哲学家,他曾经提出过鱼类和鸟类这样的概念。全部的对象都是惟一的,但同时也是具备相同的特性和行为的对象所归属的类的一部分。
类描述了具备相同特性(数据元素)和行为(功能)的对象集合,因此一个类实际上就是一个数据类型,例如全部浮点型数字具备相同的特性和行为集合。二者的差别在于,程序员经过定义类型适应问题,而再也不被迫只能使用现有的用来表示机器中的存储单元的数据类型。
面向对象程序设计的挑战之一,就是问题空间的元素和解空间的对象之间建立一对一的映射(意思就是类型定义要准确 )
可是,怎样才成得到有用的对象呢?必须有某种方式产生对对象的请求,使对象完成各类任务。每一个对象都只能知足某些请求,这些请求由对象的接口(interface)所定义,决定接口的即是类型。
接口肯定了对某一特定对象所能发出的请求。可是,在程序中必须有知足这些请求的代码。这些代码与隐藏的数据一块儿构成了实现。从过程型编程的观点来看,这并不太复杂。在类型中,每个可能的请求都有一个方法与之相关联,当向对象发送请求时,与之相关联的方法就会被调用。此过程一般被归纳为:向某个对象”发送消息“(产生请求),这个对象便知道此消息的目的,而后执行对应的程序代码。
1.3每一个对象都提供服务(实现类型抽象的思路)
当正在试图开发或理解一个程序设计时,最好的方法之一就是将对象想象为”服务提供者“。程序自己将向用户提供服务,它将经过调用其余对象提供服务来实现这一目的。你的目的就是去建立(或者最好是在现有代码库中寻找)可以提供理想的服务来解决问题的一系列对象。
着手从事这件事的一种方式就是问一下本身:”若是我能够将问题从表象中抽取出来,那么什么样的对象能够立刻解决个人问题呢?“例如,假设你正在建立一个簿记系统,那么能够想象,系统应该具备某些包括了预约义的簿记输入屏幕的对象,一个执行簿记计算的对象集合,以及一个处理在不一样的打印机上打印和开
的对象。也许上述对象中的某些已经存在了,可是对于那些并不存在的对象,它们看起来像什么样子?它们可以提供哪些服务?它们须要哪些对象才能履行它们的义务?若是持续这样作,那么最终你会说”那个对象看起来如此简单,能够坐下来写代码了“,或者会说”我确定那个对象已经存在了“。这是将问题分解为对象集合的一种合理方式。
将对象看做是服务提供者还有一个附带的好处:它有助于提升对象的内聚性。高内聚是软件设计的基本质量要求之一:这意味着一个软件构件(例如一个对象,固然它也有多是指一个方法或者一个对象库)的各个方面”组合“的很好。人们在设计对象时所面临的一个问题是,将过多了解全部的格式和打印技术。你可能会发现,这些功能对于一个对象来讲太多了,你须要的是三个甚至更多个对象,其中,一个对象能够是全部可能的排版的目录,它能够被用来查询有关如何打印一张
的信息;另外一个对象(或对象集合)能够是一个通用的打印接口,它知道有关全部不一样类型的打印机的信息(可是不包含任何有关簿记的内容,它更应该是一个须要购买而不是本身编写的对象);第三个对象经过调用另外两个对象的服务来完成打印任务。这样,每一个对象都有一个它所能提供的内聚的集合。在良好的面向对象设计中,每一个对象均可以很好地完成一项任务,可是它并不试图作更多的事。就像在这里看到的,不只容许经过购买得到某些对象(打印机接口对象),并且还能够建立可以在别处复用的新对象(
排版目录对象)。
将对象做为服务提供者看待是一件伟大的简化工具,这不只在设计过程当中很是有用,并且当其余人试图理解你的代码或重用某个对象时,若是它们看出了整个对象所能提供的服务的价值,它会使调整对象以适应其设计的过程变得简单得多。
1.4被隐藏的具体实现
将程序开发人员按照角色分为类建立者(那些建立新数据类型的程序员)和客户端程序员(那些在其应用中使用数据类型的类消费者)是大有裨益的。客户端程序员的目标是收集各类用来实现快速应用开发的类。类建立者的目标是构建类,这种类只向客户端程序员暴露必需的部分,而隐藏其余部分。为何要这样呢?由于若是加以隐藏,那么客户端程序员将不可以访问它,这意味着类建立者能够任意修改被隐藏的部分,而不用担忧对其余任何人形成影响。被隐藏的部分一般表明对象内部脆弱的部分,它们很容易被粗心的或者不知内情的客户端程序员所毁坏,所以将实现隐藏起来能够减小程序bug。
在任何相互关系中,具备关系所涉及的各方都遵照的边界是十分重要的事情。当建立一个类库时,就创建了与客户端程序员之间的关系,他们一样也是程序员,可是他们是使用你的类库来构建应用、或者构建更大的类库的程序员。若是全部的类成员对任何人都是可用的,那么客户端程序员就能够对类作任何事情,而不受任何约束。即便你但愿客户端程序员不要直接操做你的类中的某些成员,可是若是没有任何访问控制,将没法阻止此事发生。全部东西都将赤裸裸地暴露于世人面前。
所以,访问控制的第一个存在缘由就是让客户端程序员没法触及他们不该该触及的部分-------这些部分对数据类型的内部操做来讲是必需的,但并非用户解决待定问题所需的接口的一部分。这对客户端程序员来讲实际上是一项服务,所以他们能够很容易地看出哪些东西对他们来讲很重要,而哪些东西能够忽略。
访问控制的第二个存在缘由就是容许库设计者能够改变类内部的工做方式而不用担忧会影响到客户端程序员。例如,你可能为了减轻开发任务而以某种简单的方式实现了某个特定类,但稍后发现你必须改写它才能使其运行得更快。若是接口和实现能够清晰地分离并得以保护,那么你就能够垂手可得地完成这项工做。
java用三个关键字在类的内部设定边界:public、private、protected。这些访问指定词(access specifier)决定了紧跟其后被定义的东西能够被谁使用。public表示紧随其后的元素对任何人都是可用的,而private整个关键字表示除类型建立者和类型的内部方法以外的任何人都不能访问的元素。private就像你与客户端程序员之间的一堵墙,若是有人试图访问private成员,就会在编译时获得错误信息。protected关键字与private做用至关,差异仅在于继承的类能够访问protected成员,可是不能访问private成员。稍后将会对继承进行介绍。
java还有一种默认的访问权限,当没有使用前面提到的任何访问指定词时,它将发挥做用。这种权限一般被称为包访问权限,由于在这种权限下,类能够访问在同一个包(库构件)中的其余类的成员,可是在包以外,这些成员如同指定了private同样。
1.5复用具体实现
一旦类被建立并被测试完,那么它就应该(在理想状况下)表明一个有用的代码单元。事实证实,这种复用性并不容易达到咱们所但愿的那种程度,产生一个可复用的对象设计须要丰富的经验和敏锐的洞察力。可是一旦你有了这样的设计,它就可供复用。代码复用时面向对象程序设计语言所提供的最了不得的优势之一。
最简单地复用某个类的方式就是直接使用该类的一个对象,此外也能够将那个类的一个对象置于某个新的类中。咱们称其为”建立一个成员对象“。新的类能够由任意数量、任意类型的其余对象以任意能够实现新的类中想要的功能的方式所组成。由于是在使用现有的类合成新的类,因此这种概念被称为组合(composition),若是组合是动态发生的,那么它一般被称为聚合(aggregateion)。组合常常被视为”has-a“(拥有)关系,就像咱们常说的”汽车拥有引擎“同样。
组合带来了极大的灵活性。新类的成员对象一般都被声明为private,使得使用新类的客户端程序员不能访问他们。这也是得你能够在不干扰现有客户端代码的状况下,修改这些成员。也能够在运行时修改这些成员对象,以实现动态修改程序的行为。下面将要讨论的继承并不具有这样的灵活性,由于编译器必须对经过继承而建立的类施加编译时的限制。
因为继承在面向对象程序设计中如此重要,因此它常常被高度强调,因而程序员新手就会有这样的印象:到处都应该使用继承。这会致使难以使用并过度复杂的设计。实际上,在创建新类时,应该首先考虑组合,由于它更加简单灵活。若是采用这种方式,设计会变得更加清晰。一旦有了一些经验以后,便可以看出必须使用继承的场合了。
1.6继承
对象这种观念,自己就时十分方便的工具,使得你能够经过概念将数据和功能封装到一块儿,所以能够对问题空间的观念给出恰当的表示,而不用受制于必须使用底层机器语言。这些概念用关键字class来表示,它们造成了编程语言中的基本单位。
遗憾的是,这样作仍是有不少麻烦:在建立了一个类以后,即便另外一个新类与其具备类似的功能,你仍是得从新建立一个新类。若是咱们可以以现有的类为基础,复制它,而后经过添加和修改整个副原本建立新类那就要好多了。经过继承即可以达到这样的效果,不过也有例外,当源类(被称为基类、超类或者父类)发生变更时,被修改的”副本“(被称为导出类、继承类或者子类)也会反映出这些变更。
类型不只仅只是描述了做用于一个对象集合上的约束条件,同时还有与其余类型之间的关系。两个类型能够有相同的特性和行为,可是其中一个类型可能比另外一个含有更多的特性,而且能够处理更多的消息(或以不一样的方式来处理消息)。继承使用基类型和导出类型的概念表示了这种类型之间的类似性。一个基类型包含其全部导出类型所共享的特性和行为。能够建立一个基类型来表示系统中某些对象的核心概念,从基类型中导出其余类型,来表示此核心能够被实现的各类不一样方式。
以垃圾回收机为例,它用来归类散落的垃圾。”垃圾“是基类型,每一件垃圾都有重量、价值等特性,能够被切碎、溶化或分解。在此基础上,能够经过添加额外的特性(例如瓶子有颜色)或行为(例如铅罐能够被压碎,铁罐能够被磁化)导出更具体的垃圾类型。此外,某些行为可能不一样(例如纸的价值取决于其类型和状态)。能够经过使用继承来构建一个类型层次结构,以此来表示待求解的某种类型的问题。
第二个例子是经典的几何形的例子,这在计算机辅助设计系统或游戏仿真系统中可能被用到。基类是几何形,每个几何形都具备尺寸、颜色、位置等,同时每个几何形均可以被绘制、擦除、移动和着色等。在此基础上,能够导出(继承出)具体的几何形状------圆形、正方形、三角形等-------每一种都具备额外的特性和行为,例如某些形状能够被翻转。某些行为可能并不相同,例如计算几何形状的面积。类型层次结构同时体现了几何形状之间的类似性和差别性
以一样的术语将解决方案转换成问题是大有裨益的,由于不须要在问题描述和解决方案描述之间创建许多中间模型。经过使用对象,类型层次结构成为了主要模型,所以,能够直接从真实世界中对系统的描述过渡到用代码对系统进行描述。事实上,对使用面向对象设计的人们来讲,困难之一是从开始到结束过于简单。对于训练有素、善于寻找复杂的解决方案的头脑来讲,可能会在一开始被这种简单性给难道。
当继承现有类型时,也就创造了新的类型。这个新的类型不只包括现有类型的全部成员(尽管private成员被隐藏了起来,而且不可访问),并且更重要的是它复制了基类的接口。也就是说,全部能够发送给基类对象的消息同时也能够发送给导出类对象。因为经过发送给类的信息的类型可知类的类型,因此这也就意味着导出类与基类具备相同的类型。在前面的例子中,”一个圆形也就是一个几何形“。经过继承而产生的类型等价性是理解面向对象程序设计方法内涵的重要门槛。
因为基类和导出类具备相同的基础接口,因此伴随此接口的一定有某些具体实现。也就是说,当对象接收到特定消息时,必须有某些代码去执行。若是只是简单地继承一个类而并不作其余任何事,那么在基类接口中的方法将会直接继承到导出类中。这意味着导出类的对象不只与基类拥有相同的类型,并且还拥有相同的行为,这样作没有什么特别意义。
有两种方法可使基类和导出类产生差别。第一种方法很是直接:直接在导出类中添加新方法。这些新方法并非基类接口的一部分。这意味着基类不能直接知足你的全部需求,所以必需添加更多的方法。这种对继承简单而基本的使用方式,有时对问题来讲确实使一种完美的解决方式。可是,应该仔细考虑是否存在基类也须要这些额外方法的可能性。这种设计的发现与迭代过程在面向对象程序设计中会常常发生。
虽然继承有时可能意味着在接口中添加新方法(尤为是在以extends关键字表示继承的java中),但并不是总须要如此。第二种也是更重要的一种使导出类和基类之间产生差别的方法是改变现有基类的方法的行为,这被称之为覆盖(overridiing)那个方法。
要想覆盖某个方法,能够直接在导出类种建立该方法的新定义便可。你能够说:”此时,我正在使用相同的接口方法,可是我想在新类型种作些不一样的事情。“
1.6.1 ”是一个“与”像是一个“关系
对于继承可能会引起某种争论:继承应该只覆盖基类的方法(而并不添加在基类中没有的新方法)吗?若是这样作,就意味着导出类和基类是彻底相同的类型,由于它们具备彻底相同的接口。结果能够用一个导出类对象来彻底替代一个基类对象。这能够被视为纯粹替代,一般称之为替代原则。在某种意义上,这是一种处理继承的理想方式。咱们常常将这种状况下的基类与导出类之间的关系称为is-a(是一个)关系,由于能够说”一个圆形就是一个几何形状“。判断是否继承,就是要肯定是否能够用is-a来描述类之间的关系,并使之具备实际意义。
有时必须在导出类型中添加新的接口元素,这样也就扩展了接口。这个新的类型仍然能够替代基类,可是这种替代并不完美,由于基类没法访问新添加的方法。这种状况咱们能够描述为is-like-a(像是一个)关系。新类型具备旧类型的接口,可是它还包含其余方法,因此不能说它们彻底相同。以空调为例,假设房子里已经布线安装好了全部的冷气设备的控制器,也就是说,房子具有了让你控制冷气设备的接口。想象一下,若是空调坏了,你用一个既能制冷又能制热的热力泵替换了它,那么这个热力泵就is-like-a空调,可是它能够作更多的事。由于房子的控制系统被设计为只能控制冷气设备,因此它只能和新对象中的制冷部分进行通讯。尽管新对象的接口已经被扩展了,可是现有系统除了原来接口以外,对其余东西一无所知。
固然,在看过这个设计以后,很显然会发现,制冷系统这个基类不够通常化,应该将其 改名为”温度控制系统“,使其能够包括制热功能,这样咱们旧能够套用替代原则了,这张图说明了在真实世界中进行设计时可能会发生的事情。
当你看到替代原则时,很容易会认为这种方式(纯粹替代)是惟一可行的方式,并且事实上,用这种方式设计是很好的。可是你会时常发现,一样显然的是你必须在导出类的接口中添加新方法。这要仔细审视,两种方法的使用场合应该是至关明显的。
1.7 伴随多态的可互换对象
在处理类型的层次结构时,常常想把一个对象不看成它所属的特定类型来对待,而是将其看成其基类的对象来对待。这使得人们能够编写出不依赖于特定类型的代码。在”几何形“的例子中,方法操做的都是泛化(generic)的形状,而不关心它们是圆形、正方形、三角形仍是其余什么还没有定义的形状。全部的几何形状均可以被绘制、擦除和移动,因此这些方法都是直接对一个几何形对象发送消息;它们不用担忧对象将如何处理消息。
这样的代码是不会受添加新类型影响的,并且添加新类型是扩展一个面向对象程序以便处理新状况的最经常使用方式。例如,能够从”几何形“中导出一个新的子类型”五角形“,而并不须要修改处理泛化几何形状的方法。经过导出新的子类型而轻松扩展设计的能力是对改动进行封装的基本方式之一。这种能力能够极大地改善咱们的设计,同时也下降软件维护的代价。
可是,在试图将导出类型的对象看成其泛化基类型对象来看待时(把圆形看做是几何形,把自行车看做是交通工具,把鸬鹚看做是鸟等等),仍然存在一个问题。若是某个方法要让泛化几何形状绘制本身、让泛化交通工具行驶,或者让泛化的鸟类移动,那么编译器在编译时是不可能知道应该执行哪一段代码的。这就是关键所在:当发送这样的消息时,当发送这样的消息时,程序员并不想知道哪一段代码将被执行;绘图方法能够被等同地应用于圆形、正方形、三角形,而对象会依据自身的具体类型来执行恰当的代码。
若是不须要知道哪一段代码被执行,那么当添加新的子类型时,不须要更改调用它的方法,它就可以执行不一样的代码。所以,编译器没法精确地了解哪一段代码将会被执行,那么它该怎么办呢?例如,在下面的图中,BirdController对象仅仅处理泛化的Bird对象,而不了解它们的确切类型。从BirdController的角度看,这么作很是方便,由于不须要编写特别的代码来断定要处理的Bird对象的确切类型或其行为。当move()方法被调用时,即使忽略Bird的具体类型,也会产生正确的行为(Goose(鹅)走、飞或游泳,Penguin(企鹅)走或游泳),那么,这是如何发生的呢?
这个问题的结果,也是面向对象程序设计的最重要的妙诀:编译器不可能产生传统意义上的函数调用。一个非面向对象编译器产生的函数调用会引发所谓的前期绑定,这个术语你可能之前从未据说过,可能从未想过函数调用的其余方式。这么作意味着编译器将产生对一个具体函数名字的调用,而运行时将这个调用解析到将要被执行的代码的绝对地址。然而在OOP中,程序直到运行时才可以肯定代码的地址,因此当消息发送到一个泛化对象时,必须采用其余的机制。
为了解决这个问题,面向对象程序设计语言使用了后期绑定的概念。当向对象发送消息时,被调用的代码直到运行时才能肯定。编译器确保被调用方法的存在,并对调用参数和返回值执行类型检查(没法提供此类保证的语言被称为是弱类型的),可是并不知道将被执行的确切代码。
为了执行后期绑定,Java使用一小段特殊的代码来替代绝对地址调用。这段代码使用在对象中存储的信息来计算方法体的地址(这个过程将在第8章中详述)。这样,根据这一小段代码的内容,每个对象均可以具备不一样的行为表现。当向一个对象发送消息时,该对象就可以知道对这条消息应该作些什么。
在某些语言中,必须明确地声明但愿某个方法具有后期绑定属性所带来的灵活性(C++是使用virtual关键字来实现的)。在这些语言中,方法在默认状况下不是动态绑定的。而在Java中,动态绑定是默认行为,不须要添加额外的关键字来实现多态。
再来看看几何形状的例子。整个类族(其中全部的类都基于相同的一致接口)在本章前面已有图示。为了说明多态,咱们来编写一段代码,它忽略类型的具体细节,仅仅和基类交互。这段代码和具体类型信息是分离的(decoupled),这样作使代码编写更为简单,也更易于理解。并且,若是经过继承机制添加一个新类型,例如Hexagon(六边形),所编写的代码对Shape(几何形)的新类型的处理与对 已有类型的处理会一样出色。正由于如此,能够称这个程序是可扩展的。
若是用Java来编写一个方法(后边很快你就会学习若是编写):
如上图,建基类(几何形),定义方法;依次建里导出类 圆形 、三角形、等等
把导出类看作是它的基类的过程称为向上转型(upcasting)。转型(cast)这个名称的灵感来自于模型铸造动做;而向上(up)这个词来源于继承图的典型布局方式:一般基类在顶部,而导出类在其下部散开。所以,转型为一个基类就是在继承图中向上移动,即”向上转型“
一个面向对象程序确定会在某处包含向上转型,所以这正是将本身从必须知道确切类型中解放出来的关键,让咱们再看看doSomething()中的代码:
shape.erase();
shape.draw();
注意这些代码并非说”若是是Circle,请这样作;若是是Square,请那样作·······“。若是编写了那种检查Shape全部实际可能类型的代码,那么这段代码确定是杂乱不堪的,并且在每次添加了Shape的新类型以后都要去修改这段代码。这里所要表达的意思仅仅是”你是一个Shape,我知道你能够erase()和draw()你本身,那么去作吧,可是要注意细节的正确性。“
doSomething()的代码给人印象深入之处在于,不知何故,它老是作了该作的。调用Circle的draw()方法所执行的代码与调用Square或Line的draw()方法所执行的代码是不一样的,并且当draw()消息被发送给一个匿名的Shape时,也会基于该Shape的实际类型产生正确的行为。这至关神奇,由于就像在前面提到的,当java编译器在编译doSomething()的代码时,并不能确切知道doSomething()要处理的确切类型。因此一般会指望它的编译结果是调用基类Shape的erase()和draw()版本,而不是具体的Circle、Square或Line的相应版本。正是由于多态才使得事情老是可以被正确处理。编译器和运行系统会处理相关的细节,你须要立刻知道的只是事情会发生,更重要的是怎样经过它设计。当向一个对象发送消息时,即便涉及向上转型,该对象也知道要执行什么样的正确行为。
1.8 单根继承结构
在OOP中,自C++面世以来就已变得很是瞩目的一个问题就是,是否全部的类最终都继承自单一的基类。在Java中(事实上还包括除C++之外的全部OOP语言),结果是yes,这个终极基类的名字就是Object。事实证实,单根继承结构带来了不少好处。
在单根继承结构中的全部对象都具备一个共用接口,因此它们归根到底都是相同的基本类型。另外一种(C++所提供的)结构是没法确保全部对象都属于同一个基本类型。从向后兼容的角度看,这么作可以更好地适应C模型,并且受限较少,可是当要进行彻底的面向对象程序设计时,则必须构建本身的继承体系,使得它能够提供其余OOP语言内置的便利。并且在所得到的任何新类库中,总会用到一些不兼容的接口,须要花力气(有可能要经过多重继承)来使新接口融入你的设计之中。这么作来换取C++额外的灵活性是否值得呢?若是须要的话------若是在C上面投资巨大,这么作就颇有价值。若是是刚刚从头开始,那么像java这样的选择一般会有更高的效率。
单根继承结构保证全部对象都具有某些功能。所以你知道,在你的系统中你能够在每一个对象上执行某些基本操做。全部对象均可以很容易地在堆上建立,而参数传递也获得了极大的简化。
单根继承结构使垃圾回收器的实现变得容易得多,而垃圾回收器正是Java相对C++的重要改进之一。因为全部对象都保证具备其类型信息,所以不会因没法肯定对象的类型而陷入僵局。这对于系统级操做(如异常处理)显得尤为重要,而且给编程带来了更大的灵活性。
1.9 容器
一般说来,若是不知道在解决某个特定问题时须要多少个对象,或者它们将存活多久,那么就不可能知道如何存储这些对象。如何才能知道须要多少空间来建立这些对象呢?结果是你不可能知道,由于这类信息只有在运行时才能得到。
对于面向对象设计中的大多数问题而言,这个问题的解决方案彷佛过于轻率:建立另外一种对象类型。这种新的对象类型持有对其余对象的引用。固然,你能够用在大多数语言中都有的数组类型来实现相同的功能。可是这个一般被称为容器(也称为集合,不过Java类库以不一样的含义使用”集合“这个术语,因此本书将使用”容器“这个词)的新对象,在任何须要时均可扩充本身以容纳你置于其中的全部东西。所以不须要知道未来会把多少个对象置于容器中,只须要建立一个容器对象,而后让它处理全部细节。
幸运的是,好的OOP语言都有一组容器,它们做为开发包的一部分。在C++中,容器是标准C++类库的一部分,常常被称为标准模板类库(Standard Template Library,STL)。Object Pascal在其可视化构件库(Visual Component Library,VCL)中有容器;Smalltalk提供了一个很是完备的容器集;Java在其标准类库中也包含有大量的容器。在某些类库中,一两个通用容器足够知足全部的须要;可是在其余类库(例如Java)中,具备知足不一样须要的各类类型的容器,例如List(用于存储序列),Map(也被称为关联数组,用来创建对象之间的关联),Set(每种对象类型只持有一个),以及诸如队列、树、堆栈等更多的构件。
从设计的观点来看,真正须要的只是一个能够被操做,从而解决问题的序列。若是单一类型的容器能够知足全部须要,那么就没有理由设计不一样种类的序列了。然而仍是须要对容器有所选择,这有两个缘由。第一,不一样容器提供了不一样类型的接口和外部行为。堆栈相比于队列就具有不一样的接口和行为,也不一样于集合和列表的接口和行为。它们之中的某种容器提供的解决方案可能比其余容器要灵活的多。第二,不一样的容器对于某些操做具备不一样的效率。最好的例子就是两个List的比较:ArrayList和LinkedList。它们都是具备相同接口和外部行为的简单的序列,可是它们对某些操做所花费的代价却有天壤之别。在ArrayList中,随机访问元素是一个花费固定时间的操做;可是,对LinkedList来讲,随机选取元素须要在列表中移动,这种代价是高昂的,访问越靠近表尾的元素,花费的时间越长。而另外一方面,若是想在序列中间插入一个元素,LinkedList的开销却比ArrayList要小。上述操做以及其余操做的效率,依序列底层结构的不一样而存在很大的差别。咱们能够在一开始使用LinkedList构建程序,而在优化系统性能时改用ArrayList。接口List所带来的抽象,把在容器之间进行转换时对代码产生的影响降到最小限度。
1.9.1 参数化类型
在Java SE5出现以前,容器存储的对象都只具备Java中的通用类型:Object。单根继承结构意味着全部东西都是Object类型,因此能够存储Object的容器能够存储任何东西。这使得容器很容易被复用。
要使用这样的容器,只需在其中置入对象引用,稍后还能够将它们取回。可是因为容器只存储Object,因此当将对象引用置入容器时,它必须被向上转型为Object,所以它会丢失其身份。当把它取回时,就获取了一个Object对象的引用,而不是对置入时的那个类型的对象的引用。因此,怎样才能将它变回先前置入容器中时的具备实用接口的对象呢?
这里再度用到了转型,但这一次不是向继承结构的上层转型为一个更泛化的类型,而是向下转型为更具体的类型。这种转型的方式称为向下转型。咱们知道,向上转型是安全的,例如Circle是一种Shape类型;可是不知道某个Object是Circle仍是Shape,因此除非确切知道所要处理的对象的类型,不然向下转型几乎是不安全的。
然而向下转型并不是完全是是危险的,由于若是向下转型为错误的类型,就会获得被称为异常的运行时错误,稍后会介绍什么时异常。尽管如此,当从容器中取出对象引用时,仍是必需要以某种方式记住这些对象究竟时什么类型,这样才能执行正确的向下转型。
向下转型和运行时的检查须要额外的程序运行时间,也须要程序员付出更多的心血。那么建立这样的容器,它知道本身所保存的对象的类型,从而不须要向下转型以及消除犯错误的可能,这样不是更有意义吗?这种解决方案被称为参数化类型机制。参数化类型就是一个编译器能够自动定制做用于特定类型上的类。例如,经过使用参数化类型编译器能够定制一个只接纳和取出Shape对象的容器。
Java SE5的重大变化之一就是增长了参数化类型,在java中它称为泛型。一对尖括号,中间包含类型信息,经过这些特征就能够识别对泛型的使用。例如,能够用下面这样的语句来建立一个存储Shape的ArrayList:
ArrayList<Shape> shapes = new ArrayList<Shape>();
为了利用泛型的优势,不少标准类库构件都已经进行了修改。就像咱们将要看到的那样,泛型对本书中的许多代码都产生了重要的影响。
1.10 对象的建立和生命期
在使用对象时,最关键的问题之一即是它们的生成和销毁方式。每一个对象为了生存都须要资源,尤为时内存。当咱们再也不须要一个对象时,它必须被清理掉,使其占有的资源能够被释放和重用。在相对简单的编程状况下,怎样清理对象看起来彷佛不是什么挑战:你建立了对象,根据须要使用它,而后它应该被销毁。然而,你极可能会遇到相对复杂的状况。
例如,假设你正在为某个机场设计空中交通管理系统(一样的模型在仓库货柜管理系统、录像带出租系统或宠物寄宿店也适用)。一开始问题彷佛很简单:建立一个容器来保存全部的飞机,而后为每一架进入空中交通控制区的飞机建立一个新的飞机对象,并将其置于容器中。对于清理工做,只需在飞机离开此区域时删除相关的飞机对象便可。
可是,可能还有别的系统记录着有关飞机的数据,也许这些数据不须要像主要控制功能那样当即引人注意。例如,它可能记录了全部飞离机场的小型飞机的飞行计划。所以你须要有第二个容器来存放小型飞机;不管什么时候,只要建立的使小型飞机对象,那么它同时也应该置入第二个容器内。而后某个后台进程在空闲时对第二个容器内的对象进行操做。
如今问题变得更困难了:怎样才能知道什么时候销毁这些对象呢?当处理完某个对象以后,系统某个其余部分可能还在处理它。在其余许多场合中会遇到一样的问题,在必须明确删除对象的编程系统中(例如C++),此问题会变得十分复杂。
对象的数据位于何处?怎样控制对象的生命周期?C++认为效率控制是最重要的议题,因此给程序员提供了选择的权利。为了追求最大的执行速度,对象的存储空间和生命周期能够在编写程序时肯定,这能够经过将对象置于堆栈(它们有时被为自动变量 (automatic variable)或限域变量(scoped variable))或静态存储区域内来实现。这种方式将存储空间分配和释放置于优先考虑的位置,某些状况下这样控制很是有价值。可是,也牺牲了灵活性,由于必须在编写程序时知道对象确切的数量、生命周期和类型。若是试图解决更通常化的问题,例如计算机辅助设计、仓库管理或者空中交通控制,这种方式就显得过于受限了。
第二种方式是在被称为堆(heap)的内存池中动态地建立对象。在这种方式中,直到运行时才知道须要多少对象,它们的生命周期如何,以及它们的具体类型时什么。这些问题的结果只能在程序运行时相关代码被执行到的那一刻才能肯定。若是须要一个新对象,能够在须要的时刻直接在堆中建立。由于存储空间是在运行时被动态管理的,因此须要大量的时间在堆中分配存储空间,这可能要远远大于在堆栈中建立存储空间的时间。在堆栈中建立存储空间和释放存储空间一般各须要一条汇编指令便可,分别对应将栈顶指针向下移动和将栈顶指针向上移动。建立堆存储空间的时间依赖于存储机制的设计。
动态方式有这样一个通常性的逻辑假设:对象趋向于变得复杂,因此查找和释放存储空间的开销不会对对象的建立形成重大冲击。动态方式所带来的更大的灵活性正是解决通常化编程问题的要点所在。
java彻底采用了动态内存分配方式。每当想要建立新对象时,就要使用new关键字来构建此对象的动态实例。
还有一个议题,就是对象生命周期。对于容许在堆栈上建立对象的语言,编译器能够肯定对象存活的时间,并能够自动销毁它。然而,若是是在堆上建立对象,编译器就会对它的生命周期一无所知。在像C++这样的语言中,必须经过编程方式来肯定什么时候销毁对象,这可能会由于不能正确处理而致使内存泄露(这在C++程序中是常见的问题)。Java提供了被称为“垃圾回收器”的机制,它能够自动发现对象什么时候再也不被使用,并继而销毁它。垃圾回收器很是有用,由于它减小了所必须考虑的议题和必须编写的代码。更重要的是,垃圾回收器提供了更高层的保障,能够避免暗藏的内存泄露问题,这个问题已经使许多C++项目折戟沉沙。
Java的垃圾回收器被设计用来处理内存释放问题(尽管它不包括清理对象的其余方面)。垃圾回收器“知道”对象什么时候再也不被使用,而且自动释放对象占用的内存。这一点同全部对象都是继承自单根基类Object以及只能以一种方式建立对象(在堆上建立)这两个特性结合起来,使得用Java编程的过程较之用C++编程要简单得多,所要作出的决策和要克服的障碍也少得多。
1.11 异常处理:处理错误
自从编程语言问世以来,错误处理就始终是最困难的问题之一。由于设计一个良好的错误处理机制很是困难,因此许多语言直接略去这个问题,将其交给程序库设计者处理,而这些设计者也只是提出了一些不完全的方法,这些方法可用于许多很容易就能够绕过此问题的场合,并且其解决方式一般也只是忽略此问题。大多数错误处理机制的主要问题在于,它们都依赖于程序员自身的警戒性,这种警戒性来源于一种共同的约定,而不是编程语言所强制的。若是程序员不够警戒------一般是由于他们太忙,这些机制就很容易被忽视。