Java 基础(一)重新理解面向对象

2,523 阅读13分钟

抽象的进步

汇编语言对基础机器的少量抽象;
命令式语言是对汇编语言的一种抽象;

对象的五大基本特征

  • 所有的东西都是对象
  • 程序是一大堆对象的组合
  • 每个对象都有自己的存储空间,可容纳其他对象
  • 每个对象都有一种类型,一个类最重要的特征就是“能将生命消息发给他”
  • 同一类所有对象都能接受相同的消息

对象的接口

如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其做一些实际的事情。接口就是对一个对象的行为进行规范,使对象具有做某些事情的能力。

实现方案的隐藏

假如现在有一个这样的需求:送外卖。对于老板来说,最主要的目标就是把外卖送到客户手上,而对于配送员来说,他要做的是领取外卖、规划路线、送到买家手上,只向老板提供送外卖的能力(接口),其他所有的细节都隐藏。为什么要这样做?隐藏之后,老板就不能接触和改变那些细节,所以配送员也不会担心老板会干扰他用什么交通工具去配送,可确保不会影响外卖正常送到。

“接口”规定了可以对一个特定的对象发出哪些请求,然而,必须在某个地方存在着一些代码,以便满足这些请求。这些代码于哪些隐藏起来的数据叫做“隐藏的实现”,站在程式化编写的角度,整个问题并不显得复杂。一种类型含有与每种可能的请求关联起来的函数。 一旦向对象发出一个特定的请求,就会调用那个函数。我们通常将这个过程总结为向对象“发送一条消息”(提出一个请求)。对象的职责就是决定如何对这条消息作出反应(执行相应的代码)。

对于任何关系,重要的一点是让牵连到的所有成员都遵守相同的规则。创建一个配送员时,相当于和老板建立了一种关系,对方也是人类,但他们的目标是组合出一个特定的平台,如“饿了么”。

有两个方面的原因促使我们控制对成员的访问,第一个就是防止篮板干扰配送员送外卖的行为---通常是内部数据类型的设计思想。若只是为了解决特定的问题,老板注意要操作“配送”接口即可。配送员向老板提供的实际是一种配送服务,因为老板只关心配送员能否帮他送外卖,而对配送员怎么送外卖并不关心。

进行访问控制的第二个原因是允许配送员修改内部结构,不用担心会对老板造成什么影响。比如说配送员找了个女朋友。

若任何人都能使用一个类的所有成员,那么老板可以对配送员 A 操作任何事情,比如说不能骑车去送外卖,外卖送达要规则给客户,配送员 B 怕配送员 A 业绩超过自己,偷偷的把配送员 A 的小电驴扎爆胎,那么整个平台就乱套了。

Java 采用了三个显式关键字以及一个隐式关键字来设置类边界。public、private、protected 以及暗示性的friendly。关键字的边界就不再赘述了,不懂的小伙伴自行搜索。

方案的重复使用

创建并测试好一个类之后,从理想的角度来说它应代表一个有用的单位。但并不像许多人期望的那样,这种重复使用的能力并不容易实现;它要求较多的经验及洞察力,这样才能设计出一个好的方案,才有可能重复使用

为重复使用一个类,最简单的办法是仅直接使用那个类的对象。但同时也能 将那个类的一个对象置入一个新类。我们把这叫作“创建一个成员对象”。新类 可由任意数量和类型的其他对象构成。无论如何,只要新类达到了设计要求即可。 这个概念叫作“组织”——在现有类的基础上组织一个新类。有时,我们也将组 织称作“包含”关系。

对象的组织具有极大的灵活性。新类的“成员对象”通常设为“私有”(rpivate),使其不能被其他类访问。这样一来,我们可以在不影响其他类的前提下,从容的修改那些成员。这进一步增大了灵活性。后面要讲的“继承”并不具有这种灵活性,因为编译器必须对通过继承创建的类加以限制以提高复用性。

可能上面这一段讲得有点抽象,我们再拿刚刚那个配送员的女朋友来举例:女朋友具有做饭的能力,通常女朋友都是私有的(private)只给自己做饭吃。但是如果女朋友变成公有的(public),配送员 B 也用 A 的女朋友做饭吃。此时问题就出现了,配送员 A 要是和换了个不会做饭的女朋友, B 就会没饭吃。

继承:重新使用接口

就其本身来说,对象的概念可为我们带来极大的便利。它在概念上允许我们 将各式各样数据和功能封装到一起。这样便可恰当表达“问题空间”的概念,不 用刻意遵照基础机器的表达方式。在程序设计语言中,这些概念则反映为具体的 数据类型(使用 class 关键字)。

我们费尽心思做出一种数据类型后,假如不得不又新建一种类型,令其实现 大致相同的功能,那会是一件非常令人灰心的事情。但若能利用现成的数据类型, 对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承” 正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原 始类(正式名称叫作基础类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子类)也会反映出这种变化。在 Java 语言中,继承是通 过 extends 关键字实现的

使用继承时,相当于创建了一个新类。这个新类不仅包含了现有类型的所有 成员(尽管 private 成员被隐藏起来,且不能访问),但更重要的是,它复制了基 础类的接口。也就是说,可向基础类的对象发送的所有消息亦可原样发给衍生类 的对象。根据可以发送的消息,我们能知道类的类型。这意味着衍生类具有与基 础类相同的类型!为真正理解面向对象程序设计的含义,首先必须认识到这种类 型的等价关系。

还拿刚刚送外卖来说,配送员 A 骑自行车送外卖有点慢,为了更快的送达外卖,现在招聘了配送员的儿子配送员 AA(会骑摩托车),配送员 AA 继承了 A 的送外卖能力,同样可以送外卖。

