第五章隐藏具体实现在面向对象设计中,要考虑的一个基本问题是“如何将变动的事物与保持不变的事物相互隔离”。这对程序库(library)而言尤为重要。该程序库的使用者(客户端程序员,clientprogrammer)必须能够信赖他所使用的那部分程序库,并且能够知道如果程序库出现了新版本,他们并不需要改写代码。从另一个方面来说,程序库的开发者必须有权限进行修改和改进,并确保客户代码不会因为这些改动而受到影响。这一目标可以通过达成协定来加以实现。例如,程序库开发者必须同意在改动程序库中的class时不得删除任何现有方法,因为那样会破坏客户端程序员的代码。但是,与之相反的情况会更加棘手。在有域存在的情况下,程序库开发者要怎样才能知道究竟都有哪些域已经被客户端程序员所调用了呢?这对于方法仅为类的实现的一部分,因此并不想让客户端程序员直接使用的情况来说同样如此。但如果程序开发者想要移除旧的实现而要添加新的实现时,结果将会怎样呢?改动任何一个成员都有可能破坏客户端程序员的代码。于是程序库开发者会手脚被缚,无法对任何事物进行改动。为了解决这一问题,Java提供了访问权限修饰词(accessspecifier)供程序库开发人员来向客户端程序员指明哪些是可用的,哪些是不可用的。访问权限控制的等级,从最大权限到最小权限依次为:public,protected,包访问权限(没有关键词),和private。根据前述内容,你可能会认为,作为一名程序库设计员,你会尽可能将一切方法都定为private,而仅向客户端程序员公开你愿意让他们使用的方法。这样做是完全正确的,但是对于那些经常使用别的语言(特别是c语言)编写程序并在访问事物时不受任何限制的人而言,却是与他们的直觉相违背的。到了本章末,你将会信服Java的访问权限控制的价值。不过,构件程序库(componentlibrary)的概念以及对于谁有权取用该程序库构件的控制都还是不完善的。其中仍旧存在着如何将构件捆绑到一个内聚的程序库单元中的问题。对于这一点,Java用关键字package加以控制,而访问权限会因类是存在于一个相同的包,还是存在于一个单独的包而受到影响。为此,要开始学习本章,你首先要学习如何将程序库构件置于包中,然后你就会理解访问权限修饰词的全部含义。包(package):程序库单元当你使用关键字import来导入整个程序库时,如:importjava.util.*;这个包就变为可用的了。这将把作为标准Java一部分发布的整个utility程序库都引入到程序中来。例如,java.util中有一个叫作ArrayList的类,你现在既可以用全称java.util.ArrayList来指定(这样你就不必使用import语句了),也可以仅指定为ArrayList(缘于import)。如果你想引入一个单一的类,你可以在import语句中命名该类importjava.util.ArrayList;现在,你就可以不受任何限定而直接使用ArrayList了。但是,这样做java.util中的其他类就都变为是不可用的了。我们之所以要导入,就是要提供一个管理名字空间(namespaces)的机制。所有类成员的名称都是彼此隔离的。A类中的方法f()与B类中具有相同参数列表(argumentlist)的方法f()不会彼此冲突。但是如果类名称相互冲突又该怎么办呢?假设你编写了一个Stack类并安装到了一台机器上,而该机器上已经有了一个别人编写的Stack类,我们该如何解决?名字之间的潜在冲突使得在java中对名称空间进行完全控制,并能够不受Internet的限制创建唯一的名字就成为了非常重要的事情。到目前为止,书中大多数示例都存于单一文件之中,并专为本机使用(localuse)而设计,因而尚未受到包名(packagename)的干扰。(此时类名称被放于“缺省包”中)这当然也是一种选择,而且出于简易性的考虑,在本书其它任何部分都尽可能地使用了此方法。不过如果你正在准备编写能够与其他java程序在同一台机器上共存的java程序的话,你就需要考虑如何防止类名称之间的冲突。如果你编写一个java源代码文件,此文件通常被称为编译单元(compilationunit)(有时也被称为转译单元(translationunit))。每个编译单元都必须有一个后缀名.java,而在编译单元之中则可以有一个public类,该类的名称必须于文件的名称相同(包括大小写,但不包括文件的后缀名.java)。每个编译单元只能有一个public类,否则编译器就不会接受。如果在该编译单元之中还有额外的类的话,那么在包之外的世界是无法看见这些类的,这是因为它们不是public类,而且它们主要是被用于为主public类提供支持。当你编译一个.java文件时,在.java文件中每个类都会有一个输出文件,而该输出文件的名称与.java文件中每个类的名称又恰好相同,只是多了一个后缀名.class。因此,你在编译少量.java文件之后,会得到大量的.class文件。如果你已经用编译型语言编写过程序,那么对于编译器产生一个中间文件(通常是一个“obj”文件),然后再与通过链接器(linker,用以创建一个可执行文件)或程序库产生器(librarian,用以创建一个程序库)产生的其它同类文件捆绑在一起的情况,你可能早已习惯。但这并不是java的工作方式。一个java可运行程序是一组可以打包并压缩为一个java文档文件(JAR,用Java的jar文档生成器)的.class文件。Java解释器(interpreter)负责对这些文件的查找、装载和解释。程序库实际上是一组类文件。其中每个文件都有一个public类(并非强制的,但这很典型),因此每个文件都是一个构件(component)。如果你希望这些构件(每一个都有它们自己分离开的.java和.class文件)从属于同一个群组,就可以使用关键字package。当你在文件起始处写道:packagemypackage;就表示你在声明该编译单元是名为mypackage的程序库的一部分(如果使用了一个package语句,就它必须是文件中除注释以外的第一句程序代码)。或者,换种说法,你正在声明该编译单元中的public类名称是位于mypackage名称的遮蔽之下。任何想要使用该名称的人都必须指定全名或是在与mypackage的结合中使用关键字import(使java中并不强求必须要使用解释器。因为存在用来生成一个单一的可执行文件的本地代码的java编译器。用前面给出的选择)。请注意,Java包的命名规则全部使用小写字母,包括中间的字也是如此。例如,假设文件的名称是MyClass.java,这就意味着在该文件中有且只有一个public类,该类的名称必须是MyClass(注意大小写):packagemypackage;publicclassMyClass{//...现在,如果有人想用MyClass或者是mypackage中的任何其他public类,就必须使用关键字import来使得mypackage中的名称可以被使用。另一个选择是给出完整的名称:mypackage.MyClassm=newmypackage.MyClass();关键字import可使之更加简洁:importmypackage.*;//...MyClassm=newMyClass();身为一名程序库设计员,很有必要牢记package和import关键字允许你做的,是将单一的全局名字空间分割开,使得无论多少人使用Internet并用java编写类,都不会出现名称冲突问题。创建独一无二的包名你也许会发现,既然一个包从未真正将被打包的东西包装成一个单一的文件,并且一个包可以由许多文件构成,那么情况就有点复杂了。为了避免这种情况的发生,一种合乎逻辑的作法就是将特定包的所有.class文件都置于一个单一目录之下。也就是说,利用操作系统的层次化的文件结构来解决这一问题。这是java解决混乱问题的一种方式,你还会在我们介绍jar工具的时候看到另一种方式。将所有的文件收入一个子目录还可以解决了另外两个问题:怎样创建独一无二的名称以及怎样查找有可能隐藏于目录结构中某处的类。正如我们在第二章中所介绍的那样,这些任务是通过将.class文件所在的路径位置编码成package名称来实现的。按照惯例,package名称的第一部分是反顺序的类的创建者的Internet域名。如果你遵照惯例,Internet域名应是独一无二的,因此你的package名称也将是独一无二的,也就不会出现名称冲突的问题了。(也就是说,只有在你将自己的域名给了别人,而他又以你曾经使用过的路径名称来编写java程序代码时,才会出现冲突。)当然,如果你没有自己的域名,你就得构造一组不大可能与他人重复的组合,例如你的姓名,来创立一些独一无二的package名称。如果你打算发布你的java程序代码,稍微花点力气去取得一个域名,还是很有必要的。此技巧的第二部分是把package名称分解为你机器上的一个目录。所以当java程序运行并且需要加载.class文件的时候(当需要为特定的类生成对象或首次访问类的static成员时,程序会自动进行加载),它就可以确定.class文件在目录上所处的位置。Java解释器(interpreter)的运行过程如下:首先,找出环境变量CLASSPATH2(可以通过操作系统,有时也可通过在你的机器上安装用Java或Java-based工具的安装程序来设置。)CLASSPATH包含一个或多个目录,用来作为查找.class文件的根目录。从根目录开始,解释器获取包的名称并将每个句点替换成反斜杠以从CLASSPATH根中产生一个路径名称(于是,packagefoo.bar.baz就变成为foo\bar\baz或foo/bar/baz或其他什么可能的东西,这一切取决于你的操作系统)。得到的路径会与CLASSPATH中的各个不同的项相连接,解释器就在这些目录中查找与你所要创建的类相关的名称的.class文件。(解释器还会去查找某些相对于它所在位置的标准目录)。为了理解这一点,以我的域名bruceeckel.com为例。把它的顺序倒过来,bruceeckel.com就成了我所创建的类的一个别具一格的名称。(com,edu,org等扩展名先前在java包中都是大写的,但在java2中一切都已改观,包的整个名称全都变成了小写。)若我决定再创建一个名为simple的程序库,我可以将该名称进一步细分,于是我可以得到一个包的名称:packagecom.bruceeckel.simple;现在,这个包名称就可以用作下面两个文件的名字空间保护伞了://:com:bruceeckel:simple:Vector.java//Creatingapackage.packagecom.bruceeckel.simple;publicclassVector{publicVector(){System.out.println(com.bruceeckel.simple.Vector);}}///:~你在创建自己的包时,将会发现package语句必须是文件中的第一行非注释程序代码。第二个文件看起来也极其相似://:com:bruceeckel:simple:List.java//Creatingapackage.packagecom.bruceeckel.simple;publicclassList{publicList(){System.out.println(com.bruceeckel.simple.List);}}///:~当提及环境变量时,将用到大写字母(CLASSPATH)。这两个文件均被置于我的系统的子目录下:C:\DOC\JavaT\com\bruceeckel\simple如果你沿此路径向回看,你可以看到包的名称com.bruceeckel.simple,但此路径的第一部分怎样办呢?它将由环境变量CLASSPATH关照,在