第一章 对象导论
计算机革命起源于机器,因此,编程语言的产生也始于对机器的模仿。但计算机并非只是机器那么简单,面向对象程序(Object-oriented Programming, OOP)设计便是以计算机作为表达媒体的大趋势中的组成部分。
1.1 抽象过程
所有编程语言都提供抽象机制,人们所能够解决的问题的复杂性直接取决于抽象的类型和质量。
面向对象方式通过向程序员提供表示问题空间中的元素的工具而更进一步,这种表示方式非常通用,我们将问题空间中的元素以及在解空间中的表示称为“对象”。这种思想的实质是:程序可以通过添加新类型的对象,使自身适用于某个特定的问题。这是一种更灵活和更有力的语言抽象。
Alan KAy总结了第一个成功的面向对象语言,同时也是Java所基于的语言之一的Smalltalk的五个基本特性,这些特性表现了一种纯粹的面向对象程序设计方式:
1) 万物皆为对象。将对象视为奇特的变量,它可以存储数据,还可以要求它在自身上执行操作,理论上讲,你可以抽取带求解问题的任何概念化作构件。
2) 程序时对象的集合,它们通过发送消息来告知彼此要做的。要想请求一个对象,就必须给该对象发送一条消息,可以把消息想象为对某个特定对象方法的调用请求。
3) 每个对象都有自己的由其他对象所构成的存储。可以通过创建包含现有对象的包的方式来创建新类型的对象。因此,可以在程序中构件复杂的体系,同时将其复杂性隐藏在对象的简单性背后。
4) 每个对象都拥有其类型。每个对象都是某个类的一个实例。
5) 某一特定类型的所有对象都可以接收同样的消息。例如,“原形”类型的对象同时也是“几何形”类型的对象。这种可替代性是OOP中强有力的概念之一。
1.2 每个对象都有一个接口
在银行出纳员问题中,有出纳、客户、账户、交易、和货币单位等许多对象,在程序执行期间具有不同的状态而其他方面都相似的对象会被分组到对象的类中,这是关键字class的由来。
因为类描述了具有相同特性(数据元素)和行为(功能)的对象集合,所以一个类实际上就是一个数据类型。
每个对象都只能满足某些请求,这些请求由对象的接口所定义,决定接口的便是类型。
以电灯泡为例:
Light lt = new light(); Lt.on();
接口确定了对某一特定对象所能发出的请求。上例中,类的名称是Light,特定的Light对象的名称是lt,你可以向Light对象发出的请求是:打开,关闭,挑亮,调暗。
1.3 每个对象都提供服务
当你正在试图开发或理解一个程序设计时,最好的方法之一就是讲对象想象为“服务提供者”。程序本身向用户提供服务,它讲通过调用其他对象提供的服务来实现这一目的。你的目标就是去创建(或者在现有代码库中寻找)能够提供理想的服务来解决问题的一系列对象。
将对象看做是服务提供者还有一个附带好处:有助于提高对象的内聚性。高内聚是软件设计的基本质量要求之一,这意味着一个软件构件(一个对象、一个方法或者一个对象库)的各个方面组合得很好。人们在设计对象时所面临的一个问题是,将过多的功能都塞在一个对象里,但这些功能对于一个对象来说太多了,你需要的是多个对象。在良好的面向对象设计中,每个对象都可以很好地完成一项任务,但它不视图做更多的事。
将对象作为服务提供者看待是一件伟大的简化工具,这不仅在设计过程中非常有用,而且当其他人视图理解你的代码或重用某个对象时,如果他们看出这个对象所能提供的服务的价值,它会使调整对象以适应其设计的过程变得简单得多。
1.4 被隐藏的具体实现
将程序开发人员按照角色分为类创建者和客户端程序员。客户端程序员的目标是手机各种用来实现快速应用开发的类;类创建者的目标是构建类,这种类只向客户端程序员暴露必须的部分,而隐藏其他部分。加以隐藏之后,客户端程序员将不能访问这些隐藏的部分。被隐藏部分通常代表对象内部脆弱的部分,他们很容易被粗心或不知内情的客户端程序员所毁坏,因此隐藏起来可以减少程序bug。
Java用三个关键字在类的内部设定边界:public, private, protected。这些访问指定词决定了紧跟其后被定义的东西可以被谁使用。Public表示该元素对任何人都可用,private表示除类型创建者和类型内部方法之外的任何人都不能访问该元素,protected与private作用相当,差别仅在于继承的类可以访问protected成员,但不能访问private成员。
Java还有一种默认的访问权限,当没有使用任何访问指定词时,这种权限被称为包访问权限,在这种权限下,类可以访问在同一个包(库构件)中的其他类的成员,在包之外,这些成员如同指定了private一样。
1.5 复用具体实现
代码复用是面向对象程序设计语言所提供的最了不起的优点之一。
最简单的复用某个类的方式就是直接使用该类的一个对象,此外也可以将那个类的一个对象置于某个新的类中,我们称其为“创建一个成员对象”。因为是使用现有的类合成新的类,这种概念被称为组合(composition),如果组合是动态发生的,那么通常称为聚合(aggregation)。
组合带来了极大的灵活性。新类的成员对象通常被声明为private,是的使用新类的客户端程序员不能访问他们,这也使得你可以在不干扰现有客户端代码的情况下,修改这些成员。也可以在运行时修改这些成员对象,以实现动态修改程序的行为。
1.6 继承
对象这种观念,本身就是十分方便的工具,使得你可以通过概念将数据和功能封装到一起,因此可以对问题空间的观念给出恰当的表示,而不用受制于必须使用底层机器语言。这些概念用关键字class来表示,他们形成了编程语言中的基本单位。
通过继承我们可以达到,能够以现有的类为基础,复制他,然后通过添加和修改这个副本来创建新的类。
以经典的几何形为例,基类是几何形,每一个几何形都具有尺寸、颜色、位置等,同时每一个几何形都可以被绘制、擦除、移动和着色等。在此基础上,可以继承出具体的几何性质——圆形、正方形、三角形等,每一种都具有额外的特征和行为。某些行为可能并不相同,如计算几何形状的面积。类型层次结构同时体现了集合形状之间的相似性和差异性(如下图):
当继承现有类型时,也就创造了新的类型。这个新的类型不仅包括现有类型的所有成员,更重要的是它复制了基类的接口。也就是说,所有可以发送给基类对象的消息同时也可以发送给导出类对。由于通过发送给类的消息的类型可知类,也就意味着导出类与基类具有相同的类型。“一个圆形也就是一个几何形”,通过继承而产生的类型等价性是理解买你想对象程序设计方法内涵的重要门槛。
由于基类和导出类具有相同的基础接口,所有伴随此接口的必定有某些具体实现,也就是说,当对象接收到特定消息时,必须有代码去执行。如果只是简单地继承一个类而不做其他任何事,那么在基类接口中的方法会直接集成到导出类中,这意味着导出类的对象不仅与基类拥有相同的类型,而且还拥有相同的行为,这样做没有什么特别的意义。
有两种方法可以使基类与导出类产生差异。第一种方法非常直接:直接在导出类中添加新的方法。这些新方法不是基类的一部分,是因为基类不能满足你所有的需求,你必须自己添加更多的方法。如下左图所示。
第二种方法是更重要的一种,这种方法被称为覆盖(overriding),也就是基类的方法名不变,但是方法的内部代码在导出类中进行了重写。如上右图所示。
1.7 伴随多态的可互换对象
在处理类型的层次结构时,经常想把一个对象不当做他所属的特定类型来对待,而是将其当做其基类的对象来对待。如在几何形的例子中,方法操作都是泛化的形状,而不关心他们是圆形、正方形、三角形还是其他什么形状,所有的几何性质都可以被绘制、擦除、移动。
添加新类型是扩展一个面向对象程序以便处理新情况的最常用方式。例如从几何形中导出一个新的子类五角形,我们不需要修改处理泛化的几何形状的方法。这种能力可以极大地改善我们的设计,同时降低软件维护的代价。
如果用Java来编写一段简单的代码:
void doSomething(Shape shape){ shape.erase(); // ... shape.draw(); }
这个方法可以与任何的shape对象对话,如:
Circle circle = new Circle(); Triangle triangle = new Triangle(); Line line = new Line(); doSomething(circle); doSomething(triangle); doSomething(line);
对doSomething()的调用会自动地正确处理,而不用管对象的确切类型。当Circle对象被传入到预期接收的Shape对象方法中后,Circle对象向上转型(upcasting)为Shape对象,如下图所示:
在方法doSomething()中的这些代码并不是说“如果你是Circle类,请这么做;如果你是Square类,请那样做……”,而是“只要你是Shape类,你就可以这么做,但是要注意细节的正确性”。
1.8 单根继承结构
在OOP中,自C++面世以来就已变得非常瞩目的一个问题就是,是否所有的类最终都继承自单一的基类,在Java中,这个答案是YES,这个终极基类就是Object,事实上还包括除了C++意外的所有OOP语言都是如此。
单根继承结构保证所有对象都具备某些功能,因此在你的系统中你可以在每个对象上执行某些基本操作,所有对象都可以很容易地在堆上创建,而参数的传递也得到了极大的简化。单根继承机构使垃圾回收期的实现变得容易很多,而垃圾回收器正是Java和C++的重要改进之一。由于所有对象都保证具有其类型信息,因此不会因无法确定对象的乐行而陷入僵局。这对于系统级操作,如异常处理,显得尤其重要,给编程带来了更大的灵活性。
1.9 容器
容器可以在任何需要的时候扩充自己以容纳你置于其中的所有东西,因此不需要知道将来会把多少对象置于容器中,只需要创建一个容器对象,然后让它处理所有细节。
从设计的观点来看,真正西药的知识一个可以被操作,从而解决问题的序列。我们需要对容器有一定的选择,这有两个原因:第一,不同容器提供了不同类型的接口和外部行为,堆栈相比于队列就具有不同的接口和行为,也不同于集合和列表的接口和行为;第二,不同的容器对于某些操作具有不同的效率,如ArrayList和LinkedList。
要使用这样的容器,只需在其中置入对象引用,稍后还可以将他们取回。但是由于同期只存储Object,所以当将对象置入容器时,必须向上转型为Object,当把他取回时,需要将它恢复身份,也就是向下转型,由泛化的一个类型向下转为更具体的类型。我们知道向上转型是安全的,例如Circle是一种Shape类型,但是不知道某个Object是Circle还是Shape,所以除非确切知道所要处理的对象类型,否则向下转型是不安全的。
1.10 对象的创建和生命期
创建:Java完全采用了动态内存分配方式,每当想要创建新对象时,就要使用new关键字来构建此对象的动态实例。
生命周期:Java提供了被称为“垃圾回收器”的机制,它可以自动发现对象何时不再被使用,并继而销毁它。
1.11 异常处理:处理错误
异常处理将错误处理直接置于编程语言中,有时甚至置于操作系统中。异常是一种对象,它从出错地点被“抛出”,并被相应的异常处理器“捕获”。
异常处理是是与程序正常执行路径并行的,在错误发生时执行的另一条路径,所以它不会干扰正常的执行代码。
Java一开始就内置了异常处理,强制你必须使用它,它是唯一可接受的错误报告方式。如果没哟编写正确的处理异常的代码,那么就会得到一条编译时出错的信息,这种有保障的一致性有助于编写出更健壮的程序。
1.12 并发编程
在计算机编程中有一个基本概念,就是在同一时刻处理多个任务的思想。最初,程序员们用有关机器底层的知识来编写中断服务程序,主要是通过硬件中断来出发,但是难度太大,费时又费力。
在程序中,我们把这些彼此独立运行的部分称为线程,上述概念称为“并发”。通常线程知识一种为单一处理器分配执行时间的手段,但是如果操作系统支持多处理器,那么每个任务都可以指派给不同的处理器。
有关共享资源(资源占用)的问题,例如打印机,必须在使用期间被锁定。
Java的并发是内置于语言中的,Java SE5已经增添了大量额外的库支持。
1.13 Java与Internet
1.13.1 Web是什么
1.客户/服务器计算技术
客户/服务器系统的核心思想是:系统有一个中央信息存储池用来存储某种数据,它通常存在于数据库中,你可以根据需要将它分发给某些人员或机器集群。信息存储池、用于分发信息的软件以及信息与软件所驻留的机器或机群被总称为服务器。
2.Web就是一台巨型服务器
Web实际上就是一个巨型客户/服务器系统,但稍微差一点,因为所有的服务器和客户机都同时共存于一个网络中。
1.13.2 客户端编程
大多数运行Web浏览器的机器都是能够执行大型任务的强有力的引擎,在使用原始的静态HTML方式的情况下,他们只是闲在那里,等着服务器送来下一个页面。客户端编程意味着Web浏览器能用来执行任何他可以完成的工作,使得返回给用户的结果更加迅捷,使你的网站更加具有交互性。
1.插件
客户端编程所迈出的最重要一步就是插件(plug-in)的开发。插件对于客户端编程的价值在于:它允许专家级的程序员不经过浏览器生产厂商的许可,就可以开发某种语言扩展,并将它们添加到服务器中。因此,插件提供了一个“后门”,使得可以创建新的客户端编程语言。
2.脚本语言
插件引发了浏览器脚本语言的开发。通过使用某种脚本语言,你可以将客户端程序的源代码直接嵌入到HTML页面中,解释这种预言的插件在HTML页面被显示时自动激活。
在Web浏览器内部使用的脚本语言实际上总是被用来解决特定类型的问题,主要是用来创建更丰富、更有交互性的图形化用户界面(graphic user interface, GUI)。
3.Java
Java通过applet以及使用Java Web Start来进行客户端编程的。Java不仅是一种功能强大的、安全的、跨平台的、国际化的编程语言,而且它还在不断的被扩展,以提供更多的语言功能和类库,能够优雅地出来在传统编程语言中很难解决的问题,例如并发、数据库访问、网络编程、分布式计算。
4.备选方案
Flex使得我们在编程时无需担心浏览器相关性,对于客户端编程而言,这是一种值得考虑的备选方案。
5..NET和C#
.NET平台大致相当于Java虚拟机和Java类库,C#与Java也有类似之处。
6.Internet与Intranet
当Web技术仅限用于特定公司的信息网络时,它就被称为Intranet(企业内部网),Intranet比Internet提供更高的安全性,可以物理地控制公司内部服务器的访问。
1.13.3 服务器端编程
当你要加入一个团体或下订单的时候,可能想在数据库中注册自己的名字,这将涉及对数据库的修改,这些数据库请求必须通过服务器端的某些代码来处理,这就是所谓的服务器端编程。
过去,服务器端编程都是通过Perl、Python、C++或其他某种语言编写CGI程序实现的,但却造成了行刺以后更加复杂的系统,其中就包括Java的Web服务器,它让你用Java编写被称为servlet的程序来实现服务器端编程。