1.8 单根的继承结构<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

        

    自从C++诞生以来,面向对象程序设计就出现了这样一个问题:是否所有的类最终都应该继承于一个共同的基类?在Java中(实际上除C++以外所有的OOP语言都是这样)给出了肯定的回答,并把最终基类简单的命名为Object。这种结构称为单根继承结构。后来的结果显示,单根的继承结构是相当有用的。

   

    在单根继承结构中,所有的对象都有一个共同的基类,所以它们归根结底都是同一个基本类型。而在C++中则没有这种情况。C++这么做是为了更好的向后兼容C模型,使之受限更小,但当你要进行一个完全的面向对象程序设计时,你必须亲自建立这种单根继承结构,来提供那些已经内置在其他OOP语言中的特性。当你需要向程序中添加新的类库时,就会遇到一些不兼容的接口,这时你就要尽可能让这些接口融入到你的设计之中(也许要通过多继承实现)。以这样的代价来换取C++额外的灵活性是否值得呢?如果你在C上面投入了大量的人力物力,那它就是值得的。如果你是刚刚开始涉足OOP,那Java的立场无疑会为你提供更高的效率。

   

    在单根继承结构中,能够确保所有对象都能实现一些功能,每个对象都能执行最终基类中的那些操作。所有的对象都可以很容易的建立在堆上,参数的传递也被大大的简化了。

   

    单根继承结构可以很轻松的实现垃圾回收器(garbage collector),而垃圾回收器也是Java相对于C++的重要改进之一。因为所有对象都确保具有类型信息,你不会因为无法确定一个对象的类型而陷入僵局。这在系统级操作中是非常重要的(比如异常处理),而且还能为编程带来更大的灵活性。

   

1.9 容器

 

    大部分情况下,在解决一个问题时,你不知道你要用到多少对象,也不知道它们会存在多久,很可能你也不知到应该如何存储这些对象。如果直到运行时才能知道所需要的信息,那你怎么可能在之前就知道要开辟多大的存储空间呢?

   

    相比于面向对象设计中的其它问题,这个问题的解决方法似乎有点儿挑衅:再创建个新的类型呗!这个新的类型应当持有对其他对象的引用。当然,在大多数语言中,你可以用数组(array)来完成这样的工作。但是这里创建的这个新的对象,它可以扩展自身的空间,以便在需要时适应你向它里面添加的任何东西,就像是吹气球一样。我们把这种新的类型称作容器container),有的地方也叫做集合collection),但是在Java库中使用了前者来表达一些其他的含义,所以我们也使用容器这个词。所以在使用容器时,你不需要知道你将要向里面添加多少对象,也就是不用担心这个容器够不够用。只需创建一个容器对象,然后把细节交给它去操心就行了。

   

    万幸!好的OOP语言都会在开发包中为你提供一组容器。在C++中,它是标准C++库(Standard C++ Library)中的一部分,被称作标准模板库Standard Template LibrarySTL)。在Smalltalk中,同样有一组非常完备的容器。Java在标准库中也有着大量的容器,在其中一些库中,有那么一两个通用容器可以满足所有的需求,在其他库中,有适应各种特殊需求的不同类型的容器:List可以用来装序列;Map,也叫关联数组(associative array),可以将一些对象和其他对象关联起来;Set,可以创建一个对象的集合,不会包含重复的元素,模仿了数学上的集合的概念。还有一些其他的组件,比如队列,树,栈,等等。

   

    从设计的角度出发,你所需要的就是一个你可以操作的容器。如果只需要一种容器就能满足你所有的需求,那分出这么多不同的容器就是吃饱了撑的。有两个原因使你在使用时需要选择容器的种类。第一,所有的容器提供不同的接口和不同的行为以操作其中的对象。栈和队列就具有不同的接口和行为,和集合、链表也不同,每一种都可能在你解决问题时提供更大的灵活性;第二,不同的容器在进行某些相同的操作时的效率也不同。比如,有两种基本的ListArrayListLinkedList。它俩都是简单的序列,都有可识别的接口和行为。但是某些操作的开销有着明显的不同。在ArrayList中,随机访问一个元素只需花费固定的时间,花费的时间和你选择的元素没有关系。然而在LinkedList中,遍历链表来访问一个随机位置上的元素的开销是巨大的,而且元素的位置越靠后,花费的时间越久。而当你想在序列中间插入一个元素时,LinkedList的开销又要小于ArrayList。由于底层数据结构的不同,所以这些各种各样的操作的效率也不同。你在刚开始建立程序时,可能用的是一个LinkedList,当你要调整性能时,没准儿又需要把它改成一个ArrayList。由于它们都是List这个接口的抽象,所以在两者之间进行转换时,对代码的影响是非常小的。

   

