Java-对象导论

来源:互联网 发布:matlab书籍 知乎 编辑:程序博客网 时间:2024/05/18 08:55

1.Java-对象导论

我们之所以将自然界分解,组织成各种概念,并按其含义分类,主要是因为我们是整个口语交流社会共同遵守的协定的参与者,这个协定以语言的形式固定下来… …除非赞成这个协定中规定的有关语言信息的组织和分类,否则我们根本无法交谈。—Benjamin Lee Whorf

计算机革命起源于机器,因此,编程语言的产生也始于对机器的模仿。

计算机是头脑延伸的工具,同时还是一种不同类型的表达媒体。

因此,这种工具看起来已经越来越不像机器,而更像我们头脑的一部分,以及一种如写作、绘画、雕刻、动画、电脑等一样的的表达形式。

面向对象程序设计(Object-oriented Programming, OOP)便是这种以计算机作为表达媒体的大趋势的组成部分。

1.1 抽象过程

所有编程语言都提供抽象机制。可以认为,人们所能够解决的问题的复杂性直接取决于抽象的类型和质量。

所谓的“类型”是指“所抽象的是什么?”

汇编语言是对底层机器的轻微抽象。

接着出现的许多所谓“命令式”语言(如:C语言等)都是对汇编语言的抽象。这些语言在汇编语言的基础上有了大幅改进,但是它们所作的主要抽象仍要求在解决问题时要基于计算机的结构,而不是基于所要解决的问题的结构来考虑。

程序员必须建立起在机器模型(位于“解空间”内,这是你对问题建模的地方,如计算机)和实际待解问题的模型(位于“问题空间”内,这是问题存在的地方,如一项业务)之间的关联。

另一种对机器建模的方式就是只针对待解决问题建模。早期的编程语言,如Lisp,都选择考虑世界的某些特定视图(分别对应于“所有问题最终都是列表”或者“所有问题都是算法形式的”)。这些方式对于它们所要解决的特定类型的问题都是不错的解决方案,但是一旦超出其特定领域,它们就力不从心了。

面向对象方式通过向程序员提供表示问题空间中的元素的工具而更进了一步。这种表示方式非常通用,使得程序员不会受限于任何特定类型的问题。

我们将问题空间中的元素及其在解空间中的表示称为“对象”。(你还需要一些无法类比为问题空间元素的对象。)

这种思想的实质是:程序可以通过添加新类型的对象使自身适用于某个特定问题。

因此,当你在阅读描述解决方案的代码的同时,也是在阅读问题的表述。相比我们之前所使用的面向过程语言,这是一种更灵活和更强有力的语言抽象。

所以,OOP允许根据问题来描述问题,而不是根据运行解决方案的计算机来描述问题。

但是它仍然与计算机有联系:每个对象看起来都有点像一台微型计算机—-它具有状态,还具有操作,用户可以要求执行这些操作。如果要对现实世界中的对象作类比,那么说它们都具有特性和行为。

面向对象语言的五个基本特性:

  1. 万物皆为对象。
  2. 程序是对象的集合,它们通过发送消息来告知彼此所要做的(调用方法)。
  3. 每个对象都有自己的由其他对象所构成的存储。
  4. 每个对象都拥有其类型(类)。
  5. 某一特定类型的所有对象都可以接收同样的消息。

1.2 每个对象都有一个接口

所有的对象都是唯一的,但同时也是具有相同的特性和行为的对象归属的类的一部分。

尽管我们在面向对象程序设计中实际上进行的是创建新的数据类型,但事实上所有的面向对象程序设计语言都使用class这个关键词来表示数据类型。

当看到类型一词时,可将其作为类来考虑,反之亦然。

因为,类描述了具有相同特性(数据元素)和行为(功能)的对象集合,所以一个类实际上就是一个数据类型。

面向对象技术的应用确实可以将大量的问题,很容易地分解为一个简单的解决方案。

面向对象程序设计的挑战之一,就是在问题空间的元素和解空间的对象之间创建一对一的映射。

每个对象都只能满足某些请求,这些请求由对象的接口(interface)所定义,决定接口的便是类型。

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

在类型中,每一个可能的请求都有一个方法与之相关联,当向对象发送请求时,与之相关联的方法就会被调用。

此过程通常被概括为:向某个对象“发送消息”(产生请求),这个对象便知道此消息的目的,然后执行对象的程序代码。

1.3 每个对象都提供服务

