一.Java对象导论——Java编程思想第4版学习笔记

1.1 抽象过程

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

Alan Kay曾经总结了面向对象语言的5个基本特性:

1)万物皆为对象。

2)程序是对象的集合,它们通过发送消息来告知彼此所要做的。

3)每个对象都有自己的由其他对象所构成的存储。

4)每个对象都拥有其类型。

5)某一个特定类型的所有对象都可以接受同样的消息。

Booch对对象提出了一个更加简洁的描述:对象具有状态、行为和标识

1.2 每个对象都有一个接口

创建抽象数据类型(类)是面向对象程序设计的基本概念之一。抽象数据类型的运行方式与内置(built-in)类型几乎完全一致:你可以创建某一类型的变量(对象或实例),然后操作这些变量(发送消息或请求)。每个类的成员或元素都具有某种共性,同时,每个成员都有其自身的状态,每一个对象都属于定义了特性和行为的某个特定的类。

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

怎样才能获得有用的对象呢?必须有某种方式产生对对象的请求,使对象完成各种任务,如完成一笔交易、在屏幕上画图、打开开关等。每个对象都只能满足某些请求,这些请求由对象的接口(interface)来定义,决定接口的便是类型。以电灯泡为例:

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

1.3 每个对象都提供服务

将对象想象为“服务提供者”,程序本身将向用户提供服务,它将通过调用其他对象提供的服务来实现这一目的。你的目标就是去创建(或者从现有代码库中寻找)能够提供理想的服务来解决问题的一系列对象。

将对象看作是服务提供者还有一个附带的好处:它有助于提高对象的内聚性。高内聚是软件设计的基本质量要求之一:这意味着一个软件构件的各个方面“组合”得很好。在良好的面向对象设计中,每个对象都可以很好地完成一项任务,但是它并不试图做更多的事。

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

1.4 被隐藏的具体实现

将程序开发人员按照角色分为类创建者(创建新数据类型的程序员)和客户端程序员(在其应用中使用数据类型的类消费者)是大有裨益的。客户端程序员的目标是收集各种用来实现快速应用开发的类。类创建者的目标是构建类,这种类只向客户端程序员暴露必需的部分,而隐藏其他部分。被隐藏的部分通常代表对象内部脆弱的部分,它们很容易被粗心的或不知内情的客户端程序员所毁坏,因此将实现隐藏起来可以减少程序bug。

访问控制的第一个存在原因是让客户端程序员无法触及他们不应该触及的部分——这些部分对数据类型的内部操作来说是必需的,但并不是用户解决特定问题所需的接口的一部分。这对客户端程序员来说其实是一项服务,因为他们可以很容易地看出那些东西对他们来说很重要,而哪些东西可以忽略。

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

Java用三个关键字在类的内部设定边界:public、private、protected。这些访问指定词(access specifier)决定了紧跟其后被定义的东西可以被谁使用。public表示紧随其后的元素对任何人都是可用的,而private这个关键字表示除类型创建者和类型的内部方法之外的任何人都不能访问的元素。private就像你与客户端程序员之间的一堵砖墙,如果有人试图访问private成员,就会在编译时得到错误信息。protected关键字与private作用相当,差别仅在于继承的类可以访问protected成员,但是不能访问private成员。

Java还有一种默认的访问权限,当没有使用前面提到的任何访问指定词时,它将发挥作用。这种权限通常被称为包访问权限,因为在这种权限下,类可以访问在同一个包(库构件)中的其他类的成员,但是在包之外,这些成员如同指定了private一样。

1.5 复用具体实现

一旦类被创建并被测试完,那么它就应该代表一个有用的代码单元。代码复用是面向对象程序设计语言所提供的最了不起的优点之一。

