访问控制(或隐藏具体实现)与“最初的实现并不恰当”有关。
所有优秀的作者,又是哪些编写软件的程序猿,都清楚其著作的某些部分直至重新创作的时候才会变得完美,有时甚至要反复重写多次。如果你把一个代码段放到某个位置,等过一会儿回头再看时,有可能发现有更好的方式去实现相同的功能。这正是重构的原动力之一,重构既重写代码,以使它更可读,更易理解,并因此更具可维护性。
但是,在这种修改和完善代码的愿望之下,也存在着巨大的压力。通常总会有一些消费者(客户端程序猿)需要你的代码在某些方面保持不变。因此你想改变代码,而他们却想让代码保持不变。由此而产生在面向对象设计中需要考虑的一个基本问题:“如何把变动的事物与保持不变的事物区分开来”。
这对类库(library)而言尤为重要。该类库的的消费者必须依赖他所使用的那部分类库,并且能够知道如果类库出现了新版本,他们并不需要改写代码。从另一方面来说,类库的开发者必须有权限进行修改和改进,并确保客户端代码不会因为这些改动而受到影响。
这个目标可以通过约定来达成。例如,类库开发者必须同意在改动类库是不得删除任何现有的方法,因为那相会破坏客户端的代码。但是,与之相反的情况会更加棘手。在有域(既数据成员)存在的情况下,类库的开发者怎样才能知道究竟哪些域已经被客户端所调用了呢?这对于方法仅为类的实现的一部分,因此并不想让客户端程序猿直接使用的情况来说同样如此。如果程序开发者想要移除旧的实现而要添加新的实现时,结果将会怎样呢?改动任何一个成员都有可能破快客户端程序猿的代码。于是类库开发者会手脚被缚,无法对任何事物进行改动。
对于解决这一问题,Java提供了访问权限修饰词,以供类库开发人员向客户端程序猿指明哪些是可用的,哪些是不可用的。访问权限控制的等级,从最大权限到最小权限依次是:publi, protected, 包访问权限(没有关键词),和private。根据前述内容,读者可能会认为,作为一个类库设计者,你会尽可能将一切方法都定义为private,而仅向客户端程序猿公开你愿意让他们使用的方法。这样做是完全正确的,尽管对于哪些经常使用别的语言(特别是C语言)编写程序并在访问事物时不受任何限制的人来说,这与他们的直觉相违背。到了本章末,读者将会信服Java对于访问控制的价值。
不过, 构件类库的概念以及对于谁有权取用该类库的控制问题都还是不完善的。其中仍旧存在着如何将构件捆绑到一个内聚的类库单元的问题。对于这一点,Java用关键词package 加以控制,而访问权限修饰词会因类是存在于相同的包,还是存在一个单独的包而受到影响。为此,要开始学习本章,首先要学习如果将类库构件置于包中,然后就会理解权限修饰词的全部含义。
6.1 包:库单元
包内包含一组类,他们在单一的名字空间之下被阻止在了一起。
例如,在Java的标准发布中有一个工具库,他被住址在java.util名字空间之下。java.util中有一个叫做ArrayList的类,使用ArrayList的一中方式是使用其全名java.util.ArrayList来指定。
public class FullQualification {
public static void main(String[] args) {
java.util.ArrayList list = new java.util.ArrayList()
}
}
这样就使程序变的冗长了,因此你可能想转而使用import关键字。如果你想导入单个类,可以在import 语句中命中该类:
import java.util.ArrayList;
public class FullQualification {
public static void main(String[] args) {
ArrayList list = new ArrayList()
}
}
现在就可以限定地使用ArrayList了。但是,这样做java.util中的其他类仍旧是都不可用的。想要导入其中的所有类,只需要使用“”,就像本书剩余部分的实例中所看到的那样:
import java.util.
我们之所以想要导入,就是要提供一个管理名字空间的机制。所有类成员的名称都是彼此隔离的。A类中的方法f()与B类中的具有相同特征标识(参数列表)方法f()不会彼此冲突。但是如果类名称冲突该怎么办呢?比如你编写了一个Stack类并安装到了一台机器上,而该机器上已经有了一个别人编写的Stack类,我们该如何解决呢?由于名字之间的冲突,在Java中对名称空间进行完全控制并为每个类创建唯一标识符组合就成为了非常重要的事情。
到目前为止,书中大多数的实例都存在于单一文件之中,并专为本地使用(local use)而设计,而并未受到包名的干扰。这些事例实际已经位于包中了;即未命名包,或称为默认包,这当然也是一种选择,而且为了简单起见,在本书其他部分都尽可能使用此方法。不过如果你准备编写对一台机器上共存的其他java程序友好的类库或程序的话,就需要考虑如何防止名称之间的冲突问题。
当编写一个Java源码文件时,此文件通常称为编译单元(有时候也被称为转移单元)。每个编译单元必须有一个后缀名.java,而在编译单元内则可以有一个public类,该类的名称必须与文件的名称相同。每个编译单元只能有一个public类,否则编译器不会接受。如果在编译单元之中还有额外的类的话,那么在包之外的世界是无法看见这些类的。这正是因为他们不是public类,而且他们主要用来为public配提供支持。
6.11 代码组织
当编译一个.java文件时,在.java文件的每个类都会有一个输出文件,而该输出文件的文件名与.java文件里的每个类的名称相同,只是多了一个后缀名.class。因此,在编译少量.java文件之后会得到大量的.class文件。如果用编译型语言编写过程序,那么对于编译器产生一个中间文件(通常是一个obj文件),然后再与链接器(用以创建一个可执行文件)或类库产生器(librarian,用以常见一个类库)产生的其他同类文件捆绑在一起的情况,可能早已司空见惯。但这并不是java的工作方式。java的可运行程序是一组可以打包并压缩为一个jar文档文件的.class文件。java解释器负责这些文件的查找、装载和解释。类库实际上是一组类文件。其中每个文件都有一个public类,以及任意数量的非public类。因此每一个文件都有一个构件。如果希望这些构件从属于一个群组就可以使用关键词package。
如果使用package语句,他必须是文件中除注释以外的第一句程序代码。在文件起始处写: package access;
就表明你在声明该编译单元是名为access的类库的一部分。或者换种说法,你正在声明该编译单元中public 类名称是位于access名称的保护伞下。任何想要使用该名称的人都必须使用前面给出的选择:指定全名或者与access结合使用关键字import。(请注意。java的包名规则全部使用小写字母,包括中间的字也是如此。)
例如,假设文件的名称是MyClass.java,这就意味着在该文件中有且只有一个public类,该类的名称必须是MyClass:
package access.mypackage;
public class MyClass {
//…………….
}
现在如果有人想用MyClass或者是access中任何其他public类,就必须使用关键字import 来使access中的名称可用。另一个选择时给出完整名称。
//: access/QualifiedMyClass.java
public class QualifiedMyClass {
public static void main(String[] args) {
access.mypackage.MyClass m =
new access.mypackage.MyClass();
}
} ///:~
关键字import可使之更加简洁:
//: access/ImportedMyClass.java
import access.mypackage.*;
public class ImportedMyClass {
public static void main(String[] args) {
MyClass m = new MyClass();
}
} ///:~
身为一个类库设计员,很有必要牢记: package和import关键字允许你做的,是将单一的全局名字空间分隔开,使得无论多少人使用Internet以及Java开始编写类,都不会出现名字冲突问题。
6.1.2 创建独一无二的包名
读者也许会发现,既然一个包从未真正将打包的东西包装成一个单一的文件,并且一个包可以由许多.class文件构成,那么情况就有点复杂了。为了避免这种情况的发生,一中合乎逻辑的做法就是将特定包的所有.calss文件都置于一个目录下。也就是说,利用操作系统的层次化文件结构来解决这一问题。这是Java解决混乱问题的一种方式,读者还会在我们介绍jar工具的时候看到另一种方式。
将所有的文件收入一个子目录还可以解决另外两个问题:怎样创建独一无二的名称以及怎样查找有可能隐藏于目录结构中某处的类。这些任务是通过将.class文件所在的路径位置编辑成package的名称来实现的。按照惯例,package名称的第一部分是类的创建者的反顺序的internet域名。如果你遵照惯例,Internet域名应该是独一无二的,因此你的package名称也将是独一无二的,也就不会出现名称冲突的问题了。当然如果你没有自己的域名,你就得构造一组不大可能与他人重复的组合来创建独一无二的package名称。如果你打算发布你的java程序代码,稍微花点力气取得一个域名还是很有必要的。
此技巧的第二部分是把package名称分解成你机器上的一个目录。所以当java程序运行并且需要加载.class文件的时候,他就可以确定.class文件在目录所处的位置。
Java解释器的运行过程如下:首先找出环境变量CLASSPATH。CLASSPATH包含一个或多个目录,用作查找.class文件的根目录。从根目录开始,解释器获取包名称并将每个句点替换成反斜杠,以从CLASSPATH根中产生一个路径名称(于是,package foo.bar.baz 就会变成foo\bar\baz或者foo/bar/baz或其他,这一切取决于操作系统)。得到的路径会与CLASSPATH中的一个或各个不同的项相连接,解释器就在这些目录中查找你要创建的类名称相关的.class文件。(解释器还会去查找某些设计Java解释器所在目录的标准目录)。
为了理解这一点,以我的域名MindView为例,把他的顺序倒过来,并且全部转为小写,net.mindview就成了我所创建的类的独一无二的全局名称。若我决定在创建一个名为simple的类库,我就可以以该名称进一步细分,于是我可以得到一个包的名称如下:
package net.mindview.simple
现在,这个包名称就可以用作下面两个文件的保护伞了:
//: net/mindview/simple/Vector.java
// Creating a package.
package net.mindview.simple;
public class Vector {
public Vector() {
System.out.println("net.mindview.simple.Vector");
}
} ///:~
如前所述,package语句必须是文件的第一行非注释程序代码。第二个文件看起来也是相似:
//: net/mindview/simple/List.java
// Creating a package.
package net.mindview.simple;
public class List {
public List() {
System.out.println("net.mindview.simple.List");
}
} ///:~
这两个文件均被置于我的文件系统的子目录下:
C:\Doc\JavaT\net\mindview\simple
如果沿此路径往回看可以看到包的名称net.mindview.simple, 但此路径的第一部分怎么办呢?它将由环境变量CLASSPATH关照,在我的机器上是:
CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT
可以看到CLASSPATH可以包含多个可供选择的查询路径。
但是在使用JAR文件时会出现一点变化。必须在类路径中将JAR文件的实际名称写清楚,而不仅仅是指明它所在的位置的目录。因此,对一个名为grape.jar的JAR文件。类路径应该包括:
CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar
一旦类路径得以正确建立,下面的文件就可以放于任何目录之下:
//: access/LibTest.java
// Uses the library.
import net.mindview.simple.*;
public class LibTest {
public static void main(String[] args) {
Vector v = new Vector();
List l = new List();
}
} /* Output:
net.mindview.simple.Vector
net.mindview.simple.List
*///:~
当编译器遇到simple库的import 语句时,就开始在CLASSPATH所指定的目录中查找,查找子目录net\mindview\simple,然互从已编译的文件中找出名称相符者。