Thinking in Java_1

第1章 对象导论

1.1 抽象过程

1. 如何理解抽象过程

所有编程语言都提供抽象机制,可以认为,人们所能够解决的问题的复杂性直接取决于抽象的类型和质量。OOP是允许根据问题来描述问题(面向对象),而不是根据运行解决方案的计算机来描述问题(面向过程)

2. Java语言所基于的五个基本特性

这些特性表现了一种纯粹的面向对象程序设计方式,而 Booch 对对象提出了一个更加简洁的描述:对象具有状态、行为和标识。这意味着每个对象都可以拥有内部数据(它们给出了该对象的状态)和方法(它们产生的行为),并且对每个对每个对象都可以唯一地与其他对象区分开来,具体来说就是每个对象在内存中都有一个唯一的地址。

(1)万物皆为对象。将对象视为奇特的变量,它可以存储数据,除此之外,你还可以要去它在自身上执行操作。

(2)程序是对象的集合,它们通过发送消息来告知彼此所要做的。要想请求一个对象,就必须对该对象发送一条消息。更具体的说,可以把消息想像为对某个特定对象的方法的调用请求。

(3)每个对象都要自己的有其他对象所构成的存储。换句话说,可以通过创建包含现有对象的包的方式来创建新类型的对象。因此,可以在程序中构建复杂的体系,同时将其复杂性隐藏在对象的简单性背后。

(4)每个对象都拥有其类型。每个对象都是某个类(class)的一个实例,每个类最重要的区别于其他类的特性就是"可以发送什么样的消息给它"。

(5)某一特定类型的所有对象都可以接收同样的消息。就像 “几何形” 和 “圆形”的例子。因为 “圆形” 类型的对象同时也是 “几何形” 类型的对象,所以一个 “圆形” 对象必定能够接受发送给 “几何形” 对象的消息。这种可代替性是OOP中最强有力的概念之一。

1.2 每个对象都有一个接口

1.什么是对象的接口

接口确定了对某一特定对象所能发出的请求。但是,在程序中必须有满足这些请求的代码。这些代码与隐藏的数据一起构成了实现(用interface来定义对象的接口,然后用implements 来实现这个接口)。

所有对象都是唯一的,但同时也是具有相同的特性和行为的对象所归属的类的一部分。创建抽象数据类型(类)是面向对象程序设计的基本概念之一。你可以创建某一类型的变量(按照面向对象的说法,称其为对象或实例),然后操作这些变量(称其为发送消息或请求;发送消息,对象就知道要做什么)。每个类的成员或元素都具有某种特性

1.3 每个对象都提供服务

将对象看做是服务的提供着它会有助于提高对象的内聚性。高内聚是软件的设计的基本质量要求之一,每个对象都有一个它所能提供服务的内聚的集合。在良好的面向对象设计中,每个对象都可以很好的完成一项任务,但是它并不试图做更多的事。

1.4 被隐藏的具体实现

1.为什么需要隐藏具体实现

将程序的开发人员按角色分为类创建者(那些创建新数据类型的程序员)和客户端程序员(那些在其应用中使用数据类型的类消费者)。客户端程序员的目标是收集各种用来实现快速应用开发的类。类创建者的目标是构建类,这种类向客户端程序员暴露必须的部分,而隐藏其他部分。访问控制的第一个存在原因就是:让客户端程序员无法触及他们不应该触及的部分——这些部分对数据类型的内部操作来说是必需的,但并不是用户解决特定问题所需的接口的一部分。这对客户端程序员来说其实是一项服务,因为他们可以很容易的地看出哪些东西对他们很重要,那些可以被忽略。访问控制的第二个存在原因:允许库设计者可以改变类内部的工作方式而不用担心会影响到客户端程序员。你可能为了减轻开发任务而以某种简单的方式实现了某个特定类,但稍后发现你必须改写它才能使其运行得更快。如果接口和实现可以清晰地分离并得以保护,那么你就可以轻而易举地完成这项工作。

2.Java的四个访问控制权限
Thinking in Java_1

1.5 复用具体实现

代码复用是面向对象程序设计语言所提供的最了不起的优点之一。最简单地复用某个类的方式就是直接使用该类的一个对象,此外也可以将那个类的一个对象置于某个新的类中。我们称其为“创建一个成员对象”。新的类可以由任意数量、任意类型的其他对象以任意可以实现新类中想要的功能的方式组成。因为是在使用现有的类合成新的类,所以这种概念被称为“组合”,如果组合是动态发生的,那么它通常被称为聚合。组合通常被视为 " has a " (拥有)关系,就像我们常说的 “汽车拥有引擎”。

组合带来了极大的灵活性。新类的成员对象通常被声明为 private ,使得使用新类的客户端程序员不能访问它们。这也使得你可以在不干扰现有客户端代码的情况下,修改这些成员。也可以在运行时修改这些成员对象,以实现动态修改程序的行为。但是继承是不具有这样的灵活性,因为编译器必须对通过继承而创建的类施加编译时的限制,所以在建立新类时,应该首先考虑组合,因为它更加简单灵活。如果采用这种方式,设计会变得更加清晰。一旦有了一些经验 之后,便能够看出必须使用继承的场合了。