最简单地复用某个类的方式就是直接使用该类的一个对象,此外也可以将那个类的一个对象置于某个新的类中。我们称其为“创建一个成员对象”。新的类可以由任意数量、任意类型的其他对象以任意可以实现新的类中想要的功能的方式所组成。因为是在使用现有的类合成新的类,所以这种概念被称为组合(composition),如果组合是动态发生的,那么它通常被称为聚合(aggregation)。组合经常被视为“has-a”(拥有)关系,就像我们常说的“汽车拥有引擎”一样。

组合带来了极大的灵活性。新类的成员对象通常都被声明为private,使得使用新类的客户端程序员不能访问它们。这也使得你可以在不干扰现有客户端代码的情况下,修改这些成员。也可以在运行时修改这些成员对象,以实现动态修改程序的行为。下面将要讨论的继承并不具备这样的灵活性,因为编译器必须对通过继承而创建的类施加编译时的限制。

由于继承在面向对象程序设计中如此重要,所以它经常被高度强调,于是程序员新手就会有这样的印象:处处都应该使用继承。这会导致难以使用并过分复杂的设计。实际上,在建立新类时,应该首先考虑组合,因为它更加简单灵活。如果采用这种方式,设计会变得更加清晰。一旦有了一些经验之后,便能够看出必须使用继承的场合了。

1.6 继承

对象这种观念,本身就是十分方便的工具,使得你可以通过概念将数据和功能封装到一起,因此可以对问题空间的观念给出恰当的表示,而不用受制于必须使用底层机器语言。这些概念用关键字class来表示,它们形成了变成语言中的基本单位。

遗憾的是,这样做还是有很多麻烦:在创建了一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。如果我们能够以现有的类为基础,复制它,然后通过添加和修改这个副本来创建新类那就要好多了。通过继承便可以达到这样的效果,不过也有例外,当源类(基类、超类或父类)发生变动时,被修改的“副本”(导出类、继承类或子类)也会反映出这些变动。

类型不仅仅只是描述了作用于一个对象集合上的约束条件,同时还有与其他类型之间的关系。两个类型可以有相同的特性和行为,但是其中一个类型可能比另一个含有更多的特性,并且可以处理更多的消息(或以不同的方式来处理消息)。继承使用基类型和导出类型的概念表示了这种类型之间的相似性。一个基类型包含所有导出类型所共享的特性和行为。可以创建一个基类型来表示系统中某些对象的核心概念,从基类型中导出其他类型,来表示此核心可以被实现的各种不同方式。

当继承现有类型时,也就创造了新的类型。这个新的类型不仅包括现有类型的所有成员(尽管private的成员被隐藏了起来,并且不可访问),而且更重要的是它复制了基类的接口。也就是说,所有可以发送给基类对象的消息同时也可以发送给导出类对象。由于通过发送给类的消息的类型可知类的类型,所以这也就意味着导出类与基类具有相同的类型。通过继承而产生的类型等价性是理解面向对象程序设计方法内涵的重要门槛。

由于基类和导出类具有相同的基础接口,所以伴随此接口的必定有某些具体实现。如果只是简单地继承一个类而并不做其他任何事,那么在基类接口中的方法将会直接继承到导出类中,这样做没有什么特别意义。有两种方法可以使基类与导出类产生差异。第一种是直接在导出类中添加新方法,这些新方法并不是基类接口的一部分;第二种是改变现有基类的方法的行为,称为覆盖,即直接在导出类中创建该方法的新定义。

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

对于继承可能会引发某种争论“继承应该只覆盖基类的方法(而并不添加在基类中没有的新方法)吗?如果这样做,就意味着导出类和基类是完全相同的类型,因为它们具有完全相同的接口。结果可以用一个导出类对象来完全替代一个基类对象。这可以被视为纯粹替代,通常称之为替代原则。这种情况下的基类与导出类的之间的关系称为is-a(是一个)关系。判断是否继承,就是要确定是否可以用is-a来描述类之间的关系,并使之具有实际意义。