当正在试图开发或理解一个程序设计时,最好的方法之一就是将对象想像为“服务提供者”,程序本身将向用户提供服务,它将通过调用其他对象提供的服务来实现这一目的。

你的目标就是去创建(或者最好是在现有的代码库中寻找)能够提供理想的服务来解决问题的一系列对象。

着手从事这件事的一种方式就是问一下自己:“如果我可以将问题从表象中抽取出来,那么什么样的对象可以马上解决我的问题呢?”

对于那些并不存在的对象,它们看起来像什么样子?

它们能够提供哪些服务?

它们需要哪些对象才能履行它们的义务?

如果持续这样做,那么最终你会说“那个对象看起来很简单,可以坐下来写代码了”,或者会说“我肯定那个对象已经存在了”。这是将问题分解为对象集合的一种合理方式。

将对象看作是服务提供者还有一个附带的好处:它有助于提高对象的内聚性。

将对象作为服务提供者看待是一件伟大的简化工具,这不仅在设计过程中非常有用,而且当其他人试图理解你的代码或重用某个对象时,如果他们看出了这个对象所能提供的服务的价值,它会使调整对象以适应其设计的过程变得简单得多。

1.4 被隐藏的具体实现

将程序开发人员按角色分为类创建者(那些创建新数据类型的程序员)和客户端程序员(那些在其应用中使用数据类型的类消息者)是大有好处的。

客户端程序员的目标是收集各种用来实现快速应用开发的类。类的创建者的目标是构建类,这种类只向客户端程序员暴露必需的部分,而隐藏其它部分。被隐藏的部分通常代表对象内部脆弱的部分,它们很容易被粗心的或不知内情的客户端程序员所毁坏,因此将实现隐藏起来可以减少程序bug。

在任何相互关系中,具有关系所涉及的各方都遵守的边界是十分重要的事情。

访问控制的第一个存在原因就是:让客户端程序员无法触及他们不应该触及的部分—-这部分对数据类型的内部操作是必需的,但并不是用户解决特定问题所需的接口部分。

这对客户端程序员来说其实是个好处,因为他们可以很容易得看出那些对他们来说是有用的,是重要的,而哪些是不重要的,可以忽略的。

访问控制的第二个存在原因就是:允许类库设计者可以改变类内部的工作方式而不用担心会影响到客户端程序员。

Java用了三个关键字在类的内部设定边界:

  1. public (公开)
  2. private (私有)
  3. protected (继承可见)
  4. 默认 (包可见)

1.5 复用具体实现

代码复用是面向对象程序设计语言所提供的最了不起的优点之一。

最简单的利用某个类的方式就是直接使用该类的一个对象,些外也可以将那个类的一个对象置于某个新的类中。

因为是使用现有的类合成新的类,所以这种概念被称为组合(composition),如果组合是动态发生的,那么它通常称为聚合(aggregation)。

组合经常被视为“has-a”(拥有)关系,就像我们常说的“汽车拥有引擎”一样。

新类的成员对象通常都被声明为private,使用使用新类的客户端程序员不能访问他们。

另一个实现复用的方式就是继承,新手会有这样的印象,处处都应该使用继承。实际上,在建立新类时,应该首先考虑组合,因为它更加简单灵活。如果采用这种方式,设计会变得更加清晰。

1.6 继承

继承要解决的问题是:当创建了一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。

类型不仅仅只是描述了作用于一个对象集合上的约束条件,同时还有与其他类型之间的关系。

两个类型可以有相同的特性和行为,但是其中一个类型可能比另一个含有更多的特性,并且可以处理更多的消息。

继承使用基类型和导出类型的概念表示了这种类型之间的相似性。

一个基类型包含其所有导出类型所共享的特性和行为。可以创建一个基类型来表示系统中某些对象的核心概念,从基类型中导出其他类型,来表示此核心可能被实现的各种不同方式。

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

  1. 直接在导出类中添加新方法。
  2. 改变现有基类的方法的行为(覆盖 overriding)。

覆盖:我正在使用相同的接口方法,但是我想在新类型中做些不同的事情。

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

如果导出类和基类是完全相同的类型,具有完全相同的接口。结果可以用一个导出类对象来完全替代一个基类对象。这可以被视为纯粹替代,通常称之为替代原则。

我们将这种情况下的基类与导出类之间的关系称为:is-a (是一个)关系。

有时必须在导出类型中添加新的接口元素,这样也就扩展了接口。这个新的类型仍然可以替代基类,但是这种替代并不完美,因为基类无法访问新添加的方法。