1.9.1 参数化类型(泛型)

 

    Java SE5之前,容器中只装载那个通用的Java类型:Object。单根继承结构使得任何事物都是一个Object,所以一个容器装载了Object便能装载一切,它虽然不能装载基本类型,但Java SE5的自动打包(autoboxing)解决了这个问题,这在后面会讲到。这样的机制使得容器非常便于重用。

   

    使用这样的容器时,你只需向容器中添加对象的引用,并在需要时再取出来就可以了。可是,因为容器只装载Object,所以当你向容器中添加了一个对象的引用后,它就向上转型到了Object类型,失去了它本身的特性。当你把它取出来后,你得到的引用是Object类型而不是当初的那个类型了。那你怎么才能把它变回来呢?

   

    这里,再一次运用转型的概念,不过这次的转型不是向上而是向下,是从基类转到子类,也就是向下转型downcasting)。在向上转型的时候,你知道一个Circle是一个Shape,所以可以安全的转型。可是向下转型的时候你不知到一个Object究竟是Circle还是Shape,所以除非你清楚的知道到底是哪种类型,否则这样的向下转型是不安全的。

   

    虽说这样的向下转型是不安全的,但也不是完全危险的,因为如果你在向下转型时转到了错误的类型,就会出现一个运行时错误:异常(exception),这个会在后面讲到。所以当你从容器中取出对象引用时,你必须清楚的知道当初放进去的是什么类型,这样才能安全的向下转型。

   

    向下转型以及运行时的检查需要花费额外的时间,并且需要程序员做更多的工作。既然有这样那样的麻烦,如果能让容器知道它到底装的是什么类型岂不快哉?解决方案就是参数化类型parameterized type)。参数化类型就是说有这么一个类,编译器可以定制它,以作用于特定的类型。如果容器是参数化的,那么编译器就可以定制它,使得这个容器只能接收Shape,并且从它里面拿出来的也还是Shape

   

    Java SE5的重大改变之一就是加入了参数化类型机制,在Java中把它叫做泛型generics)。泛型的使用就是用一对儿尖括号把类型信息括在里面。举个栗子!一个装载ShapeArrayList可以这样创建:

   

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

   

    Java SE5中,为了体现泛型的特点,许多标准库组件都作出了相应的改变。你会发现,泛型对本书的许多代码都产生了重要的影响。

   

