Thinking in Java 3Edition 致读者: 我从2002年7月开始翻译这本书,当时还是第二版。但是翻完前言和介绍部分 后, chinapub就登出广告,说要出版侯捷的译本。于是我中止了翻译,等着侯 先生的作品。 我是第一时间买的这本书,但是我失望了。比起第一版,我终于能看懂这本书 了,但是相比我的预期,它还是差一点。所以当 Bruce Eckel在他的网站上公开 本书的第三版的时候,我决定把它翻译出来 说说容易,做做难。一本1000多页的书不是那么容易翻的。期间我也曾打过退 堂鼓,但最终还是全部翻译出来了。从今年的两月初起,到7月底,我几乎放 弃了所有的业余时间,全身心地投入本书的翻译之中。应该说,这项工作的难 度超出了我的想像。 首先,读一本书和翻译一本书完全是两码事。英语与中文是两种不同的语言, 用英语说得很畅的句子,翻成中文之后就完全破了相。有时我得花好几分钟 用中文重述一句我能用几秒钟读懂的句子。更何况作为读者,一两句话没搞 懂,并不影响你理解整本书,但对译者来说,这就不一样了。 其次,这是一本讲英语的人写给讲英语的人的书,所以同很多要照顾非英语读 者的技术文档不同,它在用词,句式方面非常随意。英语读者会很欣赏这 点,但是对外国读者来说,这就是负担了 再有, Bruce Eckel这样的大牛人,写了1000多页,如果都让你读懂,他岂不是 太没面子?所以,书里还有一些很有“禅意”的句子。比如那句著名的“The genesis of the computer revolution was in a machine. The genesis of our programming languages thus tends to look like that machine.”我就一直没吃准该怎 么翻译。我想大概没人能吃准,说不定 Bruce要的就是这个效果。 这是一本公认的名著,作者在技术上的造诣无可挑剔。而作为译者,我的编程 能力差了很多。再加上上面讲的这些原因,使得我不得不格外的谨慎。当我重 读初稿的时候,我发现需要修改的地方实在太多了。因此,我不能现在就公开 全部译稿,我只能公开已经修改过的部分。不过这不是最终的版本,我还会继 续修订的。 本来,我准备到10月份,等我修改完前7章之后再公开。但是,我发现我又有 点要放弃了,因此我决定给自己一点压力,现在就公开。以后,我将修改完 章就公开一章,请关注www.wgqgh.com/shhgs/tii.html 如果你觉得好,请给告诉我,你的鼓励是我工作的动力;如果你觉得不好,那 就更应该告诉我了,我会参考你的意见作修改的。我希望能通过这种方法,译 出一本配得上原著的书 2003年9月8日 第1页共23页 w. wgqqh. com/shhgs/tij. h emailshhgs@sohu.com
Thinking in Java 3rd Edition 第 1 页 共 23 页 www.wgqqh.com/shhgs/tij.html email: shhgs@sohu.com 致读者: 我从 2002 年 7 月开始翻译这本书,当时还是第二版。但是翻完前言和介绍部分 后,chinapub 就登出广告,说要出版侯捷的译本。于是我中止了翻译,等着侯 先生的作品。 我是第一时间买的 这本书,但是我失望了。比起第一版,我终于能看懂这本书 了,但是相比我的预期,它还是差一点。所以当 Bruce Eckel 在他的网站上公开 本书的第三版的时候,我决定把它翻译出来。 说说容易,做做难。一本 1000 多页的书不是那么容易翻的。期间我也曾打过退 堂鼓,但最终还是全部翻译出来了。从今年的两月初起,到 7 月底,我几乎放 弃了所有的业余时间,全身心地投入本书的翻译之中。应该说,这项工作的难 度超出了我的想像。 首先,读一本书和翻译一本书完全是两码事。英语与中文是两种不同的语言, 用英语说得很畅的句子,翻成中文之后就完全破了相。有时我得花好几分钟, 用中文重述一句我能用几秒钟读懂的句子。更何况作为读者,一两句话没搞 懂,并不影响你理解整本书,但对译者来说,这就不一样了。 其次,这是一本讲英语的人写给讲英语的人的书,所以同很多要照顾非英语读 者的技术文档不同,它在用词,句式方面非常随意。英语读者会很欣赏这一 点,但是对外国读者来说,这就是负担了。 再有,Bruce Eckel 这样的大牛人,写了 1000 多页,如果都让你读懂,他岂不是 太没面子?所以,书里还有一些很有“禅意”的句子。比如那句著名的“The genesis of the computer revolution was in a machine. The genesis of our programming languages thus tends to look like that machine.”我就一直没吃准该怎 么翻译。我想大概没人能吃准,说不定 Bruce 要的就是这个效果。 这是一本公认的名著,作者在技术上的造诣无可挑剔。而作为译者,我的编程 能力差了很多。再加上上面讲的这些原因,使得我不得不格外的谨慎。当我重 读初稿的时候,我发现需要修改的地方实在太多了。因此,我不能现在就公开 全部译稿,我只能公开已经修改过的部分。不过这不是最终的版本,我还会继 续修订的。 本来,我准备到 10 月份,等我修改完前 7 章之后再公开。但是,我发现我又有 点要放弃了,因此我决定给自己一点压力,现在就公开。以后,我将修改完一 章就公开一章,请关注 www.wgqqh.com/shhgs/tij.html。 如果你觉得好,请给告诉我,你的鼓励是我工作的动力;如果你觉得不好,那 就更应该告诉我了,我会参考你的意见作修改的。我希望能通过这种方法,译 出一本配得上原著的书。 shhgs 2003 年 9 月 8 日
Chapter 5: Hiding the Implementation 5:隐藏实现 在面向对象的设计中,最关键的问题就是“将会变和不会变的东西分离开 来 这一点对类库尤为重要。类库的使用者(客户程序员)应该能完全仰赖类 库,他们知道,即使类库出了新版本,他们也不必重写代码。另一方面 类库的创建者也应该可以在确保不影响客户程序员代码的前提下,保留对 类库作修正和改进的权利 要达到上述目的,可以使用约定。比方说,类库的开发人员必须遵守:修 改类的时候不删除现有的方法,因为这可能会影响客户程序员的代码。但 是还有一些更棘手的问题。就拿成员数据来说,类库的开发人员又怎么知 道客户程序员会使用哪些数据呢?对于那些只与类的内部实现有关的,不 应该让客户程序员使用的方法来说,情况也一样。但是,如果类库的开发 人员想用一种新的实现来替换旧的,那他又该怎么做呢?对类的任何修改 都可能会破坏客户程序员的代码。这样,类库的开发人员就被套上了紧箍 咒,什么都不能改了。 为了解决这个问题,Java提供了访问控制符( access specifier),这样类 库的开发人员能告诉客户程序员,他们能用什么,不能用什么了。访问控 制权限从松到紧依次是 public, protected, package权限(也就是不 给任何关键词),以及 private。读了上面那段,你可能会认为,作为类 库的设计者,你应该尽可能的把所有东西都做成“ private”的,并且只 公开你想让客户程序员使用的方法。完全正确!尽管对于那些用其它语言 (特别是C编程,并且已经习惯了不受限制地访问任何东西的人来说,这 么做通常是有违常理的。读过本章之后,你就会对Java的访问控制更有 信心了。 但是,什么是组件类库( library of component)以及怎样去控制“谁能 访问类库中组件”的问题还没有完全解决。还有一个问题,就是组件是怎 样被捆绑成一个联系紧密的类库单元的。这是由Java的 package关键 词控制的,此外类是不是属于同一个 package,还会对访问控制符产生 影响。所以,我们将从怎样将类库组件( library components)放入 package里入手,开始本章的学习。接下来,你就能完全理解访问控制 符的意思了。 package:类库的单元 当你使用 import关键词引入一个完整的类库的时候,这个 package就 能为你所用了,例如 import java.util.* 第2页共23页 www.wgqgh.com/shhgs/tij.html email:shhes@sohu.com
Chapter 5: Hiding the Implementation 第 2 页 共 23 页 www.wgqqh.com/shhgs/tij.html email: shhgs@sohu.com 5: 隐藏实现 在面向对象的设计中,最关键的问题就是“将会变和不会变的东西分离开 来。” 这一点对类库尤为重要。类库的使用者(客户程序员)应该能完全仰赖类 库,他们知道,即使类库出了新版本,他们也不必重写代码。另一方面, 类库的创建者也应该可以在确保不影响客户程序员代码的前提下,保留对 类库作修正和改进的权利。 要达到上述目的,可以使用约定。比方说,类库的开发人员必须遵守:修 改类的时候不删除现有的方法,因为这可能会影响客户程序员的代码。但 是还有一些更棘手的问题。就拿成员数据来说,类库的开发人员又怎么知 道客户程序员会使用哪些数据呢?对于那些只与类的内部实现有关的,不 应该让客户程序员使用的方法来说,情况也一样。但是,如果类库的开发 人员想用一种新的实现来替换旧的,那他又该怎么做呢?对类的任何修改 都可能会破坏客户程序员的代码。这样,类库的开发人员就被套上了紧箍 咒,什么都不能改了。 为了解决这个问题,Java 提供了访问控制符(access specifier),这样类 库的开发人员能告诉客户程序员,他们能用什么,不能用什么了。访问控 制权限从松到紧依次是 public,protected,package 权限(也就是不 给任何关键词),以及 private。读了上面那段,你可能会认为,作为类 库的设计者,你应该尽可能的把所有东西都做成“private”的,并且只 公开你想让客户程序员使用的方法。完全正确!尽管对于那些用其它语言 (特别是 C)编程,并且已经习惯了不受限制地访问任何东西的人来说,这 么做通常是有违常理的。读过本章之后,你就会对 Java 的访问控制更有 信心了。 但是,什么是组件类库(library of component)以及怎样去控制“谁能 访问类库中组件”的问题还没有完全解决。还有一个问题,就是组件是怎 样被捆绑成一个联系紧密的类库单元的。这是由 Java 的 package 关键 词控制的,此外类是不是属于同一个 package,还会对访问控制符产生 影响。所以,我们将从怎样将类库组件(library components)放入 package 里入手,开始本章的学习。接下来,你就能完全理解访问控制 符的意思了。 package: 类库的单元 当你使用 import 关键词引入一个完整的类库的时候,这个 package 就 能为你所用了,例如 import java.util.*;
Thinking in Java 3d Edition 会把Java标准版里的工具类库( utility library)全都引进来。比如, java.util里面有一个 Array List类,因此你既可以用全名 java.util. Array List(这样就用不着 import语句了),也可以直接写 Array List了(因为已经有了 import) 如果你只想引入一个类,那你可以在 import语句里面指名道姓地引用 类了 import java.util. ArrayList 现在你就可以直接使用 Array List而不用添加任何限定词了。但是 java.util:里面的其它的类就不能用了 之所以要使用 import,是因为它提供了一种管理名字空间(name spaces)的机制。类的所有成员的名字都是相互独立的。A类里面的f() 方法不会同B类里面,有着相同“调用特征( signiture,即参数列表)” 的f()相冲突。但是类的名字呢?假设你创建了一个 Stack类,并且把 它装到一台已经有了一个别人写的 Stack类的机器上,那又会发生什么 事呢?Java之所以要对名字空间拥有完全的控制,就是要解决这种潜在 的名字冲突,并且能不受 Internet的束缚,创建出完全唯一的名字。 到目前为止,本书所举的都是单文件的例子,而且都是在本地运行的,因 此没必要使用 package.。(在这种情况下,类的名字是放在“ default package”的名下的。)当然这也是一种做法,而且为了简单起见,本书 的其余章节也尽可能使用这种方法。但是,如果你打算创建一个,能同机 器上其它Java程序相互兼容的类库或程序,你就得考虑一下如何避免名 字冲突了。 ava的源代码文件通常被称为编译单元( compilation unit有时也称翻 译单元 translation unit)。每个编译单元都必须是一个以java结尾的 文件,而且其中必须有一个与文件名相同的 public类(大小写也必须相 同,但是不包括java的文件扩展名)。每个编译单元只能有一个 public类,否则编译器就会报错。如果编译单元里面还有别的类,那么 这些类就成了这个主要的 public的类的“辅助”类了,这是因为它们都 不是 public的,因此对外面世界来说它们都是看不到的。 编译java文件的时候,它里面的每个类都会产生输出。其输出文件的名 字就是java文件里的类的名字,但是其扩展名是, class。这样,写不了 几个 . java文件就会产生一大堆 class文件。如果你有过用编译语言编 程的经验,那么你可能会对这个过程感到习以为常了:先用编译器生成 第3页共23页 www.wgqqh.com/shhgs/tij.html emailshhgs@sohu.com
Thinking in Java 3rd Edition 第 3 页 共 23 页 www.wgqqh.com/shhgs/tij.html email: shhgs@sohu.com 会把 Java 标准版里的工具类库(utility library)全都引进来。比如, java.util 里面有一个 ArrayList 类,因此你既可以用全名 java.util.ArrayList(这样就用不着 import 语句了),也可以直接写 ArrayList 了(因为已经有了 import)。 如果你只想引入一个类,那你可以在 import 语句里面指名道姓地引用 类了。 import java.util.ArrayList; 现在你就可以直接使用 ArrayList 而不用添加任何限定词了。但是 java.util 里面的其它的类就不能用了。 之所以要使用 import,是因为它提供了一种管理名字空间(name spaces)的机制。类的所有成员的名字都是相互独立的。A 类里面的 f( ) 方法不会同 B 类里面,有着相同“调用特征(signiture,即参数列表)” 的 f( )相冲突。但是类的名字呢?假设你创建了一个 Stack 类,并且把 它装到一台已经有了一个别人写的 Stack 类的机器上,那又会发生什么 事呢?Java 之所以要对名字空间拥有完全的控制,就是要解决这种潜在 的名字冲突,并且能不受 Internet 的束缚,创建出完全唯一的名字。 到目前为止,本书所举的都是单文件的例子,而且都是在本地运行的,因 此没必要使用 package。(在这种情况下,类的名字是放在“default package”的名下的。) 当然这也是一种做法,而且为了简单起见,本书 的其余章节也尽可能使用这种方法。但是,如果你打算创建一个,能同机 器上其它 Java 程序相互兼容的类库或程序,你就得考虑一下如何避免名 字冲突了。 Java 的源代码文件通常被称为编译单元(compilation unit 有时也称翻 译单元 translation unit)。每个编译单元都必须是一个以 .java 结尾的 文件,而且其中必须有一个与文件名相同的 public 类 (大小写也必须相 同,但是不包括 .java 的文件扩展名)。每个编译单元只能有一个 public 类,否则编译器就会报错。如果编译单元里面还有别的类,那么 这些类就成了这个主要的 public 的类的“辅助”类了,这是因为它们都 不是 public 的,因此对外面世界来说它们都是看不到的。 编译.java 文件的时候,它里面的每个类都会产生输出。其输出文件的名 字就是.java 文件里的类的名字,但是其扩展名是.class。这样,写不了 几个.java 文件就会产生一大堆.class 文件。如果你有过用编译语言编 程的经验,那么你可能会对这个过程感到习以为常了:先用编译器生成一
Chapter 5: Hiding the Implementation 大堆中间文件(通常是“obj”文件),然后再用 linker(创建可执行文件 或 librarian(创建类库)把这些中间文件封装起来。但是,Java不是这样 工作的。一个能正常工作的程序就是一大堆 class文件,当然也可以(用 Java的jar工具)把它们封装和压缩成 Java archive(JAR)文件。 Java解释器会负责寻找,装载和解释这些文件的。 类库就是一组类文件。每个文件都有一个 public类(不是一定要有 public类,但通常都是这样),因此每个文件都代表着一个组件。如果你 想把这些组件(都在它们自己的那个ava和cass文件里)都组织起 来,那就应该用 package关键词了 当你把 package mypackagei 放到文件开头的时候(如果要用 package,那么它必须是这个文件的第 个非注释的行),你就声明了,这个编译单元是 mypackage类库的 组成部分。或者换一种说法,你要表达的意思是,这个编译单元的 public类的名字是在 mypackage的名字之下的( under the umbrella of the name mypackage),任何想使用这个类的人必须使 用它的全名,或者用 import关键词把 mypackage引进来(用前面讲 的办法)。注意Java的约定是用全小写来表示 package的名字,中间单 词也不例外。 举例来说,假设这个文件的名字是 My class. java。于是文件里面可以 有,而且只能有一个 public类,而这个类的名字只能是 MyClass(大 小写都要相同) package mypackage ublic class MyClass i 现在如果有人想要用 Myclass,或者 mypackage里面的其它 public类,那他就必须使用 import关键词来引入 mypackage下的 名字了。还有一个办法,就是给出这个类的全名 mypackage MyClass m new mypackage MyClass()i 用 import可以让代码显得更清楚一点 import mypackage. 第4页共23页 www.wgqgh.com/shhgs/tij.html email:shhes@sohu.com
Chapter 5: Hiding the Implementation 第 4 页 共 23 页 www.wgqqh.com/shhgs/tij.html email: shhgs@sohu.com 大堆中间文件(通常是“obj”文件),然后再用 linker(创建可执行文件) 或 librarian(创建类库)把这些中间文件封装起来。但是,Java 不是这样 工作的。一个能正常工作的程序就是一大堆.class 文件,当然也可以(用 Java 的 jar 工具)把它们封装和压缩成 Java ARchive (JAR)文件。 Java 解释器会负责寻找,装载和解释[26]这些文件的。 类库就是一组类文件。每个文件都有一个 public 类 (不是一定要有 public 类,但通常都是这样),因此每个文件都代表着一个组件。如果你 想把这些组件(都在它们自己的那个.java 和.class 文件里)都组织起 来,那就应该用 package 关键词了。 当你把: package mypackage; 放到文件开头的时候 (如果要用 package,那么它必须是这个文件的第 一个非注释的行),你就声明了,这个编译单元是 mypackage 类库的 组成部分。或者换一种说法,你要表达的意思是,这个编译单元的 public 类的名字是在 mypackage 的名字之下的(under the umbrella of the name mypackage),任何想使用这个类的人必须使 用它的全名,或者用 import 关键词把 mypackage 引进来(用前面讲 的办法)。注意 Java 的约定是用全小写来表示 package 的名字,中间单 词也不例外。 举例来说,假设这个文件的名字是 MyClass.java。于是文件里面可以 有,而且只能有一个 public 类,而这个类的名字只能是 MyClass (大 小写都要相同): package mypackage; public class MyClass { // . . . 现在如果有人想要用 MyClass,或者 mypackage 里面的其它 public 类,那他就必须使用 import 关键词来引入 mypackage 下的 名字了。还有一个办法,就是给出这个类的全名: mypackage.MyClass m = new mypackage.MyClass(); 用 import 可以让代码显得更清楚一点: import mypackage.*;
Thinking in Java 3d Edition Myclass m new MyClass () 作为类库的设计者,你得记住, package和 import这两个关键词的作 用是要把一个单独的全局名字空间分割开来,这样不论 Internet上有多 少人在用Java编程,你就都不会碰到名字冲突的问题了 创建独一无二的 package名字 可能你也发现了,由于 package没有被真的“封装”成一个单独的文 件,而 package又是由很多, class文件组成的,因此事情就有点乱 了。要解决这个问题,较为明智的做法是把所有同属一个包的. class文 件都放到一个目录里;也就是利用操作系统的层次文件结构来解决这个问 题。这是Java解决这个问题的方法之一;后面要介绍的jar程序是另 个解决办法。 将 package的文件收进一个单独的子目录里还解决了另外两个问题:创 建独一无二的 package名字,以及帮助Java在复杂的目录结构中找到 它们。我们己经在第2章讲过了,这是通过将, class文件的路径信息放 到 package的名字里面来完成的。Java的约定是 package名字的第 部分应该是类的创建者的 Internet域名的反写。由于 Internet域名的 唯一性是有保证的,因此只要你遵守这个约定, package的名字就肯定 是唯一的,这样就不会有名字冲突的问题了。(除非你把域名让给了别 人,而他又用同一个域名来写Java程序。)当然,如果你还没有注册域 名,那你完全可以编一个(比如用你的姓和名),然后用它来创建 package的名字。如果你打算要发布Java程序,那么还是应该稍微花点 精力去搞个域名。 这个技巧的第二部分是把 package的名字映射到本地机器的目录,这样 当你启动Java程序,需要装载, class文件的时候(当程序需要创建某 个类的对象,或者第一次访问那个类的 static成员的时候,它会动态执 行这个过程的),它就知道该在哪个目录寻找这个, class文件了 Java解释器是这样工作的。首先,它要找到 CLASSPATH2环境变量 (这是通过操作系统设置的,有时Java安装程序或者Java工具的安装程 序会为你设置)。 CLASSPATH包含了一个或多个目录,这些目录会被当 作根目录供Java搜索. class文件。从这个根目录出发,解释器会将 package名字里的每个点都换成斜杠(因此,根据操作系统的不同, package foo bar. baz就被转换成foo\bar\baz或 foo/bar/baz,或者其它可能的形式),这样它生成了以 CLASSPATH 为根的相对路径。然后这些路径再与 CLASSPATH里的各条记录相连 第5页共23页 w. wgqqh. com/shhgs/tij. h emailshhgs@sohu.com
Thinking in Java 3rd Edition 第 5 页 共 23 页 www.wgqqh.com/shhgs/tij.html email: shhgs@sohu.com // . . . MyClass m = new MyClass(); 作为类库的设计者,你得记住,package 和 import 这两个关键词的作 用是要把一个单独的全局名字空间分割开来,这样不论 Internet 上有多 少人在用 Java 编程,你就都不会碰到名字冲突的问题了。 创建独一无二的 package 名字 可能你也发现了,由于 package 没有被真的“封装”成一个单独的文 件,而 package 又是由很多 .class 文件组成的,因此事情就有点乱 了。要解决这个问题,较为明智的做法是把所有同属一个包的 .class 文 件都放到一个目录里;也就是利用操作系统的层次文件结构来解决这个问 题。这是 Java 解决这个问题的方法之一;后面要介绍的 jar 程序是另一 个解决办法。 将 package 的文件收进一个单独的子目录里还解决了另外两个问题:创 建独一无二的 package 名字,以及帮助 Java 在复杂的目录结构中找到 它们。我们已经在第 2 章讲过了,这是通过将 .class 文件的路径信息放 到 package 的名字里面来完成的。Java 的约定是 package 名字的第 一部分应该是类的创建者的 Internet 域名的反写。由于 Internet 域名的 唯一性是有保证的,因此只要你遵守这个约定,package 的名字就肯定 是唯一的,这样就不会有名字冲突的问题了。(除非你把域名让给了别 人,而他又用同一个域名来写 Java 程序。)当然,如果你还没有注册域 名,那你完全可以编一个(比如用你的姓和名),然后用它来创建 package 的名字。如果你打算要发布 Java 程序,那么还是应该稍微花点 精力去搞个域名。 这个技巧的第二部分是把 package 的名字映射到本地机器的目录,这样 当你启动 Java 程序,需要装载 .class 文件的时候 (当程序需要创建某 个类的对象,或者第一次访问那个类的 static 成员的时候,它会动态执 行这个过程的),它就知道该在哪个目录寻找这个 .class 文件了。 Java 解释器是这样工作的。首先,它要找到 CLASSPATH[27]环境变量 (这是通过操作系统设置的,有时 Java 安装程序或者 Java 工具的安装程 序会为你设置)。 CLASSPATH 包含了一个或多个目录,这些目录会被当 作根目录供 Java 搜索.class 文件。从这个根目录出发,解释器会将 package 名字里的每个点都换成斜杠 (因此,根据操作系统的不同, package foo.bar.baz 就被转换成 foo\bar\baz 或 foo/bar/baz,或者其它可能的形式),这样它生成了以 CLASSPATH 为根的相对路径。然后这些路径再与 CLASSPATH 里的各条记录相连