有时必需在导出类型中添加新的接口元素,这样也就扩展了接口。这个新的类型仍然可以替代基类,但是这种替代并不完美,因为基类无法访问新添加的方法。这张情况我们可以描述为is-like-a(像是一个)关系。新类型具有旧类型的接口,但是它还包含其他方法,所以不能说它们完全相同。

1.7 伴随多态的可互换对象

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

面向对象程序设计的最重要的妙诀是:编译器不可能产生传统意义上的函数调用。一个非面向对象编程的编译器产生的函数调用会引起所谓的前期绑定。这么做意味着编译器将产生对一个具体函数名字的调用,而运行时将这个调用解析到将要被执行的代码的绝对地址。然而在OOP中,程序直到运行时才能够确定代码的地址,所以当消息发送到一个泛化对象时,必须采用其他的机制。

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

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

在某些语言中,必须明确地声明希望某个方法具备后期绑定属性所带来的灵活性(C++是使用virtual关键字来实现的)。在这些语言中,方法在默认情况下不是动态绑定的。而在Java中,动态绑定是默认行为,不需要添加额外的关键字来实现多态。

来看看几何形状的例子。为了说明多态,我们要编写一段代码,它忽略类型的具体细节,仅仅和基类交互。这段代码和具体类型信息是分离的(decoupled),这样做使代码编写更为简单,也更易于理解。而且,如果通过继承机制添加一个新类型,例如Hexagon(六边形),所编写的代码对Shape(几何形)的新类型的处理与对已有类型的处理会同样出色。正因为如此,可以称这个程序是可扩展的。

如果用Java来编写一个方法:

void doSomething(Shape shape){
    shape.erase();
    // ...
    shape.draw();
}

这个方法可以与任何Shape对话,因此它是独立于任何它要绘制和擦除的对象的具体类型的。如果程序中其他部分用到了doSomething()方法:

Circle circle = new Circle();
Triangle triangle = new Triangle();
Line line = new Line();
doSomething(circle);
doSomething(triangle);
doSomething(line);
对doSomething()的调用会自动地正确处理,而不管对象的确切类型。

对于doSomething(circle); 当Circle被传入到预期接收Shape的方法中,究竟会发生什么。由于Circle可以被doSomething()看作是Shape,也就是说,doSomething()可以发送给Shape的任何消息,Circle都可以接收,那么,这么做是完全安全且合乎逻辑的。

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

让我们再看看doSomething()中的代码:

shape.erase();
// ...
shape.draw();
doSomething()的代码给人印象深刻之处在于,它总是做了该做的。调用Circle的draw()方法所执行的代码与调用Square()或Line的draw()方法所执行的代码是不同的,而且当draw()消息被发送给一个匿名的Shape时,也会基于该Shape的实际类型产生正确的行为。正是因为多态才使得事情总是能够被正确处理。编译器和运行系统会处理相关的细节,你需要马上知道的只是事情会发生,更重要的是怎样通过它来设计。当向一个对象发送消息时,即使涉及向上转型,该对象也知道要执行什么样的正确行为。

1.8 单根继承结构

在Java中,所有的类最终都继承自单一的基类。这个终极基类的名字就是Object。事实证明,单根继承结构带来了很多好处。

在单根继承结构中的所有对象都具有一个共用接口,所以它们归根到底都是相同的基本类型

单根继承结构保证所有对象都具备某些功能。因此你知道,在你的系统中你可以在每个对象上执行某些基本操作。所有对象都可以很容易地在堆上创建,而参数传递也得到了极大的简化。

单根继承结构使垃圾回收器的实现变得容易得多,而垃圾回收器正是Java相对C++的重要改进之一。由于所有对象都保证具有其类型信息,因此不会因无法确定对象的类型而陷入僵局。这对于系统级操作(如异常处理)显得尤其重要,并且给编程带来了更大的灵活性。

1.9 容器

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