1.6 继承

1.什么是继承

一个基类型包含其所有导出类型所共享的特性和行为。可以创建一个基类型来表示系统中某些对象的核心概念,从基类型中导出其他类型,来表示此核心被实现的各种不同方式。当继承现有类型时,也就创建了新的类型。这个新的类型不仅包括现有类型的所有成员(尽管private 成员被隐藏了起来, 并且不可访问)而且更重要的是它复制了基类的接口。也就是说,所有可以发送给基类对象的消息同时也可以发送给导出类对象。
由于通过发送给类的消息的类型可知类的类型,所以这也就意味着导出类与基类具有相同的类型。

2.如何区分对象的具体实现方法

由于基类和导出类具有相同的基础接口,所以伴随此接口的必定有某些具体实现。也就是说,当对象接收到特定消息时,必须有某些代码去执行。有两种方法可以使基类和导出类产生差异。

第一种方法是:直接在导出类中添加新方法。这些新方法并不是基类接口的一部分。这意味着基类不能直接满足你的所有需求,因此必需添加更多的方法。

第二种方法是:改变现有基类的方法的行为,这被称为覆盖那个方法。要想覆盖某个方法,可以直接在导出类中创建该方法的新定义即可。

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

如果继承只覆盖了基类的方法(不添加在基类中没有的方法),就意味着导出类和基类是完全相同的类型,因为它们具有完全相同的接口。结果可以用一个导出类对象来完全替代一个基类对象。这可以被视为纯粹替代。通常称之为替代原则。我们通常情况下基类和导出类的关系为 “ is a ” (是一个)关系。

有时必须在导出类型中添加新的接口元素,这样也就扩展了接口。这个新的类型仍然可以替代基类,但是这种替代并不完美,因为基类无法访问新添加的方法。这种情况我们描述为 “ is like a ” (像是一个)关系。

1.7 伴随多态的可互换对象

在处理类型的层次结构时,经常想把一个对象不当作它所属的特定类型来对待,而是将其当作其基类的对象来对待。这使得人们可以编写出不依赖特定类型的代码。这样的代码是不会受到添加新类型影响的,而且添加新类型是扩展一个面向对象程序以便处理新情况的最常用方式。通过导出新的子类型而轻松扩展设计的能力是对改动进行封装的基本方式之一。这种能力可以极大地改善我们的设计,同时也降低软件维护的代价。

一个非面向对象编程的编译器产生的函数调用会引起所谓的前期绑定。这么做意味着编译器将产生对一个具体函数名字的调用,而在运行时将这个调用解析到将要被执行的代码的绝对地址。在面向对象中,程序直到运行时才能确定代码的地址。面向对象程序设计语言使用了后期绑定的概念。当向对象发送消息时,被调用的代码直到运行时才能确定。编译器确定被调用方法的存在,并对调用参数和返回值执行类型检查,但是并不知道将被执行的确切代码。为了执行后期绑定,java 使用一小段特殊的代码来替代绝对地址调用。这段代码使用在对象中存储的信息来计算方法体的地址。这样,根据这一小段代码的内容,每一个对象都可以具有不同的行为表现。当向一个对象发送消息时,该对象就能够知道对这条消息该做什么。在java中,动态绑定是默认行为,不需要添加额外的关键字来实现多态。将导出类看做是它的基类的过程称为向上转型。“向上”这个词来源于继承图的典型布局方式:通常基类在顶部,而导出类在其下部散开。 因此,转型为一个基类就是继承图中向上移动,即“向上转型”。当向一个对象发送消息时,即使涉及向上转型,该对象也知道要执行什么样的正确行为。(不同类型,多种形态)

1.8 单根继承结构

在Java中,存在一个所有类都最终继承的一个类 “Object” ,它是一个单一的基类。在单根继承结构中的所有对象都具有一个共用的接口,所以它们归根到底都是相同的基本类型。单根继承结构保证所有对象都具备某些功能。因此你知道,在你的系统中你可以在每个对象上执行某些基本操作。所有对象都可以很容易的在堆上创建,而参数传递也得到了极大的简化。单根继承结构使垃圾回收器的实现变得容易得多,而垃圾回收器正是Java相对于C++的重要改进之一。由于所有对象都保证具有其类型的信息,因此不会因为无法确定对象的类型而陷入僵局。

1.9 容器