我们将这种情况下的基类与导出类之间的关系称为:is-like-a (像是一个)关系。

1.7 伴随多态的可互换对象

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

但是,在试图将导出类型的对象当作其泛化基类型对象来看待时(把圆形看作是几何形,把自行车看作是交通工具),仍然存在一个问题。如果某个方法要让泛化几何形状绘制自己、让泛化交通工具行驶,或者让泛化的鸟类移动,那么编译器在编译时是不可能知道应该执行哪一段代码的。这就是关键所在:当发送这样的消息时,程序员并不想知道哪一段代码将被执行;绘图方法可以被等同地应用于圆形、正文形、三角形,而对象会依据自身的具体类型来执行恰当的代码。

OOP中,程序直到运行时才能确定代码的地址,所以当消息发送到一个泛化对象时,必须采用其他机制。

为了解决这个问题,面向对象程序设计语言使用了后期绑定的概念。当向对象发送消息时,被调用的代码直到运行时才能确定。编译器确保被调用方法的存在,并对调用参数和返回值执行类型检查(无法提供此类保证的语言被称为是弱类型的),但是并不知道将被执行的确切代码。

为了执行后期绑定,Java使用一小段特殊的代码来替代绝对地址调用。这段代码使用在对象中存储的信息来计算方法体的地址。这样,根据这一小段代码的内容,每一个对象都可以具有不同的行为表现。当向一个对象发送消息时,该对象就能够知道对这条消息应该做些什么。

而在Java中,动态绑定是默认行为,不需要添加额外的关键字来实现多态。

把将导出类看做是它的基类的过程称为向上转型(upcasting)。

转型(cast)这个名称的灵感来自于模型铸造的塑模动作,而向上(up)这个词来源于继承图的典型布局方式,通常基类在顶部,而导出类在其下部散开。因此,转型为一个基类就是在继承图中向上移动,即“向上转型”。

1.8 单根继承结构

在Java中,有一个终极基类,它就是Object。

单根继承的好处:

  1. 单根继承结构中,所有对象都具有一个共用接口,所以它们归根到底都是相同的基本类型。
  2. 单根继承结构保证所有对象都具备某些功能。
  3. 单根继承结构使垃圾回收器的实现变得容易得多,而垃圾回收器下在Java相对于C++的重要改进之一。

1.9 容器

要解决的问题:如果不知道在解决某个特定问题时需要多少个对象,或者它们将存活多久,那么就不可能知道如何存储这些对象。如何才能知道需要多少空间来创建这些对象呢?答案是你不可能知道,因为这类信息只有在运行时才能获得。

Java中使用一种解决方案来解决这类问题:创建另一种对象类型。这种新的对象类型持有对其它对象的引用。

这个对象类型通常被称为容器(也称为集合),这个新的对象可以在任何需要的时候扩充自己以容纳放置在其中的所有东西。因此不需要知道将来会有多少个对象置于容器中,只需要创建一个容器对象,然后让它处理所有细节。

从设计的观点来看,真正需要的只是一个可以被操作,从而解决问题的序列。

如果单一类型的容器可以满足所有需求,那么就没有理由设计不同种类的序列了。

然而还是需要对容器有所选择,这里有两个原因:

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

参数化类型

在Java 5出现之前,容器存储的对象都只具有Java中的通用类型:Object。

单根继承结构意味着的有东西都是Object类型,所以可以存储Object的容器可以存储任何东西。这使得容器很容易被复用。

要使用这样的容器,只需在其中置入对象引用,稍后还可以将它们取回。但是由于容器只存储Object,所以当将对象引用置入容器时,它必须被向上转型为Object,因此它会丢失其身份。当把它取回时,就获取了一个对Object对象的引用,而不是对置入时的那个类型的对象的引用。

所以,怎样才能将它变回先前置入容器中时的具有实用接口的对象呢?

这里再度用到了转型,但这一次不是向继承结构的上层转型为一个更泛化的类型,而是向下转型为更具体的类型。这种转型的方式称为向下转型。

我们知道,向上转型是安全的,但除非确切知道所要处理的对象的类型,否则向下转型几乎是不安全的。

然而,向下转型并非一定是危险的,因为如果向下转型为错误类型,就会等得到被称为异常的运行时错误。尽管如此,当容器中取出对象引用时,还是必须要以某种方式记住这些对象究竟是什么类型,这样才能执行正确的向下转型。