1.10 对象的创建及其生命周期

 

    在处理对象时,一个重要的问题就是对象创建和销毁的方式。一个对象为了活下去需要很多资源,尤其是内存。当一个对象不再需要时,它必须被清理以便释放它所占用的资源,使这些资源可以回收再利用。在简单的程序设计里,如何清理对象似乎够不成什么挑战:创建一个对象,使用它,使用完了销毁不就可以了。然而,你很可能遇到非常复杂的情况。

   

    举个栗子!设想一下,假如你正在为一个机场设计一套空中交通管理系统。(同样的模型可被用在仓库管理、影碟出租、宠物收养系统中。)刚开始设计时可能比较轻松:创建一个容器,可以装载飞机对象,然后为每一个进入该系统所管理的区域的飞机创建一个飞机对象,把它装到这个容器中。清理的时候,清除离开该区域的飞机对象就可以了。

   

    但是!这个但是很重要!你可能会需要其他的子系统来记录一些关于这些飞机的数据,这些数据可能不需要及时在主系统中处理。也许这些数据是一份所有小型飞机离开该机场后的飞行计划。如此,你便需要另一个容器来装载所有的小型飞机对象,当你创建了一个飞机对象并装入第一个容器后,如果它同时是一个小型飞机对象,那也要装入第二个容器中。在空闲时间里,后台进程会对第二个容器中的对象进行必要的操作。

   

    你看,情况一下就变得复杂了。在这个新的情况中,你觉得应该何时清除对象呢?当主系统中完成了对这个对象的处理,其他的子系统没准儿还需要对它进行一些操作。许多其他情况也有着同样的问题,而在程序设计语言中(比如C++),当你用完一个对象是必须显式删除它的!根据上面的描述可以看出,这是一件非常复杂的工作。

   

    对象的数据在哪里?如何控制对象的生命周期?C++采取了高效控制的路线,它将选择权交给程序员。为了获得最大的运行速度,在程序编写时,可以通过将对象置于栈中(这种对象被称为自动变量(automatic variable)或局部变量(scoped variable))或静态存储区中就可以决定对象的存储和生命周期。这种控制优先考虑了存储的分配和释放,在某些情况下特别有效。然而你却失去了很多灵活性,你必须在编写程序时清楚的知道对象的大小、生命周期以及类型。如果你要设计一个更显通用的系统,比如计算机辅助设计、仓库管理或上面说的空中管理系统,这样的机制就显得十分受限。

   

    第二条路线就是在内存池中动态的创建对象,这个内存池称为”(heap)。在这条路线的指导下,你需要多少对象、它们的生命周期或者它们的精确类型是什么这种信息到了运行时,相关代码被执行到了才会知道。如果需要一个对象,直接在堆中创建就可以了。由于存储区是动态管理,所以在运行时,在堆中分配存储空间所花费的时间要明显多于在栈中分配存储空间花费的时间。在栈中分配存储空间只需要两条汇编指令,一条指令负责向下移动栈指针(分配存储空间),另一条负责将它再移回来(释放存储空间)。而在堆中分配存储空间花费的时间则取决于存储机制。

   

    “动态的路线作出了一个符合逻辑的一般性假设,那就是假设所有对象都趋向于变得十分庞大而复杂,这样,为了查找和释放存储空间所做的额外的工作,在创建对象时产生的影响就相对减弱了。而且,这种机制带来的更大的灵活性在解决一般性的程序设计问题时是必不可少的。

   

    Java完全采用动态内存分配(后面你会看到基本类型只是一个特例)。当你要创建一个对象时,使用new操作符就可以动态的创建一个对象的实例。

   

    还有个问题,那就是对象的生命周期。在栈中创建对象的语言里,编译器决定对象能活多久,并且能自动销毁它。相反,如果在堆中创建对象,那么编译器就不会知道对象的声明周期。在类似于C++的语言中,你必须编写代码来销毁对象,这会导致内存泄漏(这在C++中是一个普遍性问题)。Java提供了一种特性称为垃圾回收器garbage collector),它能自动发现那些不再被使用的对象并销毁它们。这种特性带来了极大的便利,因为它减少了你要考虑的问题和要编写的代码。更重要的是,垃圾回收器为潜在的内存泄漏问题提供了更高层次的安全保障,而这个问题则是C++项目中的软肋。

   

    Java中,垃圾回收器被设计用来处理内存释放的问题(尽管它不包括对象清理的其他方面)。垃圾回收器知道一个对象何时不再被使用,并自动释放它占用的内存。这样,再结合Java的单根继承结构以及在堆中创建对象,使得用Java编程要比用C++更简单。要做的决策和要克服的困难也要少得多。

   

1.11 异常处理:如何处理错误

 

    从编程语言的诞生开始,错误的处理就是个棘手的问题。因为要想设计出一个良好的错误处理机制是非常困难的,许多语言只是简单的忽略这个问题,并交给程序库的设计者去处理,而他们也只是采取了一些不彻底的方法,这些方法虽然适用很广,但也很容易绕过那些错误,而他们所做的处理大部分也只是选择忽略它们。大部分错误处理机制的问题在于,它们都依赖于程序员对于那些约定俗成而非语言强制的规则的警惕性。如果他们不够警惕-如果他们很忙-这种机制就会被轻易忽视。

   

    异常处理是一种将错误处理直接置于编程语言中的机制,有时甚至置于操作系统中。一个异常也是一个对象,它在发生错误的地方被抛出,被适当的异常处理器捕获,异常处理器是被设计用来处理特定类型的错误。异常处理就好象是一条与程序正常执行路线并行的另一条路线,当错误发生时,它便会执行。由于它采用了一条独立的执行路线,所以不会干扰正常执行的代码。这会让编写代码变得轻松,因为你不是非得经常去检查错误。而且,有些方法为了显示有错误条件出现,会返回一个错误值或者设置一个标志位,这些都是可以忽略的。而异常则是不能被忽略的,因此会确保异常在某处被处理。最后一点,异常提供了一个让程序在出错的情况下恢复正常的可靠方法。你可以将错误改正并恢复程序的正常执行,而不仅仅是退出程序,这将增强程序的健壮性。

   

    Java的异常处理在众多编程语言中独树一帜,因为在Java一开始就内置了异常处理并且强制你使用它。它是唯一的可以被接受的错误报告方式。如果你不编写适当的代码来处理异常,那你就会得到一个编译时错误。这种有保障的一致性有时候会让错误处理变得非常简单。

   

    值得注意的是,尽管在面向对象语言中通常用对象来表示异常,但异常处理机制并不是面向对象特性中的一种。异常处理机制在面向对象语言出现之前就已经有了。

 

