Java-面向对象

面向对象思想

概述

随着计算机技术的发展,我们想用计算机来解决越来越复杂的问题。问题越复杂,解决问题需要的抽象层往往越高。

编程语言的的发展,也顺应着这样的趋势。一开始的汇编语言只是对计算机硬件的简单抽象,C语言又对其进行了进一步的抽象,使其更加贴近问题而不是硬件;到了 Java,面向对象编程(OOP)的思想已近很成熟了,这是一种更加抽象的语言,对计算机硬件的表达更少,而对问题的描述更多。

认识面向对象

  • 什么是对象

对象就是一个存在的东西!

在面向过程的编程中,我们需要把一个问题转换成一系列的数据和操作数据的算法,同时我们还需要考虑这些数据和算法如何用计算机可以理解的方式表达出来,即遵照基础机器的表达方式。

对象的概念可为我们带来极大的便利。它在概念上允许我们将各式各样数据和功能封装到一起。这样便可恰当表达“问题空间”的概念,不用刻意遵照基础机器的表达方式。

  • 什么是面向对象的程序设计

面向对象编程,就是把一切问题的部分抽象成对象,用那些通用的特征和行为来描述他们,然后再处理这些对象之间的沟通,得到完整的问题描述,并通过给这些对象一些,信息让他们最终给出一个解。

  • “纯粹”的面向对象程序设计方法是什么样的?

    1. 所有东西都是对象。可将对象想象成一种新型变量;
    2. 程序是一大堆对象的组合;通过消息传递,各对象知道自己该做些什么。
    3. 每个对象都有自己的存储空间,可容纳其他对象。
    4. 每个对象都有一种类型。
    5. 同一类所有对象都能接收相同的消息。

对象的接口

所有对象,尽管各有特色,都属于某一系列对象的一部分,这些对象具有通用的特征和行为。

“接口”(Interface)就是把一种类型的对象的共同特征抽象出来,并将其表示。(大多数语言中我们使用关键字 class 来描述)

“类型”决定了接口,而“类”是那个接口的一种特殊实现方式。

接口建立的同时,也规定了可对一个特定的对象发出哪些请求。所以建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。

实现方案的隐藏

在面向对象的程序设计的过程中,我们时常需要控制对成员的访问

一方面,防止调用类的程序员接触到内部数据类型的设计思想。
另一方面,允许库设计人员修改内部结构,不用担心它会对客户程序员造成什么影响。

在我们的接口中,要实现这样的访问控制,把一部分设计细节隐藏起来,便需要一种机制来实现对访问的控制,在 Java 中这种控制用关键字 publicprivateprotected 以及暗示性的 friendly来实现:

  • “public”(公共)意味着后续的定义任何人均可使用。
  • “private”(私有)意味着除您自己、类型的创建者以及那个类型的内部函数成员,其他任何人都不能访问后续的定义信息。
  • “protected”(受保护的)与“private”相似,只是一个继承的类可访问受保护的成员,但不能访问私有成员。继承的问题不久就要谈到。
  • “friendly”(友好的)涉及“包装”或“封装”(Package)的概念——即Java用来构建库的方法。若某样东西是“友好的”,意味着它只能在这个包装的范围内使用(所以这一访问级别有时也叫作“包装访问”)。

方案的重复使用

创建并测试好一个类后,它应(从理想的角度)代表一类事物,是可以重复在很多地方使用的。

为重复使用一个类,最简单的办法是仅直接使用那个类的对象。同时也能将那个类的一个对象置入一个新类,这种类中置入类的方法成为“组织”或“包含”,比如“一辆车包含了一个变速箱”。

继承:重新使用接口

我们费尽心思做出一种数据类型后,假如不得不又新建一种类型,令其实现大致相同的功能,那会是一件非常令人灰心的事情。
但若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承”正是针对这个目标而设计的。

在 Java 语言中,继承是通过 extends 关键字实现的。

使用继承时,相当于创建了一个新的类,不过这个“克隆”类(正式名称叫作继承类或者子类)中一开始就包含了原始类(正式名称叫作基础类、超类或父类)中的所有成员(private会被隐藏起来,禁止访问)与接口。

于此同时,在继承过程中,若父类发生了变化,子类也会反映出这种变化。

在完成继承后,可向父类的对象发送的所有消息亦可原样发给子类的对象。如果不对子类做任何修改,子类对象接收到一条特定的消息后,也总有一个父类提供的“方法”能够执行。这意味着子类具有与父类相同的类型和行为!

然而,在绝大多数情况下,我们希望子类和父类有所区别(不然为什么要建立一个子类呢?),实现这个效果,我们有一下方法可用:

  • 为衍生类添加新函数(功能)

如果需要子类拥有父类没有的功能,只需要给子类增加新的方法(函数)就行了。

  • 改变(”改善”)基础类一个现有函数的行为(重载)

为改善一个函数,只需为衍生类的函数建立一个新定义即可。我们的目标是:“尽管使用的函数接口未变,但它的新版本具有不同的表现”。

多形对象的互换使用

1-2.gif

举个例子,我们构建一个 Shape 类,Shape 类拥有 draw(), erase(), move() 等方法;
然后我们在构建几个 Shape 的子类(继承自 Shape)的类:Circle, Line, Triangle
对每个子类,我们都会根据实际情况重载对应的方法。

这时,如果我们构建一个 doStuff 函数:

1
2
3
4
5
void doStuff(Shape s) {
s.erase();
// ...
s.draw();
}

它可以接收任何 Shape 包括其子类进行操作:

1
2
3
4
5
6
Circle c = new Circle();
Triangle t = new Triangle();
Line l = new Line();
doStuff(c);
doStuff(t);
doStuff(l);

在这里,我们把 Shape 的子类,当作 Shape 使用,这称为“上溯造型”,这样做常常可以避免检查一个对象到底是何种具体类型。

值得注意的是,即使 Circle、Line、Triangle 用不同的 erase、draw 的具体实现方式,doStuff 还是可以很好得完成工作。我们可以理解为 doStuff 只是在告诉 Shape :“你是一种几何形状,我知道你能将自己删掉,即erase();请自己采取那个行动,并自己去控制所有的细节吧”,而不是对 Circle 做某种操作,对 Triangle 做某种操作…

动态绑定

我们在使用上述代码时,不论我们传给 doStuff 的是那种具体类型,它总能调用我们想要的函数。

像这样将一条消息发给对象时,如果不知道对方对具体类型是什么,但采取的行动同样是正确的,这种情况称作“多形性”,实现“多形性”的办法叫做“动态绑定”。

对于动态绑定,编译器会自己处理细节,我们只需要知道会发生什么。

在 Java 中,我们无需使用关键字来实现 动态绑定,它将是自动的。

抽象的基础类和接口

基础类 只是为自己的衍生类提供一个接口的类(上溯造型成它),我们不希望实际创建这种类的实例对象。
这时,我们就可以把这个类声明成“抽象的”。

在 Java 中,我们使用 关键字 abstract 来说明一个类是抽象的。编译器将阻止创建抽象类的实例。

interface 关键字 将抽象类的概念延伸,它完全禁止所有函数的定义。


【注】这篇文章,是学习《Thinking in Java》的笔记与摘录整理而成的。

由于找到的这本书太老,加上一些翻译上的问题,所以有些地方的表述可能和现在的主流方式不同(如书中对“重载”、“多态”等词的表述😂)。

这部分主要学习那种思想,就不纠结这些遣词造句是的细节了。