幸运的是,好的OOP语言都有一组容器,它们作为开发包的一部分。在C++中容器是标准C++类库的一部分,经常被称为标准模板类库(Standard Template Library,STL)。Object Pascal在其可视化构件库(Visual Component Library,VCL)中有容器,Smalltalk提供了一个非常完备的容器集;Java在其标准类库中也包含有大量的容器。在某些类库中,一两个通用容器足够满足所有的需要;但是在其他类库(例如Java)中,具有满足不同需要的各种类型的容器,例如List(用于存储序列),Map(也被称为关联数组,用来建立对象之间的关联),Set(每种对象类型只持有一个),以及诸如队列、树、堆栈等更多的构件。

为什么要涉及不同种类的序列呢?第一,不同容器提供了不同类型的接口和外部行为。堆栈相比于队列就具备不同的接口和行为,也不同于集合和列表的接口和行为。它们之中的某种容器提供的解决方案可能比其他容器要灵活得多。第二,不同的容器对于某种操作具有不同的效率。最好的例子就是两种List的比较:ArrayListLinkedList。它们都是具有相同接口和外部行为的简单的序列,但是它们对某些操作所花费的代价却有天壤之别。在ArrayList中,随机访问元素是一个花费固定时间操作;但是对LinkedList来说,随机选取元素需要在列表中移动,这种代价是高昂的,访问越靠近表尾的元素,花费的时间越长。而另一方面,如果想在序列中间插入一个元素,LinkedList的开销却比ArrayList要小。上述操作以及其他操作的效率,依序列底层结构的不同而存在很大的差异。我们可以在一开始使用LinkedList构建程序,而在优化系统性能时改用ArrayList。接口List所带来的抽象,把在容器之间进行转换时对代码产生的影响降到最小限度。

1.9.1 参数化类型

在Java SE5出现之前,容器存储的对象都只具有Java中的通用类型:Object。单根继承结构意味着所有东西都是Object类型,所以可以存储Object的容器可以存储任何东西。这使得容器很容易被复用。

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

这里再次用到了转型,但这一次不是向继承结构的上层转型为一个更泛化的类型,而是向下转型为更具体的类型。向上转型是安全的,但是除非确切知道所要处理的对象的类型,否则向下转型几乎是不安全的。

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

向下转型和运行时的检查需要额外的程序运行时间,也需要程序员付出更多的心血。那么创建这样的容器,它知道自己所保存的对象的类型,从而不需要向下转型以及消除犯错误的可能,这样不是更有意义吗?这种解决方案被称为参数化类型机制。参数化类型就是一个编译器可以自动定制作用于特定类型上的类。例如,通过使用参数化类型,编译器可以定制一个只接纳和取出Shape对象的容器。

Java SE5的重大变化之一就是增加了参数化类型,在Java中它被称为范型一对尖括号,中间包含类型信息,通过这些特征就可以识别对范型的使用。例如,可以用下面这样的语句来创建一个存储Shape的ArrayList:

ArrayList<Shape> shapes = new ArrayList<Shape>();

为了利用范型的优点,很多标准类库构件都已经进行了修改。

1.10 对象的创建和生命期

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

对象的数据位于何处?怎样控制对象的生命周期?C++认为效率控制是最重要的议题,所以给程序员提供了选择的权力。为了追求最大的执行速度,对象的存储空间和生命周期可以在编写程序时确定,这可以通过将对象置于堆栈(有时称为自动变量或限域变量)或静态存储区域内来实现。这种方式将存储空间分配和释放置于优先考虑的位置,某些情况下这样控制非常有价值。但是也牺牲了灵活性,因为必须在编写程序时知道对象确切的数量、生命周期和类型。如果试图解决更一般化的问题,例如计算机辅助设计、仓库管理或者空中交通控制,这种方式就显得过于受限了。

