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 里的各条记录相连
这才是Java用 package的名字寻找cass文件的地方。(此外,它还 会根据Java解释器所在的位置查找一些标准目录。) 为了能讲得更清楚,就拿我的域名 bruceeckel,com举例。它倒过来 就是 com bruceeckel,这样我写的类就有了全球唯一的名字了。(过 去,com,edu,org这些扩展,在Java的 package名字里面是要大 写的,但是Java2作了改进,所以现在 package的名字都是小写的。) 我还可以进一步分下去,创建一个名为 simple的类库,所以 package 的名字是 现在,你就能用这个 package的名字来管下面这两个文件了 //: com: bruceeckel: simple: Vector. java // Creating a package package com. bruceeckel simple public class Vector public Vector()I System. out. println("com. bruceeckel simple Vector")i }///:~ 等到你要自己写 package的时候,你就会发现, package语句必须是 文件里的第一个非注释行。第二个文件看上去非常相似 com: bruceeckel: simple: List. java // Creating a package package com. bruceeckel simple ublic class list public List()t System. out. println("com. bruceeckel simple. List")i }///:~ 在我的机器上这两个文件都放在这个子目录里 C: \DOC\Javar\com\bruceeckel\simple 第6页共23页 www.wgqgh.com/shhgs/tij.html email:shhes@sohu.com
Chapter 5: Hiding the Implementation 第 6 页 共 23 页 www.wgqqh.com/shhgs/tij.html email: shhgs@sohu.com 这才是 Java 用 package 的名字寻找.class 文件的地方。(此外,它还 会根据 Java 解释器所在的位置查找一些标准目录。) 为了能讲得更清楚,就拿我的域名 bruceeckel.com 举例。它倒过来 就是 com.bruceeckel,这样我写的类就有了全球唯一的名字了。(过 去,com,edu,org 这些扩展,在 Java 的 package 名字里面是要大 写的,但是 Java 2 作了改进,所以现在 package 的名字都是小写的。) 我还可以进一步分下去,创建一个名为 simple 的类库,所以 package 的名字是: package com.bruceeckel.simple; 现在,你就能用这个 package 的名字来管下面这两个文件了: //: com:bruceeckel:simple:Vector.java // Creating a package. package com.bruceeckel.simple; public class Vector { public Vector() { System.out.println("com.bruceeckel.simple.Vector"); } } ///:~ 等到你要自己写 package 的时候,你就会发现,package 语句必须是 文件里的第一个非注释行。第二个文件看上去非常相似: //: com:bruceeckel:simple:List.java // Creating a package. package com.bruceeckel.simple; public class List { public List() { System.out.println("com.bruceeckel.simple.List"); } } ///:~ 在我的机器上这两个文件都放在这个子目录里: C:\DOC\JavaT\com\bruceeckel\simple
Thinking in Java 3Edition 只要看一遍这个路径,你就会发现 package的名字 com. bruceeckel. simple,但是这个路径的前面部分又是什么呢?这 是由 CLASSPATH环境变量控制的,在我的机器上,它是: CLASSPATH=iD: \JAVA\LIB; C: \DOC\JavaT 你会看到 CLASSPATH可以有好几个可供选择的搜索路径 但是,使用JAR文件的时候会有一点变化。除了要告诉它该到哪里去找 这个JAR文件,你必须将文件名放到 CLASSPATH里面。所以对名为 grape.Jar的JAR来说, CLASSPATH应该包括 CLASSPATH=; D: \JAVA\LIB; C: \flavors\grape. jar 设完 CLASSPATH之后,下面这个文件就可以放在任何目录里了 //: c05: LibTest java / Uses the lib import com. bruceeckel. simpletest. import com. bruceeckel simple public class LibTest static Test monitor new Test( public static void main(String[] args) i Vector v new Vector(i monitor. expect(new String[] le vector " com. bruceeckel simple List }/// 当编译器碰到了 simple类库的 import语句的时候,它就开始在 CLASSPATH所给出的目录下搜索,先找com\ bruceeckel\simple子 目录,再找编译后的文件( Vector就找 Vector class,List就找 List class)。注意 Vector和List类,以及其中要用的方法都必须是 public的。 对Java的初学者来说,设置 CLASSPATH曾经是一桩非常棘手的事(至 少我开始的时候是这样的),所以Sun在Java2的JDK里面作了一些改 进,让它变得稍微智能一些。你会发觉安装之后,即使不设置 CLASSPATH,它也能编译和运行一些基本的Java程序。然而要编译和 第7页共23页 www.wgqqh.com/shhgs/tij.html emailshhgs@sohu.com
Thinking in Java 3rd Edition 第 7 页 共 23 页 www.wgqqh.com/shhgs/tij.html email: shhgs@sohu.com 只要看一遍这个路径,你就会发现 package 的名字 com.bruceeckel.simple,但是这个路径的前面部分又是什么呢?这 是由 CLASSPATH 环境变量控制的,在我的机器上,它是: CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT 你会看到 CLASSPATH 可以有好几个可供选择的搜索路径。 但是,使用 JAR 文件的时候会有一点变化。除了要告诉它该到哪里去找 这个 JAR 文件,你必须将文件名放到 CLASSPATH 里面。所以对名为 grape.jar 的 JAR 来说,CLASSPATH 应该包括: CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar 设完 CLASSPATH 之后,下面这个文件就可以放在任何目录里了: //: c05:LibTest.java // Uses the library. import com.bruceeckel.simpletest.*; import com.bruceeckel.simple.*; public class LibTest { static Test monitor = new Test(); public static void main(String[] args) { Vector v = new Vector(); List l = new List(); monitor.expect(new String[] { "com.bruceeckel.simple.Vector", "com.bruceeckel.simple.List" }); } } ///:~ 当编译器碰到了 simple 类库的 import 语句的时候,它就开始在 CLASSPATH 所给出的目录下搜索,先找 com\bruceeckel\simple 子 目录,再找编译后的文件(Vector 就找 Vector.class,List 就找 List.class)。注意 Vector 和 List 类,以及其中要用的方法都必须是 public 的。 对 Java 的初学者来说,设置 CLASSPATH 曾经是一桩非常棘手的事(至 少我开始的时候是这样的),所以 Sun 在 Java 2 的 JDK 里面作了一些改 进,让它变得稍微智能一些。你会发觉安装之后,即使不设置 CLASSPATH,它也能编译和运行一些基本的 Java 程序。然而要编译和
Chapter 5: Hiding the Implementation 运行本书的源代码(可以从www.BruceEcke!com下载),你就必须将这 些代码的根目录加到 CLASSPATH里面。 冲突 如果两个“*”所引入的类库都包括一个同名的类,那又会怎样呢?举例 来说,假设有个程序 import com. bruceeckel simple.*i import java.util.*i 由于 java. util.*也包括了一个 Vector类,因此这就有可能会引发冲 突。然而,只要你不写会引起冲突的代码,一切会OK——这种做法很 好,因为不然的话,你得为了避免根本不可能发生的冲突而多写很多代 码 但是如果你要创建一个 Vector的话,冲突就真的会来了: Vector v= new vector o 你指的是那个 Vector类呢?编译器不知道,读代码的人也不知道。所以 编译器就报错了,它会要你明确地指明这是哪个类。比方说,如果我要使 用Java标准的 Vector,我就必须说: java util. Vector v new javautil. Vector()i 由于这种写法(再加上 CLASSPATH)已经能完全指明 Vector的位置 了,因此除非你还要使用 java. util的其它类,否则就不必再使用 import java.util*。 一个自定义的工具类库 有了这些知识,你就能创建你自己的工具类库以减少甚至彻底消除重复代 码了。假设我们要为 System. out. printin()创建一个别名以减少打 字的量。这可以是 tools package的一部分 com: bruceeckel: tools: P java / The P rint P. rintln shorthand package com. bruceeckel. tools; public class P public static void rint(string s)t 第8页共23页 www.wgqgh.com/shhgs/tij.html email:shhes@sohu.com
Chapter 5: Hiding the Implementation 第 8 页 共 23 页 www.wgqqh.com/shhgs/tij.html email: shhgs@sohu.com 运行本书的源代码(可以从 www.BruceEckel.com 下载),你就必须将这 些代码的根目录加到 CLASSPATH 里面。 冲突 如果两个“*”所引入的类库都包括一个同名的类,那又会怎样呢?举例 来说,假设有个程序: import com.bruceeckel.simple.*; import java.util.*; 由于 java.util.* 也包括了一个 Vector 类,因此这就有可能会引发冲 突。然而,只要你不写会引起冲突的代码,一切会 OK——这种做法很 好,因为不然的话,你得为了避免根本不可能发生的冲突而多写很多代 码。 但是如果你要创建一个 Vector 的话,冲突就真的会来了: Vector v = new Vector(); 你指的是那个 Vector 类呢?编译器不知道,读代码的人也不知道。所以 编译器就报错了,它会要你明确地指明这是哪个类。比方说,如果我要使 用 Java 标准的 Vector,我就必须说: java.util.Vector v = new java.util.Vector(); 由于这种写法(再加上 CLASSPATH)已经能完全指明 Vector 的位置 了,因此除非你还要使用 java.util 的其它类,否则就不必再使用 import java.util.*。 一个自定义的工具类库 有了这些知识,你就能创建你自己的工具类库以减少甚至彻底消除重复代 码了。假设我们要为 System.out.println( ) 创建一个别名以减少打 字的量。这可以是 tools package 的一部分: //: com:bruceeckel:tools:P.java // The P.rint & P.rintln shorthand. package com.bruceeckel.tools; public class P { public static void rint(String s) {
Thinking in Java 3Edition System. out. print(s)i public static void rintln(String s)( System. out. println(s) }///:~ 这种简写形式既能以加换行符的形式(P, tintIn(),也能以不加换行符的 形式( Print(打印 String。 你能猜到,这个文件一定是位于 CLASSPATH的某个目录的 com/ bruceeckel/ tools子目录下。编译之后,你就能在系统的任何 地方用 import语句引入 P class文件了 CO5: TooITest. java Uses the tools library import com. bruceeckel tools* import com. bruceeckel simpletest. public class ToolTest static rest monit public static void main(string[] args)t P. rintln("Available from now P.rintln(""+100);// Force it to be a string P.rint1n(""+3.14159) monitor. expect(new String[] 'Available from now on "100", 3.14159 }///:~ 注意,不论哪种对象,只要放进了 String表达式,它就会被强制转化为 这个对象的 String表示形式了;在上述程序中,把空的 String放进表 达式就是为了达到这个目的。但是这却让我们注意到了一个有趣的现象。 如果你用 System. out. println(100)的方式进行调用,那么它就不会 把参数转换成 String了。经过一番特别的重载之后,你可以也可以让P 具备这样的功能(这是本章练习的要求)。 所以从现在开始,只要你写了什么新的,能派上用场的工具,你就可以把 它加到你自己的 tools或util目录。 使用 import来改变程序的行为方式 第9页共23页 www.wgqqh.com/shhgs/tij.html emailshhgs@sohu.com
Thinking in Java 3rd Edition 第 9 页 共 23 页 www.wgqqh.com/shhgs/tij.html email: shhgs@sohu.com System.out.print(s); } public static void rintln(String s) { System.out.println(s); } } ///:~ 这种简写形式既能以加换行符的形式(P.rintln()),也能以不加换行符的 形式(P.rint())打印 String。 你能猜到,这个文件一定是位于 CLASSPATH 的某个目录的 com/bruceeckel/tools 子目录下。编译之后,你就能在系统的任何 地方用 import 语句引入 P.class 文件了: //: c05:ToolTest.java // Uses the tools library. import com.bruceeckel.tools.*; import com.bruceeckel.simpletest.*; public class ToolTest { static Test monitor = new Test(); public static void main(String[] args) { P.rintln("Available from now on!"); P.rintln("" + 100); // Force it to be a String P.rintln("" + 100L); P.rintln("" + 3.14159); monitor.expect(new String[] { "Available from now on!", "100", "100", "3.14159" }); } } ///:~ 注意,不论哪种对象,只要放进了 String 表达式,它就会被强制转化为 这个对象的 String 表示形式了;在上述程序中,把空的 String 放进表 达式就是为了达到这个目的。但是这却让我们注意到了一个有趣的现象。 如果你用 System.out.println(100)的方式进行调用,那么它就不会 把参数转换成 String 了。经过一番特别的重载之后,你可以也可以让 P 具备这样的功能(这是本章练习的要求)。 所以从现在开始,只要你写了什么新的,能派上用场的工具,你就可以把 它加到你自己的 tools 或 util 目录。 使用 import 来改变程序的行为方式
Chapter 5: Hiding the Implementation Java没有实现C的“条件编译( conditiona/ compilation)”。所谓条件 编译就是,不用修改源代码,只用一个开关就能让程序产生不同的行为方 式。Java之所以要剔除这个特性,可能是因为它主要是用来解决C的跨 平台的问题的:程序的不同部分会根据平台的不同而采用不同的编译方 式。由于Java的初衷就是要跨平台,因此这种特性就变得多余了 但是条件编译还有一些别的很有价值的用途。最常见的就是用它来调试代 码。调试功能在开发版里是能用的,但在正式版里则被禁了。你可以通过 修改 package来切换调试版和发布版所使用的代码,来达到上述目的 这种技巧能用于任何类型的“有条件的代码( conditional code)”。 使用 package的忠告 值得注意的是,每次创建 package给它起名的时候,你也隐含地设置了 个目录结构。这个 package必须保存在由它的名字所指示的目录里 而这个目录又必须在 CLASSPATH下面。刚开始做 package的实验的 时候,可能会让人觉得有些泄气,因为除非你严格遵守了 package的名 字就是目录路径这一规则,否则即便这个类就呆在同一个目录里,你也会 得到一大串莫名其妙的,告诉你找不到这类的消息。如果你得到这种消 息,就先把 package语句注释掉,如果它能运行了,你就知道问题出在 哪里了。 Java的访问控制符 编程的时候, public, protected以及 private这三个Java访问控 制符,应该放在类的每个成员的定义部分的前面,不管这个成员是数据还 是方法。一个访问控制符只管它所定义的这一项。这同C++形成了鲜明 的对比。C++的访问控制符会一直管下去,直到出现另一个。 每样东西都会有一个访问控制符,不是这个就是那个。下面我们就从默认 的访问权限开始,学习各种访问控制符的权限。 package访问权限 如果像本章之前的程序那样,根本就不给访问控制符,那情况又会如何 呢?默认的访问权限没有关键词,但通常还是把它称为 package权限 ( package access,有时也称为“ friendly”)。它的意思是,所有同属 这个 package的类都能访问这个成员,但是对那些不属于这个 package 的类来说,这个成员就是 private的了。由于编译单元—也就是源文 件——只能属于一个 package,因此同一个编译单元里的各个类,自动 就能通过 package权限进行相互访问了。 package权限能让你将相互关联的类组织成 package,这样它们之间就 能很方便地进行访问了。当你把类放到 package的时候,也就是说赋予 第10页共23 www.wgqgh.com/shhgs/tij.html email:shhes@sohu.com
Chapter 5: Hiding the Implementation 第 10 页 共 23 www.wgqqh.com/shhgs/tij.html email: shhgs@sohu.com Java 没有实现 C 的“条件编译(conditional compilation)”。所谓条件 编译就是,不用修改源代码,只用一个开关就能让程序产生不同的行为方 式。Java 之所以要剔除这个特性,可能是因为它主要是用来解决 C 的跨 平台的问题的:程序的不同部分会根据平台的不同而采用不同的编译方 式。由于 Java 的初衷就是要跨平台,因此这种特性就变得多余了。 但是条件编译还有一些别的很有价值的用途。最常见的就是用它来调试代 码。调试功能在开发版里是能用的,但在正式版里则被禁了。你可以通过 修改 package 来切换调试版和发布版所使用的代码,来达到上述目的。 这种技巧能用于任何类型的“有条件的代码(conditional code)”。 使用 package 的忠告 值得注意的是,每次创建 package 给它起名的时候,你也隐含地设置了 一个目录结构。这个 package 必须保存在由它的名字所指示的目录里, 而这个目录又必须在 CLASSPATH 下面。刚开始做 package 的实验的 时候,可能会让人觉得有些泄气,因为除非你严格遵守了 package 的名 字就是目录路径这一规则,否则即便这个类就呆在同一个目录里,你也会 得到一大串莫名其妙的,告诉你找不到这类的消息。如果你得到这种消 息,就先把 package 语句注释掉,如果它能运行了,你就知道问题出在 哪里了。 Java 的访问控制符 编程的时候,public,protected 以及 private 这三个 Java 访问控 制符,应该放在类的每个成员的定义部分的前面,不管这个成员是数据还 是方法。一个访问控制符只管它所定义的这一项。这同 C++形成了鲜明 的对比。C++的访问控制符会一直管下去,直到出现另一个。 每样东西都会有一个访问控制符,不是这个就是那个。下面我们就从默认 的访问权限开始,学习各种访问控制符的权限。 package 访问权限 如果像本章之前的程序那样,根本就不给访问控制符,那情况又会如何 呢?默认的访问权限没有关键词,但通常还是把它称为 package 权限 (package access,有时也称为“friendly”) 。它的意思是,所有同属 这个 package 的类都能访问这个成员,但是对那些不属于这个 package 的类来说,这个成员就是 private 的了。由于编译单元——也就是源文 件——只能属于一个 package,因此同一个编译单元里的各个类,自动 就能通过 package 权限进行相互访问了。 package 权限能让你将相互关联的类组织成 package,这样它们之间就 能很方便地进行访问了。当你把类放到 package 的时候,也就是说赋予