读Java编程思想随笔の对象导论
1.1抽象过程
所有编程语言都提供抽象机制。可以认为,人们所能够解决问题的复杂性直接取决于抽象的类型和数量。所谓的“类型”是指“所抽象的是什么?”汇编语言是对底层机器的轻微抽象,而命令式语言(BASIC/C)则是对汇编语言的抽象。命令式语言相对于汇编语言有大幅改进,但是它们所作的抽象仍要求在解决问题时要基于计算机的结构,而不是基于所要解决问题的结构来考虑。程序员必须建立起在机器模型和实际待解决问题的模型之间的关联。建立起这种映射是费时费力的,而且这不属于编程语言所固有的功能,这使得程序难以编写,并且维护代价高昂。然后出现的另一种语言的建模方式就是针对待解决问题建模,例如LISP和APL,都选择考虑世界的某些特定视图(分别对应于“所有问题最终都是列表”或者“所有问题都是算法形式”)。这些方式对于它们所要解决的特定类型的问题都是不错的解决方案,但是一旦超出其特定领域,它们就力不从心。
面向对象的方式通过向程序员提供表示问题空间中的元素的工具而更进了一步。这种表示方式非常通用,使得程序员不会受限于任何特定类型的问题。我们将问题空间中的元素及其在解空间中的表示称为“对象”。这种思想的实质是:程序可以通过添加新类型的对象使自身适应于某个特定的问题。所以OOP允许根据问题来描述问题,而不是根据运行解决方案的计算机来描述问题。
1.2每个对象都有一个接口
面向对象程序设计的挑战之一,就是在问题空间的元素和解空间的对象之间创建一对一的映射。
1.3每个对象都提供服务
对象服务于程序,程序服务于客户。
当正在试图开发或理解一个程序设计时,最好的办法之一就是把对象想象为“服务提供者”,程序本身将向用户提供服务,它将通过调用其他对象提供的服务来实现这一目的,你的目标就是创建能够提供理想的服务来解决问题的一系列对象。
1.4被隐藏的具体实现
将程序开发人员按角色分为类创建者(那些创建新数据类型的程序员)和客户端程序员(那些在其应用中使用数据类型的类消费者)是大有裨益的。客户端程序员的目标是收集各种用于快速实现应用开发的类。类创建者的目标是构建类,这种类只向客户端程序员暴露必需的部分,而隐藏其他部分。为什么这么做呢?因为如果加以隐藏,那么客户端程序员将不能够访问它,这意味着类创建者可以任意修改被隐藏部分,而不用担心对其他任何人造成影响。被隐藏部分通常代表薄弱部分,它们很容易被粗心的或不知内情的客户端程序员所毁坏,因此将实现隐藏起来可以减少程序的bug。
访问控制的第一个存在的原因是让客户端程序员无法触及它们不应该触及的部分;
访问控制的第二个存在的原因是允许类设计者可以改变类内部的工作方式而不用担心会影响到客户端程序员。
1.5复用的具体体现
最简单的复用某个类的方式就是直接使用该类的一个对象,此外也可以将那个类的对象置于某个新类当中。我们称其为“创建一个成员变量”。新的类可以由任意对象、任意类型的其他对象以任意可以实现新的类中想要的功能的方式所组成。因为是在使用现有的类合成新的类,所以这种概念也叫组合。如果组合是动态发生的,那么它通常被称为聚合。
1.6继承
当继承现有类型时,也就创造了新的类型。这个新的类型不仅包含现有对象的所有成员,而且更重要的是它复制了基类的接口。也就是说,所以可以发送给基类对象的消息同时也可以发送给导出类对象。
有两种方法可以使导出类和基类产生差异,第一种非常直接,直接在导出类中添加新方法;第二种也是更重要的一种使导出类和基类之间产生差异的方法是改变现有基类方法的行为,这被称为覆盖。
1.7伴随多态的可互换对象
在试图将导出类型的对象当作泛化基类型对象看待时,仍然存在一个问题。如果某个方法要让泛化几何形状绘制自己,让泛化交通工具行驶,或者让泛化的鸟类移动,那么编译器在编译时是不可能知道哪一段代码将被执行。这就是关键所在:当发送这样的消息时,程序员并不想知道哪一段代码将被执行。
如果不需要知道哪一段代码将被执行,那么当添加新的子类型时,不需要更改调用它的方法,它就能够执行不同的代码。因此,编译器无法精确的了解哪一段代码将会被执行,那么它改怎么办呢?
这个问题的答案,也是面向对象程序设计的最重要的妙绝:编译器不可能产生传统意义上的函数调用。一个非面向对象编程的编译器产生的函数调用会引起所谓的前期绑定,这个术语你可能从未听说,可能从未想过函数调用的其他方式,这么做意味着编译器将产生对一个具体函数名字的调用,而运行时将这个调用解析到将要被执行代码的绝对地址。然而在OOP中,程序直到运行才知道代码的绝对地址,所以当消息发送到一个泛化对象时,必须采用其他机制。
为了解决这个问题,面向对象程序设计语言使用了后期绑定的概念。当向对象发送消息时,被调用的代码直到运行时才被确认。编译器确保被调用的方法存在,并对调用和函数和返回值执行类型检查,但是并不知道执行的具体代码。
为了执行后期绑定,Java使用一小段特殊代码来替代绝对地址调用。这段代码使用在对象中存储的具体信息来计算方法体的地址。这样根据这一小段代码的内容,每个对象都可以具有不同的行为表现。当向一个对象发送消息时,该对象就能够知道对这条消息应该做什么。
1.8单根继承结构
在Java中,所有类都继承一个终极类型--Object。在单根继承的结构中,所有类型都有一个共用的接口,所以它们归根到底是相同的基本类型。单根继承结构保证所有对象都具备某些功能。因此你知道,在你的系统中可以在每个对象执行某些基本操作。所有对象都可以很容易的在堆上创建,而参数传递也得到了极大的简化。单根继承结构是垃圾回收器的实现变得容易的多,而垃圾回收期正是Java相对C++的重要改进之一。
1.9容器
通常来说,如果不知道在解决某个特定问题时需要多少个对象,或者它们将存货多久,那么就不可能知道如何存储这些对象。如何才能知道需要多少空间来创建这些对象呢?答案是你不可能知道,因为这类信息只有在运行时才能获得,此时容器的概念慢慢浮出水面。
从设计的观点来看,真正需要的只是一个可以被操作,从而解决问题的序列。如果单一类型的容器可以满足所有需要,那么就没有理由设计不同类型的序列了。然而还是需要你对容器有所选择,这有两个原因:一,不同容器提供不同类型的接口和外部行为。堆栈相比于队列就有不同的接口和行为,也不同于集合的接口和行为;二,不同的容器对于不同的操作具有不同的效率。
1.9.1参数化类型
泛型。
1.10对象的创建和生命周期
在使用对象时,最关键的问题之一便是它们的生成和销毁方式。每个对象为了生存都需要资源,尤其是内存。当我们不再需要一个对象时,它必须被清理掉,使其被占有的资源可以被释放和重用。
对象的数据存于何处?怎样控制对象的生命周期?C++认为效率控制是最重要的议题,所以给程序员提供了选择的权利。为了追求最大的执行速度,对象存储空间和生命周期可以在编写程序时确认,这可以将对象置于堆栈或静态存储区域来实现。这种方式将存储空间分配和释放置于优先考虑的位置,某些情况下这些控制非常有价值。但是也牺牲了灵活性,因为在编写程序时要知道对象的确切数量、生命周期和类型。
第二种方式是在被称为堆的内存池中动态创建对象。在这种方式中,直到运行时才知道需要多少对象,它们的生命周期如何,以及它们的具体类型时什么,这些答案只能在程序运行时相关带啊被执行到那一刻才能确定。如果需要一个新对象,可以在需要的时刻直接在堆中创建。因为存储空间是在运行时被动态管理的,所以需要大量的时间来分配存储空间,这可能要远远大于在堆栈中创建对象的时间。在堆栈中创建存储空间和释放存储空间通常各需要一条汇编指令即可,分别对应将栈顶指针向下移动和将栈顶指针向上移动。
Java完全采用了动态内存分配方式。每当想要创建新对象时,就要使用new关键字来构建此对象的动态实例。
Java垃圾回收器被设计用来处理内存释放问题。垃圾回收器知道对象何时不再被使用,并自动释放对象占有的内存。这一点同所有对象都是继承自单根基类Object以及只能以一种方式创建对象这两个特性结合起来,使得Java编程相对于C++来说要容易的多。
1.11异常处理
1.12并发编程
多线程所带来的便利之一是程序员不用再操心机器上是有多个处理器还是一个处理器。由于程序在逻辑上被分为线程,所以机器拥有多个处理器,那么程序不需要特殊调整也能执行的更快。
并发看起来简单,但是有一个隐患:共享资源。如果有多个执行任务要访问同一资源,那么就会出现问题。正确的过程应该是:某个任务锁定某项资源,完成其任务,然后释放资源锁,使其他任务可以使用这项资源。
1.13Java和Internet