第二种方式是在称为堆(heap)的内存池中动态地创建对象。在这张方式中,直到运行时才知道需要多少对象,它们的生命周期如何,以及它们的具体类型是什么。这些问题的答案只能在程序运行时相关代码被执行到的那一刻才能确定。如果需要一个新对象,可以在需要的时刻直接在堆中创建。因为存储空间是在运行时被动态管理的,所以需要大量的时间在堆中分配存储空间,这可能要远远大于在堆栈中创建存储空间的时间。在堆栈中创建存储空间和释放存储空间通常各需要一条汇编指令即可,分别对应将栈顶指针向下移动和将栈顶指针向上移动。创建堆存储空间的时间依赖于存储机制的设计

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

对于允许在堆栈上创建对象的语言,编译器可以确定对象存活的时间,并可以自动销毁它。然而,如果是在堆上创建对象,编译器就会对它的生命周期一无所知。在像C++这样的语言中,必须通过编程方式来确定何时销毁对象,这可能会因为不能正确处理而导致内存泄漏Java提供了被称为“垃圾回收器”的机制,它可以自动发现对象何时不再被使用。更重要的是,垃圾回收器提供了更高层的保障,可以避免暗藏的内存泄漏问题,这个问题已经使许多C++项目折戟沉沙。

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

1.11 异常处理:处理错误

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

异常处理将错误处理直接置于编程语言中,有时甚至置于操作系统中。异常是一种对象,它从出错地点被“抛出”,并被专门设计用来处理特定类型错误的相应的异常处理器“捕获”。异常处理就像是与程序正常执行路径并行的、在错误发生时执行的另一条路径。因为它是另一条完全分离的执行路径,所以它不会干扰正常的执行代码。这往往使得代码编写变得简单,因为不需要被迫定期检查错误。此外,被抛出的异常不像方法返回的错误值和方法设置的用来表示错误条件的标志位那样可以被忽略。异常不能被忽略,所以它保证一定会在某处得到处理。最后需要指出的是:异常提供了一种从错误状况进行可靠恢复的途径。现在不再只是退出程序,你可以经常进行校正,并恢复程序的执行,这些都有助于编写出更健壮的程序。

Java的异常处理在众多的编程语言中格外引人注目,因为Java一开始就内置了异常处理,而且强制你必需使用它。它是唯一可接受的错误报告方式。如果没有编写正确的处理异常的代码,那么就会得到一条编译时的出错消息。这种有保障的一致性有时会使得错误处理非常容易。

值得注意的是,异常处理不是面向对象的特征——尽管在面向对象语言中异常常常被表示成为一个对象。异常处理在面向对象语言出现之前就已经存在了。


1.12 并发编程

在计算机编程中有一个基本概念,就是在同一时刻处理多个任务的思想。最初,程序员用所掌握的有关机器底层的知识来编写中断服务程序,主进程的挂起是通过硬件中断来触发的。尽管这么做可以解决问题,但是其难度太大,而且不能移植,所以使得将程序移植到新型号的机器上时,既费时又费力。

有时中断对于处理时间飨强的任务是必需的,但是对于大量的其他问题,我们只是想把问题切分成多个可独立运行的部分(任务),从而提高程序的响应能力。在程序中,这些彼此独立运行的部分称之为线程,上述概念被称为“并发”。并发最常见的例子就是用户界面。通过使用任务,用户可以在按下按钮后快速得到一个响应,而不用被迫等待到程序完成当前任务为止。

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

并发还有一个隐患:共享资源。如果有多个并行任务都要访问同一项资源,那么就会出问题。例如,两个进程不能同时向一台打印机发送信息。为了解决这个问题,可以共享的资源,例如打印机,必须在使用期间被锁定。因此,整个过程是:某个任务锁定某项资源,完成其任务,然后释放资源锁,使其他任务可以使用这项资源

Java的并发是内置于语言中的,Java SE5已经增添了大量额外的库支持。

1.13 Java与Internet

1.13.1 Web是什么

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