1.12 带有并发性的程序设计

 

    计算机程序设计中的一个基础概念就是能同时处理多个任务。许多程序设计上的问题都要求程序能够停止当前的工作,转去处理其他的问题,然后再转回到主进程中。有多种方式可以尝试解决这个问题。一开始,程序员根据底层硬件知识来编写中断服务程序,挂起主进程最初也是通过硬件中断来实现。尽管这种方法工作得很好,但它的实现难度大,并且可移植性差,将程序转移到一种新型机器上运行将变得费时费力。

   

    有时,在处理一些对时间要求苛刻的任务时需要使用中断,然而大部分的情况下我们只是想把问题分成几个独立运行的小块(任务),以提高整个程序的相应能力,这个概念叫做并发concurrency),这些独立运行的小块叫做线程thread)。并发最常见的例子就是用户界面,当用户通过按键来执行一个任务时瞬间就能得到响应,而不是等待程序完成当前任务之后才能得到响应。

   

    通常,线程就是一种为单处理器分配执行时间的方法。但如果操作系统支持多处理器,每条线程就可以被指定在不同的处理器上运行,实现真正意义上的并行。从编程语言的角度看,多线程带来的便利之一就是程序员无需关心程序到底运行在单处理器还是多处理器上。程序在逻辑上被分为多个线程,如果机器有更多的处理器,那么程序就会运行得更快,而不必做出什么调整。

   

    看了上面的描述,并发的实现看起来似乎并不困难。然而有一个潜在的问题:资源共享。如果有多个正在运行的线程要访问同样的资源,那你就会遇到这个问题。举个栗子!两个进程是无法同时向一个打印机发送信息的。为了解决这个问题,可以共享资源,但在使用资源时必须对资源进行锁定,比如这台打印机。在这样的情况下,并发的整个过程就是:一个任务锁定一个资源,完成它的工作,接触锁定之后,其他的任务才能访问这个资源。

   

    Java的并发性被内置到了语言中,Java SE5中已经添加了大量额外的类库来支持并发程序设计。

   

1.13 JavaInternet

 

    如果Java仅仅是众多编程语言中的一种,你可能会问是什么让Java显得如此重要,又是什么让Java被誉为是计算机程序设计跨出的革命性的一步。如果从传统意义上的程序设计来看,答案似乎并不轻易得见。尽管Java能够很好的解决传统的单机模式下的程序设计问题,但其重要性更体现在Java能够解决万维网(World Wide WebWWW)上的程序设计问题。

   