向下转型和运行时的检查需要额外的程序运行时间,也需要程序员付出更多的心血。那么创建这样的容器,它知道自己所保存的对象的类型,从而不需要向下转型以及消除犯错误的可能,这样不是更有意义吗?

这种解决方案被称为参数化类型机制。参数化类型就是一个编译器可以自动定制于特定类型上的类。

泛型

Java 5的重大变化之一就是增加了参数化类型,在Java中它称为泛型。

一对尖括号,中间包含类型信息,通过这些特征就可以识别对泛型的使用。

1.10 对象的创建和生命周期

问题

在使用对象时,最关键的问题之一便是它们的生成和销毁方式。

每个对象为了生存都需要资源,尤其是内存。当我们不再需要一个对象时,它必须被清理掉,使其占有的资源可以被释放和重用。

对象的数据位于何处?

怎样控制对象的生命周期?

C++的对象创建方案

C++认为效率控制是最重要的议题,所以给程序员提供了选择的权力。为了追求最大的执行速度,对象的存储空间和生命周期可以在编写程序时确定,这可以通过将对象置于堆栈或静态存储区来实现。

这种方式将存储空间分配和释放置于优先考虑的位置,某些情况下这样控制非常有价值。但是也牺牲了灵活性,因为必须在编写程序时知道对象确切的数量,生命周期和类型。

Java的对象创建方案

在被称为堆(heap)的内存池中动态地创建对象。

这种方式中,直到运行时才知道需要多少对象,它们的生命周期如何,以及它们的具体类型是什么。这些问题的答案只能在程序运行时,相关代码被执行到的那一刻才能确定。

如果需要一个新对象,可以在需要的时刻直接在堆中创建。因为存储空间是在运行时被动态管理的,所以需要大量的时间在堆中分配存储空间,这可能要远远大于堆栈中创建存储空间的时间。

栈空间创建对象

在堆栈中创建存储空间和释放存储空间通常各需要一条汇编指定即可,分别对象将栈顶指针向下移动和将栈顶指针向上移动。

堆空间创建对象

创建堆存储空间的时间依赖于存储机制的设计。

动态方式有这样一个一般性的逻辑假设:对象趋向于变得复杂,所以查找和释放存储空间的开销不会对对象的创建造成重大冲击。动态方式所带来的更大的灵活性正是解决一般化编程问题的要点所在。

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

生命周期

对象允许在堆栈上创建对象的语言,编译器可以确定对象存活的时间,并可以自动销毁它。

然而,如果是在堆上创建的对象,编译器就会对它的生命周期一无所知。

就像C++这样的语言中,必须通过编程方式来确定何时销毁对象,这可能会因为不能正确处理而导致内存泄漏(这在C++中是常见问题)。

Java提供了被称为“垃圾回收器”的机制,这可以自动发现对象何时不再被使用,并继而销毁它。垃圾回收器非常有用,因为它减少了所必须考虑的议题和必须编写的代码。更重要的是,垃圾回收器提供了更高层的保障,可以避免暗藏的内存泄漏问题,这个问题已经使得许多C++项目失败了。

Java的垃圾回收器被设计用来处理内存释放问题(尽管它不包括清理对象的其它方面)。

垃圾回收器“知道”对象何时不再被使用,并自动释放对象占用的内存。这一点同所有对象都是继承自单根基类Object以及只能以一种方式创建对象(在堆上创建)这两个特性结合起来,使得用Java编程的过程较之用C++编程要简单得多,所要做出的决定和要克服的出门在障碍也要少得多。

1.11 异常处理:处理错误

问题

大多数错误处理机制的主要问题在于,它们都依赖于程序员自身的警惕性,这种警惕性的来源于一种共同的约定,而不是编程语言所强制的。如果程序员不够警惕—通常是因为他们太忙,这些机制就很容易被忽视。

异常处理将错误处理直接置于编程语言中,有时甚至置于操作系统中。

异常是一种对象,它从错误地点“抛出”,并被专门设计用来处理特定类型错误的相应的异常处理器“捕获”。

异常处理就像是程序正常执行路径并行的、在错误发生时执行的另一条路径。因为它是另一条完全分离的执行路径,所以它不会干扰正常的执行代码。

异常提供了一种从错误状况进行可靠恢复的途径。现在不再是只能退出程序,你可以经常进行校正,并恢复程序的执行,这些都有助于编写出更健壮的程序。