由于基础类和衍生类具有相同的接口,所以那个接口必须进行特殊的设计。 也就是说,对象接收到一条特定的消息后,必须有一个“方法”能够执行。若只 是简单地继承一个类,并不做其他任何事情,来自基础类接口的方法就会直接照 搬到衍生类。这意味着衍生类的对象不仅有相同的类型,也有同样的行为,这一 后果通常是我们不愿见到的。

刚刚我们说了为了更快的送达外卖,所以才招聘A 的儿子 AA,但是如果 AA 也只从父亲 A 那里学会骑自行车送外卖,那就得不偿失了,所以我们需要的 AA 必须是会骑摩托车的。

有两种做法可将新得的衍生类与原来的基础类区分开。第一种做法十分简 单:为衍生类添加新函数(功能)。这些新函数并非基础类接口的一部分。进行 这种处理时,一般都是意识到基础类不能满足我们的要求,所以需要添加更多的 函数。这是一种最简单、最基本的继承用法,大多数时候都可完美地解决我们的 问题。然而,事先还是要仔细调查自己的基础类是否真的需要这些额外的函数。

等价与类似关系

针对继承可能会产生这样的一个争论:继承只能改善原基础类的函数吗?若 答案是肯定的,则衍生类型就是与基础类完全相同的类型,因为都拥有完全相同的接口。这样造成的结果就是:我们完全能够将衍生类的一个对象换成基础类的一个对象!可将其想象成一种“纯替换”。在某种意义上,这是进行继承的一种理想方式。此时,我们通常认为基础类和衍生类之间存在一种“等价”关系——因为我们可以理直气壮地说:“圆就是一种几何形状”。为了对继承进行测试,一 个办法就是看看自己是否能把它们套入这种“等价”关系中,看看是否有意义。

但在许多时候,我们必须为衍生类型加入新的接口元素。所以不仅扩展了接 口,也创建了一种新类型。这种新类型仍可替换成基础类型,但这种替换并不是完美的,因为不可在基础类里访问新函数。我们将其称作“类似”关系;新类型拥有旧类型的接口,但也包含了其他函数,所以不能说它们是完全等价的。举个例子来说,让我们考虑一下制冷机的情况。假定我们的房间连好了用于制冷的各 种控制器;也就是说,我们已拥有必要的“接口”来控制制冷。现在假设机器出了故障,我们把它换成一台新型的冷、热两用空调,冬天和夏天均可使用。冷、热空调“类似”制冷机,但能做更多的事情。由于我们的房间只安装了控制制冷 的设备,所以它们只限于同新机器的制冷部分打交道。新机器的接口已得到了扩 展,但现有的系统并不知道除原始接口以外的任何东西。

认识了等价与类似的区别后,再进行替换时就会有把握得多。尽管大多数时 候“纯替换”已经足够,但您会发现在某些情况下,仍然有明显的理由需要在衍生类的基础上增添新功能。通过前面对这两种情况的讨论,相信大家已心中有数 该如何做。

多形对象的互换使用

通常,继承最终会以创建一系列类收场,所有类都建立在统一的接口基础上。比如说一家公司要正常运行run(),需要招三个干活的人,而Worker 都具有干活的能力,只是不同的人具备不同的技能而已。现在公司如果要运行的话,只需要调用所有的“Worker”的 doWork 方法即可。而并不用关心是程序员写代码还是设计师做设计。

对这样的一系列类,我们要进行的一项重要处理就是将衍生类的对象当作基 础类的一个对象对待。这一点是非常重要的,因为它意味着我们只需编写单一的代码,令其忽略类型的特定细节,只与基础类打交道。这样一来,那些代码就可与类型信息分开。所以更易编写,也更易理解。此外,若通过继承增添了一种新类型,如“程序员”,那么我们为“Worker”新类型编写的代码会象在旧类型 里一样良好地工作。所以说程序具备了“扩展能力”,具有“扩展性”。

动态绑定

比如说,一个公司在运行的时候要控制三个员工工作。在公司正常运行 run()的过程中,最让人吃惊的是尽管我们没作出任何特殊指示,采取的操作也是完全正确和恰当的。我们知道,为 程序员 调用 doWork()时执行的代码与 为一个 设计师 或 产品 调用 doWork()时执行的代码是不同的。但在将doWork()消息发 给一个匿名 Worker 时,根据 Worker句柄当时连接的实际类型,会相应地采取正确 的操作。这当然令人惊讶,因为当 Java 编译器为 公司正常运行为run()编译代码时,它并不 知道自己要操作的准确类型是什么。尽管我们确实可以保证最终会为 Worker 调用doWork(),但并不能保证为特定的 程序员,设计师 或者 产品 调用什么。然而最后采取的操作同样是正确的,这是怎么做到的呢?

将一条消息发给对象时,如果并不知道对方的具体类型是什么,但采取的行 动同样是正确的,这种情况就叫作“多形性”(Polymorphism)。对面向对象的程序设计语言来说,它们用以实现多形性的方法叫作“动态绑定”。编译器和运行期系统会负责对所有细节的控制;我们只需知道会发生什么事情,而且更重要的 是,如何利用它帮助自己设计程序。有些语言要求我们用一个特殊的关键字来允许动态绑定。在 C++中,这个关 键字是 virtual。在 Java中,我们则完全不必记住添加一个关键字,因为函数的 动态绑定是自动进行的。所以在将一条消息发给对象时,我们完全可以肯定对象会采取正确的行动,即使其中涉及上溯造型之类的处理。

----读《Thinking in Java》有感