1.13.1 Web是啥?

 

    Web一词看上去有些神秘,说起来又会联想到冲浪表现主页这些词。回头审视它的真实面貌有助于对它的理解,但要想这么做首先得要知道什么是客户/服务器client/sever)系统,它是计算技术中的另一个充满迷雾的话题。

   

    1. “客户/服务器式计算

    客户/服务器系统的基本思想是,你有一个中央信息储存池(central repository of information)(这些信息通常是位于数据库里的数据),你想要根据不同的需求,将这些信息分布到不同的人群和机器上。客户/服务器概念的要点是这个信息储存池位于整个系统的中央,当对这些信息进行改变时,这些改变就会体现在信息的使用者面前。将信息储存池、负责分布信息的软件,以及它们所驻留的机器或机器群合起来,称为服务器sever)。驻留在用户机器上的软件,通过与服务器通信,获取信息,处理信息,然后在用户机器上显示出来,这样的用户机被称为客户客户机client)。

   

    “客户/服务器式计算的概念并不复杂。问题在于你要用一个服务器一下子为多个客户提供服务。这里通常会用到数据库管理系统(database management system),设计者将数据合理的存放于各个表中来获得最优的使用效果。此外,客户/服务器系统通常会允许客户机向服务器中插入新的数据。对此你必须确保一台客户机插入的新数据不会覆盖掉另一台客户机插入的数据,或者是在添加到数据库的过程中不会丢失(这叫做事务处理(transaction processing))。当客户机上的软件需要改变时,必须重新编译、调试,然后安装到客户机上,这显然比你一开始想象的要复杂。所以,用一个服务器为各种各样的计算机和操作系统提供服务就会有这样那样的问题。最后,就是非常重要的性能问题:任何时刻都可能有上百台客户机向服务器发送请求,所以一个微小的延迟也会产生巨大的影响。为了最小化延迟,程序员努力减轻任务处理的负担,或转移到客户机上,有时也会使用中间件(middleware)来转移到其他服务器上。(中间件通常用于提高程序的可维护性。)

   

    如何分布信息这个看似简单的问题在不同的层次上也存在各种各样的复杂的问题,看上去深不可测。但它仍非常重要,客户/服务器计算技术几乎占据了整个程序设计的一半。从制定订单、信用卡交易,到包括股票市场、科学计算、政府、个人在内的信息分布都是客户/服务器计算技术。我们过去所做的就是为每个问题提供一套独立的解决方案,出现一个新的问题就制定一套新的解决方案。这些方案开发难度大且难以运行,用户还要学习各种各样的新式接口。此时,整个客户/服务器技术期盼着一次重大的变革。

   

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

   

    我们现在要说的Web实际上就是一个巨型的客户/服务器系统。实际情况可能稍微复杂点,因为所有的服务器和客户机都共存于同一个网络中。但你不许要了解这些,你只需要知道如何连接到服务器并且和服务器交互就可以了(尽管你可能为此跑遍满世界来找你要连接的服务器)。

   

    起先这个系统只进行单向操作。你向服务器发送一个请求,它回传给你一个文件,你的机器(客户机)上的浏览器就会根据客户机所要求的格式来解释这个文件。但很快人们就希望系统能做更多的事情,而不仅仅是从服务器传回页面。人们希望得到完整的客户/服务器交互,从而能向服务器反馈信息。比如可以对服务器上的数据库进行查询,向服务器添加新的信息、增加新的订单(这需要特殊的安全保护)。这些变革,正是我们在Web发展过程中所目睹的。

   

    Web浏览器的产生带来了突破性的进展,它使得信息在各种类型的机器上能以同样的方式显示。然而最初的浏览器仍是非常原始的,并且因为各种各样的需求而变得不堪重负。它带来的交互性并不强,而且几乎要阻塞掉服务器和Internet,因为只要你做的工作涉及到程序计算,你就要将信息传回服务器,让服务器去处理。这可能要用几秒或者几分钟,最后告诉你在发送请求的时候出现了一些拼写错误,坑爹啊。由于浏览器只负责浏览文件,所以它连最简单的计算任务都无法执行。(当然这样也是非常安全的,因为它无法运行客户机上的那些可能包含BUG或病毒的程序。)

   

    为了解决这个问题,尝试了各种各样的方法。首先,细化了图形标准,使浏览器可以播放高质量的动画和电影。剩下的问题只好通过让浏览器具有能够运行程序的能力来解决,这也就是下面要说的客户端编程client-side programming)。

   

