# 5.1 包:库单元
我们用`import`关键字导入一个完整的库时,就会获得“包”(Package)。例如:
```
import java.util.*;
```
它的作用是导入完整的实用工具(Utility)库,该库属于标准Java开发工具包的一部分。由于`Vector`位于`java.util`里,所以现在要么指定完整名称`java.util.Vector`(可省略`import`语句),要么简单地指定一个`Vector`(因为`import`是默认的)。
若想导入单独一个类,可在`import`语句里指定那个类的名字:
```
import java.util.Vector;
```
现在,我们可以自由地使用`Vector`。然而,`java.util`中的其他任何类仍是不可使用的。
之所以要进行这样的导入,是为了提供一种特殊的机制,以便管理“命名空间”(Name Space)。我们所有类成员的名字相互间都会隔离起来。位于类`A`内的一个方法`f()`不会与位于类`B`内的、拥有相同“签名”(参数列表)的`f()`发生冲突。但类名会不会冲突呢?假设创建一个`stack`类,将它安装到已有一个`stack`类(由其他人编写)的机器上,这时会出现什么情况呢?对于因特网中的Java应用,这种情况会在用户毫不知晓的时候发生,因为类会在运行一个Java程序的时候自动下载。
正是由于存在名字潜在的冲突,所以特别有必要对Java中的命名空间进行完整的控制,而且需要创建一个完全独一无二的名字,无论因特网存在什么样的限制。
迄今为止,本书的大多数例子都仅存在于单个文件中,而且设计成局部(本地)使用,没有同包名发生冲突(在这种情况下,类名置于“默认包”内)。这是一种有效的做法,而且考虑到问题的简化,本书剩下的部分也将尽可能地采用它。然而,若计划创建一个“对因特网友好”或者说“适合在因特网使用”的程序,必须考虑如何防止类名的重复。
为Java创建一个源码文件的时候,它通常叫作一个“编辑单元”(有时也叫作“翻译单元”)。每个编译单元都必须有一个以`.java`结尾的名字。而且在编译单元的内部,可以有一个公共(`public`)类,它必须拥有与文件相同的名字(包括大小写形式,但排除`.java`文件扩展名)。如果不这样做,编译器就会报告出错。每个编译单元内都只能有一个`public`类(同样地,否则编译器会报告出错)。那个编译单元剩下的类(如果有的话)可在那个包外面的世界面前隐藏起来,因为它们并非“公共”的(非`public`),而且它们由用于主`public`类的“支撑”类组成。
编译一个`.java`文件时,我们会获得一个名字完全相同的输出文件;但对于`.java`文件中的每个类,它们都有一个`.class`扩展名。因此,我们最终从少量的`.java`文件里有可能获得数量众多的`.class`文件。如以前用一种汇编语言写过程序,那么可能已习惯编译器先分割出一种过渡形式(通常是一个`.obj`文件),再用一个链接器将其与其他东西封装到一起(生成一个可执行文件),或者与一个库封装到一起(生成一个库)。但那并不是Java的工作方式。一个有效的程序就是一系列`.class`文件,它们可以封装和压缩到一个JAR文件里(使用Java 1.1提供的`jar`工具)。Java解释器负责对这些文件的寻找、装载和解释(注释①)。
①:Java并没有强制一定要使用解释器。一些固有代码的Java编译器可生成单独的可执行文件。
“库”也由一系列类文件构成。每个文件都有一个`public`类(并没强迫使用一个`public`类,但这种情况最很典型的),所以每个文件都有一个组件。如果想将所有这些组件(它们在各自独立的`.java`和`.class`文件里)都归纳到一起,那么`package`关键字就可以发挥作用)。
若在一个文件的开头使用下述代码:
```
package mypackage;
```
那么`package`语句必须作为文件的第一个非注释语句出现。该语句的作用是指出这个编译单元属于名为`mypackage`的一个库的一部分。或者换句话说,它表明这个编译单元内的`public`类名位于`mypackage`这个名字的下面。如果其他人想使用这个名字,要么指出完整的名字,要么与`mypackage`联合使用`import`关键字(使用前面给出的选项)。注意根据Java包(封装)的约定,名字内的所有字母都应小写,甚至那些中间单词亦要如此。
例如,假定文件名是`MyClass.java`。它意味着在那个文件有一个、而且只能有一个`public`类。而且那个类的名字必须是`MyClass`(包括大小写形式):
```
package mypackage;
public class MyClass {
// . . .
```
现在,如果有人想使用`MyClass`,或者想使用`mypackage`内的其他任何`public`类,他们必须用`import`关键字激活`mypackage`内的名字,使它们能够使用。另一个办法则是指定完整的名称:
```
mypackage.MyClass m = new mypackage.MyClass();
```
`import`关键字则可将其变得简洁得多:
```
import mypackage.*;
// . . .
MyClass m = new MyClass();
```
作为一名库设计者,一定要记住`package`和`import`关键字允许我们做的事情就是分割单个全局命名空间,保证我们不会遇到名字的冲突——无论有多少人使用因特网,也无论多少人用Java编写自己的类。
## 5.1.1 创建独一无二的包名
大家或许已注意到这样一个事实:由于一个包永远不会真的“封装”到单独一个文件里面,它可由多个`.class`文件构成,所以局面可能稍微有些混乱。为避免这个问题,最合理的一种做法就是将某个特定包使用的所有`.class`文件都置入单个目录里。也就是说,我们要利用操作系统的分级文件结构避免出现混乱局面。这正是Java所采取的方法。
它同时也解决了另两个问题:创建独一无二的包名以及找出那些可能深藏于目录结构某处的类。正如我们在第2章讲述的那样,为达到这个目的,需要将`.class`文件的位置路径编码到`package`的名字里。但根据约定,编译器强迫`package`名的第一部分是类创建者的因特网域名。由于因特网域名肯定是独一无二的(由InterNIC保证——注释②,它控制着域名的分配),所以假如按这一约定行事,`package`的名称就肯定不会重复,所以永远不会遇到名称冲突的问题。换句话说,除非将自己的域名转让给其他人,而且对方也按照相同的路径名编写Java代码,否则名字的冲突是永远不会出现的。当然,如果你没有自己的域名,那么必须创造一个非常生僻的包名(例如自己的英文姓名),以便尽最大可能创建一个独一无二的包名。如决定发行自己的Java代码,那么强烈推荐去申请自己的域名,它所需的费用是非常低廉的。
②:ftp://ftp.internic.net
这个技巧的另一部分是将`package`名解析成自己机器上的一个目录。这样一来,Java程序运行并需要装载`.class`文件的时候(这是动态进行的,在程序需要创建属于那个类的一个对象,或者首次访问那个类的一个`static`成员时),它就可以找到`.class`文件驻留的那个目录。
Java解释器的工作程序如下:首先,它找到环境变量`CLASSPATH`(将Java或者具有Java解释能力的工具——如浏览器——安装到机器中时,通过操作系统进行设定)。`CLASSPATH`包含了一个或多个目录,它们作为一种特殊的“根”使用,从这里展开对`.class`文件的搜索。从那个根开始,解释器会寻找包名,并将每个点号(句点)替换成一个斜杠,从而生成从`CLASSPATH`根开始的一个路径名(所以`package foo.bar.baz`会变成`foo\bar\baz`或者`foo/bar/baz`;具体是正斜杠还是反斜杠由操作系统决定)。随后将它们连接到一起,成为`CLASSPATH`内的各个条目(入口)。以后搜索`.class`文件时,就可从这些地方开始查找与准备创建的类名对应的名字。此外,它也会搜索一些标准目录——这些目录与Java解释器驻留的地方有关。
为进一步理解这个问题,下面以我自己的域名为例,它是`bruceeckel.com`。将其反转过来后,`com.bruceeckel`就为我的类创建了独一无二的全局名称(`com`,`edu`,`org`,`net`等扩展名以前在Java包中都是大写的,但自Java 1.2以来,这种情况已发生了变化。现在整个包名都是小写的)。由于决定创建一个名为`util`的库,我可以进一步地分割它,所以最后得到的包名如下:
```
package com.bruceeckel.util;
```
现在,可将这个包名作为下述两个文件的“命名空间”使用:
```
//: Vector.java
// Creating a package
package com.bruceeckel.util;
public class Vector {
public Vector() {
System.out.println(
"com.bruceeckel.util.Vector");
}
} ///:~
```
创建自己的包时,要求`package`语句必须是文件中的第一个“非注释”代码。第二个文件表面看起来是类似的:
```
//: List.java
// Creating a package
package com.bruceeckel.util;
public class List {
public List() {
System.out.println(
"com.bruceeckel.util.List");
}
} ///:~
```
这两个文件都置于我自己系统的一个子目录中:
```
C:\DOC\JavaT\com\bruceeckel\util
```
若通过它往回走,就会发现包名`com.bruceeckel.uti`l,但路径的第一部分又是什么呢?这是由`CLASSPATH`环境变量决定的。在我的机器上,它是:
```
CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT
```
可以看出,`CLASSPATH`里能包含大量备用的搜索路径。然而,使用JAR文件时要注意一个问题:必须将JAR文件的名字置于类路径里,而不仅仅是它所在的路径。所以对一个名为`grape.jar`的JAR文件来说,我们的类路径需要包括:
```
CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar
```
正确设置好类路径后,可将下面这个文件置于任何目录里(若在执行该程序时遇到麻烦,请参见第3章的3.1.2小节“赋值”):
```
//: LibTest.java
// Uses the library
package c05;
import com.bruceeckel.util.*;
public class LibTest {
public static void main(String[] args) {
Vector v = new Vector();
List l = new List();
}
} ///:~
```
编译器遇到`import`语句后,它会搜索由`CLASSPATH`指定的目录,查找子目录`com\bruceeckel\util`,然后查找名称适当的已编译文件(对于`Vector`是`Vector.class`,对于`List`则是`List.class`)。注意`Vector`和`List`内无论类还是需要的方法都必须设为`public`。
(1) 自动编译
为导入的类首次创建一个对象时(或者访问一个类的`static`成员时),编译器会在适当的目录里寻找同名的`.class`文件(所以如果创建类X的一个对象,就应该是`X.class`)。若只发现`X.class`,它就是必须使用的那一个类。然而,如果它在相同的目录中还发现了一个`X.java`,编译器就会比较两个文件的日期标记。如果`X.java`比`X.class`新,就会自动编译`X.java`,生成一个最新的`X.class`。
对于一个特定的类,或在与它同名的`.java`文件中没有找到它,就会对那个类采取上述的处理。
(2) 冲突
若通过 `*` 导入了两个库,而且它们包括相同的名字,这时会出现什么情况呢?例如,假定一个程序使用了下述导入语句:
```
import com.bruceeckel.util.*;
import java.util.*;
```
由于 `java.util.*` 也包含了一个`Vector`类,所以这会造成潜在的冲突。然而,只要冲突并不真的发生,那么就不会产生任何问题——这当然是最理想的情况,因为否则的话,就需要进行大量编程工作,防范那些可能可能永远也不会发生的冲突。
如现在试着生成一个`Vector`,就肯定会发生冲突。如下所示:
```
Vector v = new Vector();
```
它引用的到底是哪个`Vector`类呢?编译器对这个问题没有答案,读者也不可能知道。所以编译器会报告一个错误,强迫我们进行明确的说明。例如,假设我想使用标准的Java `Vector`,那么必须象下面这样编程:
```
java.util.Vector v = new java.util.Vector();
```
由于它(与`CLASSPATH`一起)完整指定了那个Vector的位置,所以不再需要 `import java.util.*` 语句,除非还想使用来自`java.util`的其他东西。
## 5.1.2 自定义工具库
掌握前述的知识后,接下来就可以开始创建自己的工具库,以便减少或者完全消除重复的代码。例如,可为`System.out.println()`创建一个别名,减少重复键入的代码量。它可以是名为`tools`的一个包(`package`)的一部分:
```
//: P.java
// The P.rint & P.rintln shorthand
package com.bruceeckel.tools;
public class P {
public static void rint(Object obj) {
System.out.print(obj);
}
public static void rint(String s) {
System.out.print(s);
}
public static void rint(char[] s) {
System.out.print(s);
}
public static void rint(char c) {
System.out.print(c);
}
public static void rint(int i) {
System.out.print(i);
}
public static void rint(long l) {
System.out.print(l);
}
public static void rint(float f) {
System.out.print(f);
}
public static void rint(double d) {
System.out.print(d);
}
public static void rint(boolean b) {
System.out.print(b);
}
public static void rintln() {
System.out.println();
}
public static void rintln(Object obj) {
System.out.println(obj);
}
public static void rintln(String s) {
System.out.println(s);
}
public static void rintln(char[] s) {
System.out.println(s);
}
public static void rintln(char c) {
System.out.println(c);
}
public static void rintln(int i) {
System.out.println(i);
}
public static void rintln(long l) {
System.out.println(l);
}
public static void rintln(float f) {
System.out.println(f);
}
public static void rintln(double d) {
System.out.println(d);
}
public static void rintln(boolean b) {
System.out.println(b);
}
} ///:~
```
所有不同的数据类型现在都可以在一个新行输出(`P.rintln()`),或者不在一个新行输出(`P.rint()`)。
大家可能会猜想这个文件所在的目录必须从某个`CLASSPATH`位置开始,然后继续`com/bruceeckel/tools`。编译完毕后,利用一个`import`语句,即可在自己系统的任何地方使用`P.class`文件。如下所示:
```
ToolTest.java
```
所以从现在开始,无论什么时候只要做出了一个有用的新工具,就可将其加入`tools`目录(或者自己的个人`util`或`tools`目录)。
(1) `CLASSPATH`的陷阱
`P.java`文件存在一个非常有趣的陷阱。特别是对于早期的Java实现方案来说,类路径的正确设定通常都是很困难的一项工作。编写这本书的时候,我引入了`P.java`文件,它最初看起来似乎工作很正常。但在某些情况下,却开始出现中断。在很长的时间里,我都确信这是Java或其他什么在实现时一个错误。但最后,我终于发现在一个地方引入了一个程序(即第17章要说明的`CodePackager.java`),它使用了一个不同的类`P`。由于它作为一个工具使用,所以有时候会进入类路径里;另一些时候则不会这样。但只要它进入类路径,那么假若执行的程序需要寻找`com.bruceeckel.tools`中的类,Java首先发现的就是`CodePackager.java`中的`P`。此时,编译器会报告一个特定的方法没有找到。这当然是非常令人头疼的,因为我们在前面的类`P`里明明看到了这个方法,而且根本没有更多的诊断报告可为我们提供一条线索,让我们知道找到的是一个完全不同的类(那甚至不是`public`的)。
乍一看来,这似乎是编译器的一个错误,但假若考察`import`语句,就会发现它只是说:“在这里可能发现了`P`”。然而,我们假定的是编译器搜索自己类路径的任何地方,所以一旦它发现一个`P`,就会使用它;若在搜索过程中发现了“错误的”一个,它就会停止搜索。这与我们在前面表述的稍微有些区别,因为存在一些讨厌的类,它们都位于包内。而这里有一个不在包内的P,但仍可在常规的类路径搜索过程中找到。
如果您遇到象这样的情况,请务必保证对于类路径的每个地方,每个名字都仅存在一个类。
## 5.1.3 利用导入改变行为
Java已取消的一种特性是C的“条件编译”,它允许我们改变参数,获得不同的行为,同时不改变其他任何代码。Java之所以抛弃了这一特性,可能是由于该特性经常在C里用于解决跨平台问题:代码的不同部分根据具体的平台进行编译,否则不能在特定的平台上运行。由于Java的设计思想是成为一种自动跨平台的语言,所以这种特性是没有必要的。
然而,条件编译还有另一些非常有价值的用途。一种很常见的用途就是调试代码。调试特性可在开发过程中使用,但在发行的产品中却无此功能。Alen Holub(`www.holub.com`)提出了利用包(`package`)来模仿条件编译的概念。根据这一概念,它创建了C“断定机制”一个非常有用的Java版本。之所以叫作“断定机制”,是由于我们可以说“它应该为真”或者“它应该为假”。如果语句不同意你的断定,就可以发现相关的情况。这种工具在调试过程中是特别有用的。
可用下面这个类进行程序调试:
```
//: Assert.java
// Assertion tool for debugging
package com.bruceeckel.tools.debug;
public class Assert {
private static void perr(String msg) {
System.err.println(msg);
}
public final static void is_true(boolean exp) {
if(!exp) perr("Assertion failed");
}
public final static void is_false(boolean exp){
if(exp) perr("Assertion failed");
}
public final static void
is_true(boolean exp, String msg) {
if(!exp) perr("Assertion failed: " + msg);
}
public final static void
is_false(boolean exp, String msg) {
if(exp) perr("Assertion failed: " + msg);
}
} ///:~
```
这个类只是简单地封装了布尔测试。如果失败,就显示出出错消息。在第9章,大家还会学习一个更高级的错误控制工具,名为“异常控制”。但在目前这种情况下,`perr()`方法已经可以很好地工作。
如果想使用这个类,可在自己的程序中加入下面这一行:
```
import com.bruceeckel.tools.debug.*;
```
如欲清除断定机制,以便自己能发行最终的代码,我们创建了第二个`Assert`类,但却是在一个不同的包里:
```
//: Assert.java
// Turning off the assertion output
// so you can ship the program.
package com.bruceeckel.tools;
public class Assert {
public final static void is_true(boolean exp){}
public final static void is_false(boolean exp){}
public final static void
is_true(boolean exp, String msg) {}
public final static void
is_false(boolean exp, String msg) {}
} ///:~
```
现在,假如将前一个`import`语句变成下面这个样子:
```
import com.bruceeckel.tools.*;
```
程序便不再显示出断言。下面是个例子:
```
//: TestAssert.java
// Demonstrating the assertion tool
package c05;
// Comment the following, and uncomment the
// subsequent line to change assertion behavior:
import com.bruceeckel.tools.debug.*;
// import com.bruceeckel.tools.*;
public class TestAssert {
public static void main(String[] args) {
Assert.is_true((2 + 2) == 5);
Assert.is_false((1 + 1) == 2);
Assert.is_true((2 + 2) == 5, "2 + 2 == 5");
Assert.is_false((1 + 1) == 2, "1 +1 != 2");
}
} ///:~
```
通过改变导入的`package`,我们可将自己的代码从调试版本变成最终的发行版本。这种技术可应用于任何种类的条件代码。
## 5.1.4 包的停用
大家应注意这样一个问题:每次创建一个包后,都在为包取名时间接地指定了一个目录结构。这个包必须存在(驻留)于由它的名字规定的目录内。而且这个目录必须能从`CLASSPATH`开始搜索并发现。最开始的时候,`package`关键字的运用可能会令人迷惑,因为除非坚持遵守根据目录路径指定包名的规则,否则就会在运行期获得大量莫名其妙的消息,指出找不到一个特定的类——即使那个类明明就在相同的目录中。若得到象这样的一条消息,请试着将`package`语句作为注释标记出去。如果这样做行得通,就可知道问题到底出在哪儿。
- Java 编程思想
- 写在前面的话
- 引言
- 第1章 对象入门
- 1.1 抽象的进步
- 1.2 对象的接口
- 1.3 实现方案的隐藏
- 1.4 方案的重复使用
- 1.5 继承:重新使用接口
- 1.6 多态对象的互换使用
- 1.7 对象的创建和存在时间
- 1.8 异常控制:解决错误
- 1.9 多线程
- 1.10 永久性
- 1.11 Java和因特网
- 1.12 分析和设计
- 1.13 Java还是C++
- 第2章 一切都是对象
- 2.1 用引用操纵对象
- 2.2 所有对象都必须创建
- 2.3 绝对不要清除对象
- 2.4 新建数据类型:类
- 2.5 方法、参数和返回值
- 2.6 构建Java程序
- 2.7 我们的第一个Java程序
- 2.8 注释和嵌入文档
- 2.9 编码样式
- 2.10 总结
- 2.11 练习
- 第3章 控制程序流程
- 3.1 使用Java运算符
- 3.2 执行控制
- 3.3 总结
- 3.4 练习
- 第4章 初始化和清除
- 4.1 用构造器自动初始化
- 4.2 方法重载
- 4.3 清除:收尾和垃圾收集
- 4.4 成员初始化
- 4.5 数组初始化
- 4.6 总结
- 4.7 练习
- 第5章 隐藏实现过程
- 5.1 包:库单元
- 5.2 Java访问指示符
- 5.3 接口与实现
- 5.4 类访问
- 5.5 总结
- 5.6 练习
- 第6章 类复用
- 6.1 组合的语法
- 6.2 继承的语法
- 6.3 组合与继承的结合
- 6.4 到底选择组合还是继承
- 6.5 protected
- 6.6 累积开发
- 6.7 向上转换
- 6.8 final关键字
- 6.9 初始化和类装载
- 6.10 总结
- 6.11 练习
- 第7章 多态性
- 7.1 向上转换
- 7.2 深入理解
- 7.3 覆盖与重载
- 7.4 抽象类和方法
- 7.5 接口
- 7.6 内部类
- 7.7 构造器和多态性
- 7.8 通过继承进行设计
- 7.9 总结
- 7.10 练习
- 第8章 对象的容纳
- 8.1 数组
- 8.2 集合
- 8.3 枚举器(迭代器)
- 8.4 集合的类型
- 8.5 排序
- 8.6 通用集合库
- 8.7 新集合
- 8.8 总结
- 8.9 练习
- 第9章 异常差错控制
- 9.1 基本异常
- 9.2 异常的捕获
- 9.3 标准Java异常
- 9.4 创建自己的异常
- 9.5 异常的限制
- 9.6 用finally清除
- 9.7 构造器
- 9.8 异常匹配
- 9.9 总结
- 9.10 练习
- 第10章 Java IO系统
- 10.1 输入和输出
- 10.2 增添属性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File类
- 10.5 IO流的典型应用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 压缩
- 10.9 对象序列化
- 10.10 总结
- 10.11 练习
- 第11章 运行期类型识别
- 11.1 对RTTI的需要
- 11.2 RTTI语法
- 11.3 反射:运行期类信息
- 11.4 总结
- 11.5 练习
- 第12章 传递和返回对象
- 12.1 传递引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只读类
- 12.5 总结
- 12.6 练习
- 第13章 创建窗口和程序片
- 13.1 为何要用AWT?
- 13.2 基本程序片
- 13.3 制作按钮
- 13.4 捕获事件
- 13.5 文本字段
- 13.6 文本区域
- 13.7 标签
- 13.8 复选框
- 13.9 单选钮
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 视窗化应用
- 13.16 新型AWT
- 13.17 Java 1.1用户接口API
- 13.18 可视编程和Beans
- 13.19 Swing入门
- 13.20 总结
- 13.21 练习
- 第14章 多线程
- 14.1 反应灵敏的用户界面
- 14.2 共享有限的资源
- 14.3 堵塞
- 14.4 优先级
- 14.5 回顾runnable
- 14.6 总结
- 14.7 练习
- 第15章 网络编程
- 15.1 机器的标识
- 15.2 套接字
- 15.3 服务多个客户
- 15.4 数据报
- 15.5 一个Web应用
- 15.6 Java与CGI的沟通
- 15.7 用JDBC连接数据库
- 15.8 远程方法
- 15.9 总结
- 15.10 练习
- 第16章 设计模式
- 16.1 模式的概念
- 16.2 观察器模式
- 16.3 模拟垃圾回收站
- 16.4 改进设计
- 16.5 抽象的应用
- 16.6 多重分发
- 16.7 访问器模式
- 16.8 RTTI真的有害吗
- 16.9 总结
- 16.10 练习
- 第17章 项目
- 17.1 文字处理
- 17.2 方法查找工具
- 17.3 复杂性理论
- 17.4 总结
- 17.5 练习
- 附录A 使用非JAVA代码
- 附录B 对比C++和Java
- 附录C Java编程规则
- 附录D 性能
- 附录E 关于垃圾收集的一些话
- 附录F 推荐读物