1.4被隐藏的具体实现 类创建者(那些创建新数据类型的程序员)的目标是构建类,这种类只向客户端程序员暴露必需的部分,而隐藏其他部分。 客户端程序员(那些在其应用中使用数据类型的类消费者)目标是收集各种用来实现快速应用开发的类。 为什么要这样呢?因为如果加以隐藏,那么客户端程序员将不能够访问它, 这意味着类创建者可以任意修改被隐藏的部分,而不用担心对其他任何人造成影响。 被隐藏的部分通常代表对象内部脆弱的部分,它们很容易被粗心的或不知内情的客户端程序员所毁坏,因此将实现隐藏起来可以减少程序bug。
在任何相互关系中,具有关系所涉及的各方都遵守的边界是十分重要的事情。 访问控制的第一个存在原因就是让客户端程序员无法触及他们不应该触及的部分——这些部分对数据类型的内部操作来说是必需的,但并不是用户解决特定问题所需的接口的一部分。 访问控制的*第二个存在原因就是允许库设计者可以改变类内部的工作方式而不用担心会影响到客户端程序员。 Java用三个关键字在类的内部设定边界:public、private、protected 以及包访问权限。
1.5 复用具体实现 代码复用是面向对象程序设计语言所提供的最了不起的优点之一。 因为是在使用现有的类合成新的类,所以这种概念被称为组合(composition)。 如果组合是动态发生的,那么它通常被称为聚合(aggregation)。 组合经常被视为 “has-a”(拥有)关系,就像我们常说的"汽车拥有引擎"一样。 组合带来了极大的灵活性。……
实际上,在建立新类时,应该首先考虑组合,因为它更加简单灵活。
1.6 继承 源类(被称为基类、超类或父类), "副本”(被称为导出类、继承类或子类) 类型不仅仅只是描述了作用于一个对象集合上的约束条件,同时还有与其他类型之间的关系。 以垃圾回收机为例 第二个例子是经典的几何形的例子 类型层次结构同时体现了几何形状之间的相似性和差异性。 通过继承而产生的类型等价性是理解面向对象程序设计方法内涵的重要门槛。
有两种方法可以使基类与导出类产生差异。 第一种方法非常直接:直接在导出类中添加新方法。 第二种也是更重要的一种使导出类和基类之间产生差异的方法是改变现有基类的方法的行为,这被称之为覆盖(overriding)那个方法
将程序开发人员按照角色分为类创建者(那些创建新数据类型的程序员)和客户端程序员(那些在其应用中使用数据类型的类消费者)是大有裨益的。 客户端程序员的目标是收集各种用来实现快速应用开发的类。 类创建者的目标是构建类,这种类只向客户端程序员暴露必需的部分,而隐藏其他部分。 为什么要这样呢?因为如果加以隐藏,那么客户端程序员将不能够访问它, 这意味着类创建者可以任意修改被隐藏的部分,而不用担心对其他任何人造成影响。 被隐藏的部分通常代表对象内部脆弱的部分,它们很容易被粗心的或不知内情的客户端程序员所毁坏,因此将实现隐藏起来可以减少程序bug。
在任何相互关系中,具有关系所涉及的各方都遵守的边界是十分重要的事情。 当创建一个类库时,就建立了与客户端程序员之间的关系,他们同样也是程序员,但是他们是使用你的类库来构建应用、或者构建更大的类库的程序员。 如果所有的类成员对任何人都是可用的,那么客户端程序员就可以对类做任何事情,而不受任何约束。 即使你希望客户端程序员不要直接操作你的类中的某些成员,但是如果没有任何访问控制,将无法阻止此事发生。 所有东西都将赤裸裸地暴露于世人面前。
因此,访问控制的第一个存在原因就是让客户端程序员无法触及他们不应该触及的部分——这些部分对数据类型的内部操作来说是必需的,但并不是用户解决特定问题所需的接口的一部分。 这对客户端程序员来说其实是一项服务,因为他们可以很容易地看出哪些东西对他们来说很重要,而哪些东西可以忽略。
访问控制的第二个存在原因就是允许库设计者可以改变类内部的工作方式而不用担心会影响到客户端程序员。 例如,你可能为了减轻开发任务而以某种简单的方式实现了某个特定类,但稍后发现你必须改写它才能使其运行得更快。 如果接口和实现可以清晰地分离并得以保护,那么你就可以轻而易举地完成这项工作。
Java用三个关键字在类的内部设定边界:public、private、protected 。 这些访问指定词(access specifier)决定了紧跟其后被定义的东西可以被谁使用。 public表示紧随其后的元素对任何人都是可用的,而private这个关键字表示除类型创建者和类型的内部方法之外的任何人都不能访问的元素。 private就像你与客户端程序员之间的一堵砖墙,如果有人试图访问private成员,就会在编译时得到错误信息。 protected关键字与private作用相当,差别仅在于继承的类可以访问protected成员,但是不能访问 private成员。 稍后将会对继承进行介绍。
Java还有一种默认的访问权限,当没有使用前面提到的任何访问指定词时,它将发挥作用。 这种权限通常被称为包访问权限,因为在这种权限下,类可以访问在同一个包(库构件)中的其他类的成员,但是在包之外,这些成员如同指定了private一样。
一旦类被创建并被测试完,那么它就应该(在理想情况下)代表一个有用的代码单元。 事实证明,这种复用性并不容易达到我们所希望的那种程度,产生一个可复用的对象设计带要丰富的经验和敏锐的洞察力。 但是一旦你有了这样的设计,它就可供复用。 代码复用是面向对象程序设计语言所提供的最了不起的优点之一。
最简单地复用某个类的方式就是直接使用该类的一个对象,此外也可以将那个类的一个对象置于某个新的类中。 我们称其为“创建一个成员对象”。 新的类可以由任意数量、任意类型的其他对象以任意可以实现新的类中想要的功能的方式所组成。 因为是在使用现有的类合成新的类,所以这种概念被称为组合(composition)。 如果组合是动态发生的,那么它通常被称为聚合(aggregation)。 组合经常被视为 “has-a”(拥有)关系,就像我们常说的"汽车拥有引擎"一样。
组合带来了极大的灵活性。 新类的成员对象通常都被声明为private,使得使用新类的客户端程序员不能访问它们。 这也使得你可以在不干扰现有客户端代码的情况下,修改这些成员。 也可以在运行时修改这些成员对象,以实现动态修改程序的行为。 下面将要讨论的继承并不具备这样的灵活性,因为编译器必须对通过继承而创建的类施加编译时的限制。
由于继承在面向对象程序设计中如此重要,所以它经常被高度强调,于是程序员新手就会有这样的印象:处处都应该使用继承。 这会导致难以使用并过分复杂的设计。 实际上,在建立新类时,应该首先考虑组合,因为它更加简单灵活。 如果采用这种方式,设计会变得更加清晰。 一旦有了一些经验之后,便能够看出必须使用继承的场合了。
对象这种观念,本身就是十分方便的工具,使得你可以通过概念将数据和功能封装到一起,因此可以对问题空间的观念给出恰当的表示,而不用受制于必须使用底层机器语言。 这些概念用关键字class来表示,它们形成了编程语言中的基本单位。
遗憾的是,这样做还是有很多麻烦:在创建了一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。 如果我们能够以现有的类为基础,复制它,然后通过添加和修改这个副本来创建新类那就要好多了。 通过继承便可以达到这样的效果,不过也有例外,当源类(被称为基类、超类或父类)发生变动时, 被修改的"副本”(被称为导出类、继承类或子类)**也会反映出这些变动(如右图所示)。
类型不仅仅只是描述了作用于一个对象集合上的约束条件,同时还有与其他类型之间的关系。 两个类型可以有相同的特性和行为, 但是其中一个类型可能比另一个含有更多的特性,并且可以处理更多的消息(或以不同的方式来处理消息)。 继承使用基类型和导出类型的概念表示了这种类型之间的相似性。 一个基类型包含其所有导出类型所共享的特性和行为。 可以创建一个基类型来表示系统中某些对象的核心概念,从基类型中导出其他类型,来表示此核心可以被实现的各种不同方式。
以垃圾回收机为例,它用来归类散落的垃圾。 “垃圾”是基类型,每一件垃圾都有重量,价值等特性,可以被切碎、熔化或分解。 在此基础上,可以通过添加额外的特性(例如瓶子有颜色)或行为(例如铝罐可以被压碎,铁罐可以被磁化)导出更具体的垃圾类型。 此外,某些行为可能不同(例如纸的价值取决于其类型和状态)。 可以通过使用继承来构建一个类型层次结构, 以此来表示待求解的某种类型的问题。
第二个例子是经典的几何形的例子,这在计算机辅助设计系统或游戏仿真系统中可能被用到。 基类是几何形,每一个几何形都具有尺寸、颜色、位置等,同时每一个几何形都可以被绘制、擦除、移动和着色等。 在此基础上,可以导出(继承出)具体的几何形状——圆形、正方形三、角形等。例如某些形状可以被翻转。 某些行为可能并不相同, 例如计算几何形状的面积。 类型层次结构同时体现了几何形状之间的相似性和差异性。
以同样的术语将解决方案转换成问题是大有裨益的,因为不需要在问题描述和解决方案描述之间建立许多中间模型。 通过使用对象,类型层次结构成为了主要模型,因此,可以直接从真实世界中对系统的描述过渡到用代码对系统进行描述。 事实上,对使用面向对象设计的人们来说,困难之一是从开始到结束过于简单。 对于训练有素、善于寻找复杂解的决方案的头脑来说,可能会在一开始被这种简单性给难倒。
当继承现有类型时,也就创造了新的类型。这个新的类型不仅包括现有类型的所有成员(尽管private成员被隐藏了起来,并且不可访问),而且更重要的是它复制了基类的接口。 也就是,说所有可以发送给基类对象的消息同时也可以发送给导出类对象。 由于通过发送给类的消息的类型可知类的类型,所以这也就意味着导出类与基类具有相同的类型。 在前面的例子中,“一个圆形也就是一个几何形。 通过继承而产生的类型等价性是理解面向对象程序设计方法内涵的重要门槛。
由于基类和导出类具有相同的基础接口,所以伴随此接口的必定有某些具体实现。 也就是说,当对象接收到特定消息时,必须有某些代码去执行。 如果只是简单地继承一个类而并不做其他任何事,那么在基类接口中的方法将会直接继承到导出类中。 这意味着导出类的对象不仅与基类拥有相同的类型,而且还拥有相同的行为,这样做没有什么特别意义。
有两种方法可以使基类与导出类产生差异。 第一种方法非常直接:直接在导出类中添加新方法。这些新方法并不是基类接口的一部分。 这意味着基类不能直接满足你的所有需求,因此必需添加更多的方法。 这种对继承简单而基本的使用方式,有时对问题来说确实是一种完美的解决方式。 但是,应该仔细考虑是否存在基类也需要这些额外方法的可能性。 这种设计的发现与迭代过程在面向对象程序设计中会经常发生。
虽然继承有时可能意味者在接口中添加新方法(尤其是在以extends关键字表示继承的Java中),但并非总需如此。 第二种也是更重要的一种使导出类和基类之间产生差异的方法是改变现有基类的方法的行为,这被称之为覆盖 (overriding)那个方法。
要想覆盖某个方法,可以直接在导出类中创建该方法的新定义即可。 你可以说:“此时,我正在使用相同的接口方法,但是我想在新类型中做些不同的事情。”