客户/服务器系统的核心思想是:系统具有一个中央信息存储池(central repository of information),用来存储某种数据,它通常存在于数据库中,你可以根据需要将它分发给某些人员或机器集群。客户/服务器概念的关键在于信息存储池的位置集中于中央,这使得它可以被修改,并且这些修改将被传播给信息消费者。总之,信息存储池、用于分发信息的软件以及信息与软件所驻留的机器或机群掌握总称为服务器。驻留在用户机器上的软件与服务器进行通信,以获取信息、处理信息,然后将它们显示在被称为客户机的用户机器上。

基本概念并不复杂,问题在于你只有单一的服务器,却要同时为多个客户服务。通常,这会涉及数据库管理系统,因此设计者把数据“均衡”分布于数据表中,以取得最优的使用效果。此外,系统通常允许客户在服务器中插入新的信息。这意味着必须保证一个客户插入的新数据不会覆盖另一个客户插入的新数据,也不会在将其添加到数据库的过程中丢失(这被称为事务处理)。如果客户端软件发生变化,那么它必须被重新编译、调试并安装到客户端机器上,事实证明这比想象的要更加复杂与费力。如果想支持多种不同类型的计算机和操作系统,问题将更麻烦。最后还有一个最重要的性能问题:可能在任意时刻都有成百上千的客户向服务器发出请求,所以任何小的延迟都会产生重大影响。为了将延迟最小化,程序员努力减轻处理任务的负载,通常是分散给客户端机器处理,但有时也会使用所谓的中间件将负载分散给在服务器端的其他机器。(中间件也被用来提高可维护性。)

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

Web实际上就是一个巨型客户/服务器系统,但稍微差一点,因为所有的服务器和客户机都同时存在于同一个网络中。

Web浏览器向前跨进了一大步,它包含了这样的概念:一段信息不经修改就可以在任意型号的计算机上显示。然而,最初的浏览器仍然相当原始,很快就因为加诸于其上的种种需要而陷入困境。

为了解决这个问题,人们采用了各种不同的方法。首先,图形标准得到了增强,使得在浏览器中可以播放质量更好的动画和视频。剩下的问题通过引入在客户端浏览器中运行程序的能力就可以解决。这被称为“客户端编程”。

1.13.2 客户端编程

Web最初的“服务器-浏览器”设计是为了能够提供交互性的内容,但是其交互性完全由服务器提供。服务器产生静态页面,提供给只能解释并显示它们的客户端浏览器。基本的HTML(超文本标记语言)包含有简单的数据收集机制:文本输入框、复选框、单选框、列表和下拉式列表以及按钮——它只能被编程来实现复位表单上的数据或提交表单上的数据给服务器。这种提交动作通过所有的Web服务器都提供的通用网关接口(common gateway interface,CGI)传递。提交内容会告诉CGI应该如何处理它。最常见的动作就是一个在服务器中常被命名为“cgi-bin”的目录下的一个程序。几乎所有的语言都可以用来编写这些程序,Perl已经成为最常见的选择,因为它被设计用来处理文本,并且是解释型语言,因此无论服务器的处理器和操作系统如何,它都适合安装。然而,Python已对其产生了重大的冲击,因为它更强大而且更简单。

当今许多有影响力的网站完全构建于CGI之上的,实际上你几乎可以通过CGI做任何事。然而,构建于CGI程序之上的网站可能会迅速变得过于复杂而难以维护,并同时产生响应时间过长的问题。CGI程序的响应时间依赖于所必须发送的数据量的大小。以及服务器和Internet的负载。(因此,启动CGI程序也相当慢。)Web的最初设计者们并没有预见到网络带宽被人们开发的各种应用迅速耗尽。

问题的解决方法就是客户端编程。大多数运行Web浏览器的机器都是能够执行大型任务的强有力的引擎。在使用原始的静态HTML方式的情况下,它们只是闲在那里,等着服务器送来下一个页面。客户端编程意味着Web浏览器能用来执行任何它可以完成的工作,使得返回给用户的结果更加迅捷,而且使得你的网站更加具有交互性