1.13.2 客户端编程

 

    Web最初的服务器-浏览器server-browser)设计是为了提供可交互的内容,但其交互性完全由服务器一方提供。服务器为客户机浏览器产生静态页面,而浏览器就只是解释并显示它们。基本的超文本标记语言(HyperText Markup LanguageHTML)包含有简单的数据收集功能:文本输入框、复选框、单选框、列表、下拉菜单,而按钮的功能只是能够重置表单中的数据或者将表单中的数据提交给服务器。提交操作是通过所有Web服务器都提供的通用网关接口(Common Gateway InterfaceCGI)实现的。提交的内容会告诉CGI如何对它进行处理。最常见的动作就是运行服务器上“cgi-bin”目录下的程序。(当你按网页中的按钮然后观察浏览器顶部的地址栏,有的时候你就会在一长串乱七八糟的字符串中发现“cgi-bin”的身影。)大多说语言都可以用来编写这样的程序。Perl是常见的选择,因为它被设计用来进行文本操作并且是一种解释型语言,可以无视处理器和操作系统的类型安装在任何服务器上。而Pythonwww.Python.org)的出现也对它产生了巨大的冲击,因为Python更强大也更简单。

   

    当今的许多大型Web站点都是严格建立在CGI上的,你可以通过CGI完成任何事。然而,建立在CGI程序上的Web站点很快就会变得非常复杂而难以维护,而且响应时间也渐渐变成一个严重问题。CGI程序的响应依赖于传送的数据量,以及服务器和Internet的负载,而且启动一个CGI程序也非常慢。Web最初的设计者没有预见到各种各样的应用程序会如此迅速的消耗网络带宽。举个栗子!任何动态图形处理都无法连贯的进行,因为一个图形交互格式(Graphics Interchange FormatGIF)的文件的每一帧都要在服务器创建并传给客户机。此外,你一定体验过一个Web输入表单的数据验证过程:你点击页面上的提交按钮,数据被传送到服务器,服务器启动一个CGI程序发现了一处错误,然后建立一个能够为你提示错误信息的HTML页面,然后把这个页面传给你,你必须返回前一个页面重新输入。这么跑来跑去,不光慢,服务器负载又大,还有失Web服务器的大将风范。

   

    解决方案就是客户端编程。凡是能运行Web浏览器的计算机大部分都是可以执行大型任务的计算机。在使用原始的HTML方式时,它们只是衣来伸手,饭来张口,等着服务器将一道道美味的页面送过来。客户端编程意味着要发挥Web浏览器的力量,尽可能的让Web浏览器动起来,从而使对用户的响应更加迅速,并且让你的网站更具交互性。

   

    客户端编程的问题是,它与一般意义上的程序设计并不一样。参数方面倒是几乎没有什么不同,但平台上有很大的不一样。一个Web浏览器就像是一个并不完善的操作系统,所以,你所做的依然还是要编程,并通过客户端编程来解决各种各样令人头晕眼花的问题。本小节剩余的部分将对客户端编程的问题和方法进行简要介绍。

   

    1.插件

   

    客户端编程的重要进步就在于插件(plug-in)的发展。通过下载一小段代码然后把它插入到浏览器合适的位置上,程序员可以为浏览器添加新的功能。它告诉浏览器现在开始,你能执行新的功能啦!(这个插件你只需下载一次即可。)可以通过插件为浏览器添加一些迅速且功能强大的行为。但编写一个插件却不是什么轻松的事,它也不是网站建立过程中的一部分。插件对于客户端编程的价值在于,一个专家级的程序员可以不经浏览器开发者的同意,编写一些扩展功能,并将这些扩展功能添加到浏览器上。所以说,插件为浏览器开了一个后门,使得可以创建新的客户端编程语言。(不是所有的语言都可以用来编写插件。)

   

    2.脚本语言

   

    插件的出现导致了浏览器脚本语言(scripting language)的发展。通过使用脚本语言,你可以将客户端程序源代码直接嵌入到HTML页面中,当HTML页面显示的时候,插件就会自动的开始解释这些脚本语言。因为脚本语言只是加入到HTML页面中的简单文本,所以它们很容易理解,在服务器收到获取页面的请求时,它们可以迅速被加载。脚本语言的缺点在于它的代码会暴露在所有人面前(或被窃取),但通常不会用脚本语言做非常复杂的工作,所以这也就够不成什么威胁。

   

    JavaScript是一种无需插件就可以被浏览器支持的脚本语言。(它和Java只是表面上略有相似,需要专门的学习,当初这样命名只是为了搭上媒体热炒Java的顺风车。)不幸的是,对于JavaScript,每个浏览器的实现方式都有所不同,即便是同一个浏览器的不同版本,其实现方式也不尽相同。以ECMAScript为标准的JavaScript起到了一定的推动作用,但各浏览器为了达到该标准已经耗去了很长的时间(并且这种努力由于微软一直在推进它的VBScript形式的标准化而显得并不十分有效,VBScript是一种和JavaScript关系暧昧的脚本语言,相似性不言而喻)。所以,要想让你的程序可以在各种浏览器上运行,通常情况下你必须尽可能的按照在各个浏览器上变化最小的方式去编写程序。JavaScript的错误处理和调试简直就是一团糟,直到最近才有人创建出了真正复杂的JavaScript脚本段(Google在其GMail中使用),并且编写这样的复杂的脚本需要超然的奉献精神和高超的专业技巧。

   

    这一且都说明了浏览器中的脚本语言倾向于解决某些特殊的问题,主要是用来创建更加丰富和更具交互性的图形用户界面(Graphical User InterfaceGUI)。然而,脚本语言能够解决客户端编程中遇到的百分之八十的问题。你所遇到的问题可能恰好落到这百分之八十上,由于脚本语言能够提供更加便捷的开发,所以在你考虑使用Java这样更复杂的解决方案之前,应当优先考虑使用脚本语言。

   

    3.Java

   

    前文中说到脚本语言可以解决客户端编程中遇到的百分之八十的问题,那剩下的百分之二十呢?就真的那么难吗?对于剩下的这部分问题,Java是个不错的选择。不仅仅因为Java的强大、安全、跨平台和国际化,还由于Java仍在不断的扩展自身,提供新的语言特性,丰富其类库,颇为优雅的解决着那些在传统编程语言中难以解决的问题,比如并发性、数据库访问、网络编程,以及分布式计算。Java可以通过appletJava Web Start来实现客户端编程。

   

    applet是一种只能在Web浏览器中运行的小程序。它就像是网页中的图片一样可以自动的被下载下来。激活一个applet,它便开始运行一个程序。注意,这是个亮点!它提供了一种分发软件的方法,当用户需要客户端软件时,它就自动的从服务器将这个软件分发给各个客户端。用户获得的是最新版本的客户端软件,不会产生错误,无需重新安装。由于Java的这种设计方式,程序员只需创建一个程序,只要客户机的浏览器内置了Java解释器,那么这个程序就可以自动的在客户机上运行。(大多数机器都如此。)由于Java已经相当成熟,在向服务器发出请求的前后,你可以在客户机上完成大量的工作。举个栗子!不必跨越Internet向服务器发送一个请求表单来检查你是不是写错了日期或是别的什么参数,客户端计算机可以快速的标出错误数据,而不用等服务器标出错误然后给你传回个图片(啊哦!出生日期格式不对!)。这不仅可以使用户可以享受快速响应,还可以减轻网络和服务器的负载,不会使整个Internet的速度都慢下来。

   

    4.其他方案

   

    老实说,Java applet并没有实现它最初的目标。当Java出现在人们眼前时,最让人兴奋的就是applet,因为人们觉得终于有一种方法能够通过客户端编程实现提高响应速度和降低应用程序对Internet带宽的需求。人们对它予以重望。

   

    的确,你可以在Web上发现一些非常灵巧的applet。但是并没有大规模的普及开来。最大的问题在于大部分用户认为要下载并且安装一个10MB左右的Java运行时环境(Java Runtime EnvironmentJRE)是无法忍受的。而微软决定不在IE浏览器中包含JRE也彻底虐杀了applet。无论怎样,Java applet始终没有得到大规模应用。

   

    不过,appletJava Web Start在一些情况下仍具有价值。无论何时,当你想要控制用户的机器,比如在一个公司内部,很显然可以使用这种技术来向各个客户机发布和更新应用程序,这将节省大量的时间、人力和财力,特别是在需要经常更新的情况下。

   

    图形用户界面那一章中,我们将看到另一种充满前景的技术,Macromedia公司的Flex。通过这种技术可以开发出基于Flash技术的类似于applet的应用。因为Flash Player应用在超过百分之九十八的Web浏览器上(包括WindowsLinuxMac操作系统上的浏览器)所以它实际上已经成为了一个被接受的标准。Flash Player的安装和升级都非常方便。基于ECMAScriptActionScript语言和ECMAScript有着显而易见的相似性,但Flex允许你在编程的时候不去顾虑各种浏览器的特性,这显然比JavaScript更具交互性。所以对于客户端编程来说,Flex是一种值得考虑的技术。

   

    5. .NETC#

   

    曾几何时,Java的主要竞争对手是来自微软的ActiveX,虽然后者要求客户机运行Windows操作系统。从那时起,微软为了全面对抗Java,推出了.NET平台以及C#编程语言。.NET平台类似于Java虚拟机(Java Virtual MachineJVMJava程序执行时的软件平台)和Java类库,而C#自然是类似于Java的一种编程语言。这显然是微软在编程语言和编程环境上作出的最出色的成果。它们的优势在于已经看到了Java好在哪里不好在哪里,基于Java的情况开发自己的产品。这使得Java面临着前所未有的挑战。所以位于Sun公司的Java设计者们也开始研究C#,思考为什么程序员可能会转而使用它,最终在Java SE5中对Java进行了基础改进,并以此作为回应。

   

    现在,.NET最脆弱也是最终要的环节在于微软是否会允许将它完全移植到其它平台上。微软声称这将不会形成困扰,而且Mono项目(www.gomono.com)已经对.NETLinux上的运行做出了部分实现,但在该实现完成以及微软不会排斥其中的任何内容之前,将.NET作为一个跨平台的解决方案仍是一个具有风险的赌注。

   

    6.InternetIntranet

   

    对于客户/服务器问题来说,Web技术是一个广泛的解决方案,所以我们同样可以使用Web技术来解决一些小范围内的问题,比如说一个公司内部的客户/服务器系统。在一般的客户/服务器系统中,你会遇到各种不同类型的计算机,以及安装客户端软件这样的麻烦事,当然这些都随着Web浏览器和客户端编程的出现而被逐一化解。当Web技术应用于仅限于某个公司内部的特定信息网络时,就被称作Intranet(企业内部网)。Intranet相比Internet要有更高的安全性,因为你可以直接从物理上控制对服务器的访问。当人们已经了解浏览器的概念之后,对于这种新型系统的学习就不是那么困难了。

   

    我们能看到安全问题一直存在于客户/服务器系统中。如果你的程序运行在Internet上,由于客户机是多种多样的,所以你就不知到程序会在怎样的平台下工作,对此你就需要特别小心,尽量避免有bug的代码。鉴于这种情况,你就会希望能有一种跨平台的、安全的机制来保护客户/服务器系统,比如说脚本语言或者Java

   

    如果你的程序在Intranet上运行,那就会存在不同的限制。通常情况下公司里的所有机器可能全都是Intel/Windows平台。在Intranet中,你要负责代码的质量和修补漏洞。此外,你可能有一些在传统客户/服务器方法下的遗留代码,虽然那个时候每次更新都需要手动安装客户端软件。频繁更新所浪费的时间是使客户端软件转向使用浏览器主要原因之一,因为在使用浏览器做为客户端软件的时候,系统的升级可以说是自动的并且是透明的(Java Web Start也可以用来解决这个问题)。如果你是在这样的Intranet中工作,那么最轻松的就是你可以使用已经存在的代码,而不用再用新的语言重新编写。

   

    当你试图选择一种客户端编程的解决方案时,最好的方法就是分析它们的性价比。思考问题中存在的约束条件,找出最快捷的解决方法。因为客户端程序设计仍是编程,所以进行快速开发是有必要的。为那些在程序开发中不可避免的问题提早做准备是一种积极的态度。

   