通常来说,如果不知道在解决某个特定问题时需要多少个对象,或者它们将存活多久,那么久不可能知道如何存储这些对象。容器通常也称为集合,在任何需要时都可以扩充自己以容纳你置于其中的所有东西。因此不需要知道将来会把多少个对象置于容器中,只需要创建一个容器对象,然后让它处理所有细节。在Java中,不同容器提供了不同类型的接口和外部行为。堆栈相比于队列久具有不同的接口和行为,也不同于集合和列表的接口和行为。它们之中的某种容器提供的解决方案可能比其他容器要灵活得多。不同容器对于某些操作具有不同的效率。比如List的ArrayList和LinkedList。它们都是具有相同接口和外部行动的简单序列,但是它们对某些的操作所花费的代价却有天壤之别。

容器还可以用来存储对象,容器存储的对象都是具有Java中的通用类型:Object。单根继承意味着所有东西都是Object类型(List),在存储对象的过程中,我们还用到了向下转型,把要存储的对象转为更加具体的类型,但是这种转型几乎是不安全的。然后,如果在容器中取出对象引用时,一般都需要程序员以某种方式记住对象的类型。

1.10 对象的创建和生命周期

1.什么是对象的什么周期

在使用对象时,最关键的问题之一便是它们的生成和销毁方式。每个对象为了生存都需要资源,尤其是内存。当我们不再需要一个对象时,它必须被清理掉,使其占有的资源可以被释放和重用。

2.如何管理对象的生命周期

第一种,为了追求最大的执行速度,对象的存储空间和生命周期可以再编写程序时确定,这可以通过将对象置于堆栈(它们有时被称为自动变量或限域变量)或静态存储区域来实现。这种方式将存储空间分配和释放置于优先考虑的位置,某些情况下这样控制非常有价值。但是,也牺牲了灵活性,因为必须在编写程序时知道对象确切数量,生命周期和类型。

第二种,是在堆的内存池中动态的创建对象。在这种方式中,直到运行时才知道需要多少对象,它们的生命周期如何,以及它们具体类型是什么。动态方式所带来的更大的灵活性正是解决一般化编程问题的要点所在。

C++认为效率控制是最重要的议题,所以给程序员提供了选择的权力,所采用的是第一种。
Java则完全采用了动态内存分配方式,每当想创建新对象时,就使用new关键字来构建此对象的动态实例。

对于允许在堆栈上创建对象的语言,编译器可以确定对象的存活时间,并且自动销毁它。然后,如果是在堆上创建对象,编译器就会对它的生命周期一无所知。像C++语言,必须通过编程的方式来确定何时销毁对象,而Java则提供了被称为 “垃圾回收器” 的机制,它可以自动发现对象何时不再被使用,并继而销毁它。

1.11 异常处理:处理错误

异常处理将错误直接置于编程语言中,有时甚至置于操作系统中。异常是一种对象,它从出错地点被 “抛出”,并被专门设计用来处理特定类型错误的相应的异常处理器 “捕获” 。值得注意的是,异常处理不是面向对象的特征——尽管在面向对象语言中异常常常被表示成一个对象。异常处理在面向对象语言出现之前就已经存在了。

1.12 并发编程

在计算机编程中有一个基本概念,就是同一时刻处理多个任务的思想。而其中有一个隐患:共享资源。如果有多个并行任务都有访问同一项资源,那么问题就会出现。例如,两个进程不能同时向一台打印机发送信息。因此,在使用共享资源时,需要在使用期间被锁定。整个过程是:某个任务锁定某项资源,完成其任务,然后释放资源锁,使其他任务可以使用这项资源。

1.13 Java与Internet

1.Web是什么
1.客户/服务器计算技术
客户/服务器系统的核心思想是:系统具有一个*信息存储池,用来存储某种数据,它通常存在于数据库中,你可以根据需要将它发给某些人员或机器集群。客户/服务器概念的关键在于信息存储池的位置集中于*,这使得它可以被修改,并且这些修改将被传播给信息的消费者。总之,信息存储池,用于发布信息的软件以及信息与软件所驻留的机器或机群被总称为服务器。
2.Web就是一台巨型服务器
Web实际上就是一个巨型客户/服务器系统,但是必须所有服务器和客户机都同时共存于同一个网络。
2.客户端编程
Web最初的 “服务器——浏览器” 设计是为了能够提供交互性的内容,但是其交互性完全由服务器提供。
但是像登录验证这样的问题,我们其实可以在客户端就可以进行判断,以保证客户端每次发送到服务器里的数据是符合的。客户端编程的问题是:它与通常意义上的编程十分不同,参数几乎相同,而平台却不同。
而客户端编程一般是通过脚本语言或插件来实现的,比如JavaScript……

3.服务器端编程
前面讲了,通过客户端编程,我们可以保证每次传输数据的合法性,从而服务器的负载将会减小。
在Java语言里,有一个基于Java的Web服务器,它让你用Java编写被称为servlet的程序来实现服务器端编程。(servlet的衍生物JSP)从而将请求的结构进行格式编排,使其成为一个HTML页面返回给客户端。

1.14 总结

OOP在程序设计时,能够很自然的假设,由此产生的Java程序比等价的过程型程序要复杂的多。但是你会感到很惊喜:编写良好的Java程序通常比过程型程序要简单的多,而且也易于理解。