客户端编程的问题是:它与通常意义上的编程十分不同,参数几乎相同,而平台却不同。Web浏览器就像一个功能受限的操作系统。最终,你仍然必须编写程序,而且还得处理那些令人头昏眼花的成堆的问题,并以客户端编程的方式来产生解决方案。

1.插件

客户端编程所迈出的最重要的一步就是插件(plug-in)的开发。通过这种方式,程序员可以下载一段代码,并将其插入到浏览器中适当的位置,以此来为浏览器添加新功能。它告诉浏览器:从现在开始,你可以采取这个新行动了(只需要下载一次插件即可)。某些更快更强大的行为都是通过插件添加到服务器中的。但是编写插件并不是件轻松的事,也不是构建某特定网站的过程中所要做的事情。插件对于客户端编程的价值在于:它允许专家极的程序员不需经过浏览器生产产商的许可,就可以开发某种语言扩展,并将它们添加到服务器中。因此,插件提供了一个“后门”,使得可以创建新的客户端编程语言(但是并不是所有的客户端编程语言都是以插件的形式实现的)。

2.脚本语言

插件引发了浏览器脚本语言(scripting language)的开发。通过使用某种脚本语言,你可以将客户端程序的源代码直接嵌入到HTML页面中,解释这种语言的插件在HTML页面被显示时自动激活。脚本语言先天就相当易于理解,因为它们只是作为HTML页面的一部分的简单文本,当服务器收到要获取该页面的请求时,它们可以被快速加载。此方法的缺点是代码会暴露给任何人去浏览(或窃取)。但是,通常不会使用脚本语言去做相当复杂的事情,所以这个缺点并不太严重。

如果你期望有一种脚本语言在Web浏览器不需要任何插件的情况下就可以得到支持,那它非JavaScript莫属。遗憾的是,大多数Web浏览器最初都是以彼此相异的方式来实现对JavaScript的支持的,这种差异甚至存在于同一种浏览器的不同版本之间。以ECMAScript的形式实现的JavaScript的标准化有助于此问题的解决,但是不同的浏览器为了跟上这一标准化趋势已经花费了相当长的时间。通常,你必须以JavaScript的某种最小公分母形式来编程,以使得你的程序可以在所有的浏览器上运行。

在Web浏览器内部使用的脚本语言实际上总是被利用来解决特定类型的问题,主要是用来创建更丰富、更具有交互性的图形化用户界面(graphic user interface,GUI)。但是,脚本语言可以解决客户端编程中所遇到的80%的问题。

3.Java

如果脚本语言可以解决客户端编程80%的问题的话,那么剩下的那20%又该怎么办呢?Java是处理它们最流行的解决方案。Java不仅是一种功能强大的、安全的、跨平台的、国际化的编程语言,而且它还在不断地被扩展,以提供更多的语言功能和类库,能够优雅地处理在传统编程语言中很难解决的问题,例如并发数据库访问网络编程分布式计算。Java是通过applet以及使用Java Web Start来使用客户端编程的。

applet是只在Web浏览器中运行的小程序,它是作位网页的一部分而自动下载的(就像网页中的图片被自动下载一样)。当applet被激活时,它便开始执行一个程序,这正是它优雅之处:它提供了一种分发软件的方法,一旦用户需要客户端软件时,就自动从服务器把客户端软件分发给用户。用户获取最新版本的客户端软件时不会产生错误,而且也不需要很麻烦的重新安装过程。Java的这种设计方式,使得程序员只需创建单一的程序,而只需要一台计算机有浏览器,且浏览器具有内置的Java解释器(大多数的机器都如此),那么这个程序就可以自动在这台计算机上运行。由于Java是一种成熟的编程语言,所以在提出对服务器的请求之前和之后,可以在客户端尽可能多地做些事情。例如,不必跨网络地发送一张请求单来检查自己是否填写了错误的日期或其他参数,客户端计算机就可以快速地标出错误数据,而不同等待服务器作出标记并给你传回图片。这不仅立即就获得了高速度和快速的响应能力,而且也降低了网络流量和服务器负载,从而不会使整个Internet的速度都慢下来。