1.13.3 服务器端编程

 

    至此我们还没有讨论关于服务器端程序设计的话题,而这也是Java最成功的地方。当你向服务器发送请求时到底发生了什么?大多数请求都只是这个样子给我传来一份文件。然后浏览器就会将传来的文件解释成某种适当的格式:HTML页面、图形图像、Java applet、脚本程序,等等。

   

    复杂的请求通常会涉及到数据库事务。常见的情况就是向服务器发送一个执行复杂的数据库查询的请求,然后服务器将查询结果组织成HTML页面的格式回传给你。(当然,如果客户机上使用了Java或者脚本语言,那么原始数据可以在传送到客户机之后再组织其格式,这样更快,也更减轻服务器的压力。)或者当你加入了一个团体或者是下了一份订单,你想在数据库中注册你的名字,那这就涉及到数据库的更新操作。这些数据库请求都需要在服务器端进行处理,也就是所谓的服务器端编程。以前,服务器端编程通常是用PerlPythonC++或者其他的一些语言来创建CGI程序,但这也导致出现了更加复杂的系统。其中就包括了基于JavaWeb服务器,它可以通过用Java编写的servlet来实现服务器端编程。Servlet和它的衍生物JSP,这两者正是各个公司在开发网站的时候转向使用Java的主要原因,因为它们能够消除由于各个浏览器之间存在差异所带来的影响。关于服务器端编程的内容将在《Thinking in Enterprise Java》中介绍。

   

