对象导论

面向对象程序设计(Object-oriented Programming,OOP),本章内容以了解 OOP 为主。

抽象过程

人们所能够解决的问题的复杂性,直接取决于抽象对象的类型和质量。以下的五个特性表现了一种纯粹的面向对象程序设计方式:

  • 万物皆为对象
  • 程序是对象的集合,它们通过发送消息来告知彼此所要做的
  • 每个对象都有自己的由其他对象所构成的存储
  • 每个对象都拥有其类型
  • 某一特定类型的所有对象都可以接收同样的消息

一个对对象更加简洁的描述:对象具有状态、行为和标识。这意味着每一个对象都可以拥有内部数据(它们给出了该对象的状态)和方法(它们产生行为),并且每个对象都可以唯一地与其他对象区分开来(标识)。

每个对象都有一个接口

所有的对象都是唯一的,都属于定义了特性和行为的某个特定的类。一旦类被建立,就可以随心所欲地创建类的任意个对象,然后去操作它们。类决定接口,接口定义了对象所能满足的请求。

每个对象都提供服务

将对象想象成 服务提供者,通过创建提供一系列服务的对象来解决问题。以服务来划分对象,这可以提高对象的内聚性,也便于别人的理解与重用。

被隐藏的具体实现

将开发人员按照角色分为 类创建者(创建与实现类的程序员)和 客户端程序员(使用类的程序员)。这里 访问控制 是重要的:

  • 使得客户端程序员无法触及类的内部操作
  • 使得类创建者改变类的内部处理而不影响客户端程序员平常的使用

Java 以三个关键字来定义类的使用范围:publicprivateprotected

  • public :任何人都可以使用
  • private :除类创建者和类内部方法之外,任何人不可使用
  • protected :与 private 的差别在于,可以被继承的类使用。另外,protected 也提供包访问权限,也就是说,相同包内的其他类可以访问 protected 元素
  • 默认访问权限;即包访问权限,在包内可以被访问,在包外如同 private

复用具体实现

可以直接使用该类的对象,也可以将这个类的一个对象置于某个新的类中,即创建一个成员对象。使用现有的类合成新的类,这称为组合(composition),如果组合是动态发生的,则称之为聚合(aggregation)。组合经常被视为 has-a(拥有)关系。新类的成员对象,通常被声明为 private,使得新类的客户端程序员不能访问它们,这可以在不干扰现有客户端代码的情况下,修改成员。

继承

当继承现有类型(基类)时,也就创造了新的类型(导出类)。这个新的类型不仅包括基类的所有成员,而且更重要的是它复制了基类的接口。也就是说,所有可以发送给基类对象的消息,同时也可以发送给导出类对象。

有两种方法可以使基类与导出类产生差异:

  • 直接添加新方法
  • 改变现有基类的方法,即覆盖(overriding)

“是一个”与“像是一个”关系

导出类仅仅只覆盖基类的方法,这意味着导出类和基类是完全相同的类型,因为它们具备完全相同的接口。这种情况下,基类与导出类之间的关系称为 is-a(是一个)关系。

有时必须在导出类型中添加新的接口元素,也就是扩展了接口。这种情况称为 is-like-a(像是一个)关系。

伴随多态的可互换对象

在处理类型的层次结构时,经常想把一个对象不当作它所属的特定类型来对待,而是将其当作其基类的对象来对待,这使得人们可以编写出不依赖于特定类型的代码。

一个非面向对象编程的编译器产生的函数调用会引起所谓的 前期绑定。这么做意味着编译器将产生对一个具体函数名字的调用,而运行时将这个调用解析到将要被执行的代码的绝对地址。

面向对象程序设计语言使用了 后期绑定,即编译器确保被调用方法的存在,并对调用参数和返回值执行类型检查,但是并不知道将被执行的确切代码。

在 Java 里,主要使用一小段特殊的代码来替代绝对地址调用。这段代码使用在对象中存储的信息来计算方法体的地址,即根据这段代码的内容,每个对象都可以具有不同的行为表现。并且在 Java 中,这种动态绑定是默认的,不需要添加额外的关键字来实现多态。

把将导出类型看做是它的基类的过程称为 向上转型(upcasting)。一个面向对象程序肯定会在某处包含向上转型,因为这正是将自己从必须知道确切类型中解放出来的关键。

单根继承结构

所有的类最终都继承自单一的基类,Object。单根继承结构的好处:

  • 保证所有对象都具备某些功能。所有对象都可以很容易地在堆上创建,而参数传递也得到了极大的简化
  • 使垃圾回收器的实现变得容易。由于所有对象都保证具有其类型的信息,因此不会因无法确定对象的类型而陷入僵局

容器

当无法确定需要多少空间来创建对象时,可以先创建另一种对象类型。这种对象类型持有对其他对象的引用,称为容器,其在任何需要时都可以扩充自己以容纳置于其中的所有东西。在 Java 中例如,ListMapSet,以及诸如队列、树、堆栈等。

对容器有所选择的原因有两个:

  • 不同的容器提供了不同类型的接口和外部行为
  • 不同的容器对于某些操作具有不同的效率

由于容器只存储 Object,所以当将对象置入容器时,它必须被向上转型为 Object。在取回时,就要考虑如何将它变回先前置入容器时的对象。创建容器,使其知道自己所保存的对象的类型,从而不需要向下转型以及消除犯错误的可能,这种解决方案称为参数化类型机制。例如一个存储 StringArrayList

1
ArrayList<String> strings = new ArrayList<String>();

对象的创建和生命周期

有两种方式:

  • 对象的存储空间和生命周期在编写程序时确定,将对象置于堆栈或静态存储区域内实现,较为高效
  • 堆(heap)的内存池内动态地创建对象,存储空间在运行时被动态管理,较为灵活

Java 完全采用了 动态内存分配 的方式,每当想要创建新对象时,就要使用 new 关键字来构建此对象的动态实例。

Java 提供了被称为 垃圾回收器 的机制,它可以自动发现对象何时不再被使用,并继而销毁它。

异常处理:处理错误

异常是一种对象,它从出错地点被抛出,并被专门设计用来处理特定类型错误的相应的异常处理器捕获。异常提供了一种从错误状况进行可靠恢复的途径,避免退出程序。这些都有助于编写出更健壮的程序。

并发编程

通常,线程只是一种为单一处理器分配执行时间的手段。但是如果操作系统支持多处理器,那么每个任务都可以被指派给不同的处理器,并且它们是在真正地并行执行。在语言级别上,多线程所带来的便利之一便是程序员不用再操心机器上是有多个处理器还是只有一个处理器。另外,还需要注意共享资源的问题。