Java的异常处理在众多的编程语言中格外引人注目,因为Java一开始就内置了异常处理,而且强制你必须使用它。

异常处理不是面向对象的特征—尽管在面向对象语言中异常常被表示成一个对象。

异常处理在面向对象语言出现之前就已经存在了。

1.12 并发编程

在计算机编程中有一个概念,就是在同一时刻处理多个任务的思想。

有时中断对于处理时间性强的任务是必需的,但是对于大量的其他问题,我们只是想把问题切分成多个可独立运行的部分(任务),从而提高程序的响应能力。在程序中,这些彼此独立运行的部分称之为线程,上述概念被称为“并发”。

通常,线程只是一种为单一处理器分配执行时间的手段。但是如果操作系统支持多处理器,那么每个任务都可以被指派给不同的处理器,并且它们是在真正地并行执行。在语言级别上,多线程所带来的便利之一便是程序员不用再操心机器上是有多个处理器还是只有一个处理器。由于程序在逻辑上被分为线程,所以如果机器拥有多个处理器,那么程序不需要特殊调整也能执行得更快。

所有这些都使得并发看起来相当简单,但是有一个隐患:共享资源。如果有多个并行任务都要访问同一项资源,那么就会出问题。

1.13 Java与Internet

尽管Java对于解决传统的单机程序设计问题非常有用,但更重要的是,它解决了在互联网上的程序设计问题。

Web是什么

1.客户、服务器计算技术

客户、服务器系统的核心思想是:系统具有一个中央信息存储池,用来存储某种数据,它通常保存在数据库中,你可以根据需要将它分发给某些人员或机器集群。

信息存储池,用于分发信息的软件以及信息与软件所驻留的机器或机群被称为服务器。

驻留在用户机器上的软件与服务器进行通信,以获取信息、处理信息,然后将它们显示在被称为客户机的用户机器上。

2.Web就是一台巨型的服务器

Web实际上就是一个巨型客户、服务器系统。

最初只有一种很简单的单向过程:你对某个服务器产生一个请求,然后它返回给你一个文件,你的机器(也就是客户机)上的浏览器软件根据本地机器的格式来解读这个文件。

但是很快,人们就希望系统能做的更多,而不仅仅是从服务器传递回页面。人们希望实现完整的客户、服务器能力,使得客户可以将信息反馈给服务器。

为了解决这个问题,人们采用了各种不同的方法。其中就有,通过引入在客户端浏览器中运行程序的能力就可以解决。这被称为“客户端编程”。

客户端编程

基本的HTML包含简单的数据收集机制:文本输入框、复选框、单选框、列表和下拉式列表以及按钮–它只能被编程来实现提交表单上的数据给服务器。这种提交动作通过所有的Web服务器都提供的通用网关接口(common gateway interface, CGI)传递。提交内容会告诉CGI应该如何处理它。最常见的动作就是运行一个在服务器中常被命名为“cgi-bin”的目录下的一个程序。

你肯定经历过对Web输入表单进行数据验证的过程:

  1. 你按下网页上的提交按钮,
  2. 数据被发送回服务器
  3. 服务器启动一个CGI程序来检查、发现错误,并将错误组装为一个HTML页面
  4. 然后将这个页面发回给你。
  5. 之后你必须回退一个页面,然后再重新尝试。

这个过程不仅很慢,而且不太优雅。

问题的解决方法就是客户端编程。

大多数运行Web浏览器的机器都是能够执行大型任务的强有力的引擎。

插件

客户端编程所迈出的最重要的一步就是插件(plug-in)的开发。

脚本语言

插件引发了浏览器脚本语言(scripting language)的开发。

服务器端编程

服务端程序都是通过使用Perl、Python、C++或其它某种语言编写CGI程序而实现的,但却造成了从此之后更加复杂的系统。其中就包括基于Java的Web服务器,它让你用Java编写称为servlet的程序来实现服务端编程。

1.14 总结

面向过程的语言看起来就是:数据定义+函数调用。

这些程序总是容易把人搞糊涂,因为它们使用的表示术语更加面向计算机而不是你要解决的问题。

OOP在面向过程的语言的基础上,添加了许多概念。你看到的只是有关下面两部分内容的定义:用来表示问题空间概念的对象(而不是有关计算机表示方式的相关内容),以及发送给这些对象的用来表示在些空间内的行为的消息。

面向对象程序设计带给人们的好处之一就是:对于设计良好的程序,通过阅读它就可以很容易地理解其代码。

0 0
原创粉丝点击