1.14 总结

 

    一个过程式的程序就是数据定义加函数调用。为了弄面白这样的一个程序是干什么的,你必须对它进行深入思考,仔细观察它的函数调用,根据底层的概念在头脑中建立一个模型。这正是我们在设计这种过程式程序时需要中间表现形式的原因。由于它们使用的表达方式更加面向底层的计算机而不是要解决的问题,所以这些程序总是弄得人们云里雾里。

   

    由于面向对象程序设计在过程式语言的上面增加了新的概念,你自然就会觉得用Java编写的程序要比类似的过程式程序复杂的多。然而你会大吃一惊:一个编写良好的Java程序实际上要比过程式程序更加容易理解也更加简单。你所看到的对象的定义,恰好体现出了问题域中的概念(而不是对底层机器的表现),而向对象传递的消息也体现出了问题域中存在的活动。面向对象程序设计的一大亮点就是,一个设计良好的程序,通过阅读它的代码就可以很容易的理解它的工作。当然也有一些你看不到的代码,因为许多问题都需要重用类库中的代码来解决。

   

    OOPJava也并不是处处都适用。应当正确评估你的需求,判断Java是否是最好的解决方案,或者是使用其他的编程系统(包括继续沿用你正在使用的)。如果你认为你的需求在可预见的未来中会产生特殊的变化,而且Java也无法满足你的一些特殊要求,那就应该选择其他的解决方案(我推荐使用Python,见www.Python.org)。如果你仍然选择Java,那你至少要清楚还有哪些方案可选,并且要清楚的认识到是哪些理由促使你这样做。