4.备选方案

Java applet没有得到大规模应用,这其中最大的问题可能在于安装Java运行时环境(JRE)所必需的10MB带宽对于一般的用户来说过于困难了,而微软没有在IE中包含JRE这一事实也许就此已经封杀了applet的命运。

一种折中的方案,Macromedia的Flex,它允许你创建基于Flash的与applet相当的应用。因为Flash Player在超过98%的Web浏览器上都可用,因此它被认为是事实上已被接受的标准。安装和更新Flash Player都十分快捷。ActionScript语言是基于ECMAScript的,因此我们对它应该很熟悉,但是Flex使得我们在编程时无需担心浏览器相关性,因此,它远比JavaScript要吸引人得多。对于客户端编程而言,这是一种值得考虑的备选方案。

5. .NET和C#

曾几何时,Java applet的主要竞争对手是微软的ActiveX——尽管它要求客户端必需运行Windows平台。从那以后,微软以.NET平台C#编程语言的形式推出了与Java全面竞争的对手。.NET平台大致相当于Java虚拟机(JVM,即执行Java程序的软件平台)和Java类库,而C#毫无疑问与Java有类似之处。

目前,.NET主要受攻击的地方和人们所关心的最重要的问题就是,微软是否允许将它完全移植到其他平台上。微软宣称这么做没有问题,而且Mono项目已经有了一个在Linux上运行的.NET的部分实现;但是,在该实现完成及微软不会排斥其中的任何部分之前,.NET作为一种跨平台的解决方案仍旧是一场高风险的博弈。

1.13.3 服务器端编程

服务器端编程是Java已经取得巨大成功的因素之一。当提出对服务器的请求后,会发生什么呢?大部分时间,请求只是要求“给我发送一个文件”,之后浏览器会以某种适当的形式解释这个文件,例如将其作为HTML页面、图片、Java applet或脚本程序等来解释。

更复杂的对服务器的请求通常涉及数据库事务。常见的情形是复杂的数据库搜索请求,然后服务器将结果进行格式编排,使其成为一个HTML页面发回给客户端。另一种常见情形是,当你要加入一个团体或下订单时,可能想在数据库中注册自己的名字,这将涉及对数据库的修改。这些数据库请求必须通过服务器端的某些代码来处理,这就是所谓的服务器端编程。过去,服务器端编程都是通过使用Perl、Python、C++或其他某种语言编写CGI程序而实现的,但却造成了从此以后更加复杂的系统。其中就包括基于Java的Web服务器,它让你用Java编写被称为servlet的程序来实现服务器端编程。servlet及其衍生物JSP,是许多开发网站的公司迁移到Java上的两个主要原因,尤其是因为它们消除了处理具有不同能力的浏览器时所遇到的问题。

1.14 总结

面向对象程序设计带给人们的喜悦之一就是:对于设计良好的程序,通过阅读它就可以很容易地理解其代码。通常,其代码也会少很多,因为许多问题都可以通过重用现有的类库代码而得到解决。

OOP和Java也许并不适合所有的人。重要的是要正确评估自己的需求,并决定Java是否能够最后地满足这些需求,还是使用其他编程系统才是更好的选择。如果知道自己的需求在可预见的未来会变得非常特殊化,并且Java可能不能满足你的具体限制,那么就应该去考察其他的选择(例如Python)。即使最终仍旧选择Java作为编程语言,至少也要理解还有哪些选项可供选择,并且对为什么选择这个方向要有清楚的认识。


已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页