4.7 包
Java允许您在称为包的集合中分组类。包便于组织您的工作,并将您的工作与其他人提供的代码库分离。在下面的部分中,您将学习如何使用和创建包。
4.7.1 包名
使用包的主要原因是为了保证类名的唯一性。假设两个程序员想出了提供员工类的好主意。只要他们两个将类放在不同的包中,就不会发生冲突。事实上,为了绝对保证一个唯一的包名,请使用反向写入的Internet域名(已知是唯一的)。然后对不同的项目使用子包。例如,考虑horstmann.com域。当以相反的顺序写入时,它会变成包名com.horstmann。然后可以附加项目名称,例如com.horstmann.corejava。如果您随后将Employee类放入该包中,“完全限定”的名称将变为com.horstmann.corejava.Employee。
注意
从编译器的角度来看,嵌套包之间绝对没有关系。例如,java.util和java.util.jar包之间没有任何关系。每个类都是自己独立的类集合。
4.7.2 类导入
类可以使用它自己包中的所有类和其他包中的所有public类。
您可以通过两种方式访问另一个包中的公共类。第一个简单地使用完全限定名;即包名后面跟类名。例如:
java.time.LocalDate today = java.time.LocalDate.now();
这显然很乏味。一种更简单、更常见的方法是使用import语句。import语句的要点是为您提供一个引用包中类的速记。一旦添加了导入,就不再需要为类提供它们的全名。
您可以导入特定的类或整个包。将import语句放在源文件的顶部(但放在任何package语句的下面)。例如,可以使用语句导入java.time包中的所有类
import java.time.*;
然后你能够使用
LocalDate today = LocalDate.now();
而不用指定包前缀。还可以导入包中的特定类:
import java.time.LocalDate;
java.time.*
语法没有那么冗长乏味。它对代码大小没有负面影响。但是,如果您显式地导入类,代码的读卡器就会确切地知道您使用的是哪些类。
提示
在Eclipse中,您可以选择菜单选项Source→Organize Imports。诸如import java.utils.*;之类的包语句会自动扩展到特定的导入列表中,如
import java.util.ArrayList; import java.util.Date;
这是一个非常方便的功能。
但是,请注意,您只能使用*符号来导入单个包。不能使用import java.*或import Java.*.*来使用Java前缀导入所有包。
大多数情况下,您只需导入所需的包,而不必太担心它们。唯一需要注意包的时候是当您有名称冲突时。例如,java.util和java.sql包都有一个Date类。假设您编写了一个导入两个包的程序。
import java.util.*;
import java.sql.*;
如果现在使用Date类,则会得到一个编译时错误:
Date today; // ERROR--java.util.Date or java.sql.Date?
编译器无法确定所需的Date类。您可以通过添加特定的import语句来解决此问题:
import java.util.*;
import java.sql.*;
import java.util.Date;
如果你真的需要两个Date类呢?然后在每个类名中使用完整的包名称:
var deadline = new java.util.Date();
var today = new java.sql.Date(. . .);
在包中定位类是编译器的一项活动。类文件中的字节码总是使用完整的包名称来引用其他类。
C++注意
C++程序员有时会混淆
import
和#include
。这两者没有共同点。在C++中,您必须使用#include
来包含外部特征的声明,因为C++编译器不查看任何文件,除非它正在编译的文件及其显式包含的头文件。Java编译器将高兴地查看其他文件,只要您告诉它在哪里查找。在Java中,可以通过显式地命名所有类(如java.util.Date)来完全避免导入机制。在C++中,不能避免#include指令冲突。
import声明的唯一好处是方便。可以使用比完整包名称短的名称引用类。例如,在import java.utils.*(或import java.util.Date)语句之后,可以简单地将java.util.Date类引用为Date。
在C++中,类似于包机制的构造是命名空间特征。将Java中的package和import语句视为C++中的指令namespace和using的类似物。
4.7.3 static import
import语句的一种形式允许导入静态方法和字段,而不仅仅是类。
例如,如果添加指令
import static java.lang.System.*;
到源文件的顶部,可以使用System
类的静态方法和字段,而不使用类名前缀:
out.println("Goodbye, World!"); // i.e., System.out
exit(0); // i.e., System.exit
还可以导入特定的方法或字段:
import static java.lang.System.out;
在实践中,许多程序员想要缩写System.out
或System.exit
,这似乎令人怀疑。结果代码似乎不太清楚。另一方面,
sqrt(pow(x, 2) + pow(y, 2))
似乎比下面的代码更清晰点
Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
4.7.4 将类添加到包中
要将类放在包中,请将包的名称放在源文件的顶部,在定义包中类的代码之前。例如,清单4.7中的Employee.java文件是这样开始的:
package com.horstmann.corejava;
public class Employee
{
. . .
}
如果不在源文件中放置package
语句,则该源文件中的类属于未命名的包。未命名的包没有包名称。到目前为止,我们所有的示例类都位于未命名的包中。
将源文件放入与完整包名称匹配的子目录中。例如,com.horstmann.corejava
包中的所有源文件都应该位于子目录com/horstmann/corejava
(Windows上的com\horstmann\corejava
)中。编译器将类文件放入同一目录结构中。
清单4.6和4.7中的程序分布在两个包上:PackageTest
类属于未命名的包,Employee
类属于com.horstmann.corejava
包。因此,Employee.java
文件必须位于com/horstmann/corejava
子目录中。换句话说,目录结构如下:
要编译这个程序,只需更改到基本目录并运行命令
javac PackageTest.java
编译器自动查找文件com/horstmann/corejava/Employee.java
并编译它。
让我们来看一个更现实的例子,其中我们不使用未命名的包,而是将类分布在多个包上(com.horstmann.corejava
和com.mycompany
)。
在这种情况下,仍然必须从基本目录(即包含com
目录的目录)编译和运行类:
javac com/mycompany/PayrollApp.java
java com.mycompany.PayrollApp
再次注意,编译器对文件(具有文件分隔符和扩展名.java
)进行操作,而Java解释器加载一个类(带有点分隔符)。
提示
从下一章开始,我们将使用源代码包。这样,您就可以为每个章节而不是每个部分创建一个IDE项目。
小心
编译器编译源文件时不检查目录结构。例如,假设您有一个以下面指令开头的源文件
package com.mycompany;
即使文件不包含在子目录com/mycompany中,也可以编译该文件。如果源文件不依赖于其他包,则编译时不会出错。但是,除非首先将所有类文件移动到正确的位置,否则生成的程序将不会运行。如果包与目录不匹配,虚拟机将找不到类。
清单4.6 PackageTest/PackageTest.java
import com.horstmann.corejava.*;
// the Employee class is defined in that package
import static java.lang.System.*;
/**
* This program demonstrates the use of packages.
* @version 1.11 2004-02-19
* @author Cay Horstmann
*/
public class PackageTest
{
public static void main(String[] args)
{
// because of the import statement, we don't have to use
// com.horstmann.corejava.Employee here
var harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
harry.raiseSalary(5);
// because of the static import statement, we don't have to use System.out here
out.println("name=" + harry.getName() + ",salary=" + harry.getSalary());
}
}
清单4.7 PackageTest/com/horstmann/corejava/Employee.java
package com.horstmann.corejava;
// the classes in this file are part of this package
import java.time.*;
// import statements come after the package statement
/**
* @version 1.11 2015-05-08
* @author Cay Horstmann
*/
public class Employee
{
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String name, double salary, int year, int month, int day)
{
this.name = name;
this.salary = salary;
hireDay = LocalDate.of(year, month, day);
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
}
4.7.5 包访问
您已经遇到了访问修饰符public
和private
。标记为public
的特性可以被任何类使用。私有特性只能由定义它们的类使用。如果不指定public
或private
,那么同一个包中的所有方法都可以访问特性(即类、方法或变量)。
考虑清单4.2中的程序。Employee
类未定义为公共类。因此,只有同一个包中的其他类(如EmployeeTest
)才能访问这个未命名包。对于类,这是一个合理的默认值。然而,对于变量来说,这是一个不幸的选择。变量必须显式标记为private
,否则将默认为具有包访问权限。当然,这会破坏封装。问题是很容易忘记键入private
关键字。下面是java.awt包中window类的一个示例,它是随JDK提供的源代码的一部分:
public class Window extends Container
{
String warningString;
. . .
}
注意warningString
变量不是private
的!这意味着java.awt包中所有类的方法都可以访问这个变量,并将其设置为他们喜欢的任何类型(例如“Trust me!”)。实际上,访问该变量的唯一方法是在window类中,因此将该变量设置为私有是完全合适的。也许程序员很匆忙地输入了代码,而仅仅忘记了私有修饰符?也许没人在乎?20多年后,这个变量仍然不是私有的。不仅随着时间的推移,新字段被添加到类中,而且其中大约一半也不是私有字段。
这可能是个问题。默认情况下,包不是关闭的实体。也就是说,任何人都可以向包中添加更多的类。当然,有敌意或无头绪的程序员可以通过包访问添加修改变量的代码。例如,在早期版本的Java中,将另一个类导入java.awt
包是一件容易的事情。简单地从class开始
package java.awt;
然后,将生成的类文件放置在类路径上的某个子目录Java/awt
中,并且访问java.awt
包的内部结构。通过这个技巧,可以设置警告字符串(参见图4.9)。
图4.9 在小程序窗口中更改警告字符串
从版本1.2开始,JDK实现器操纵类加载器,以明确禁止加载用户定义的包名以"java"开头的类。当然,您自己的类不会从这种保护中受益。另一个现在已经过时的机制是让JAR文件声明包是密封的,以防止第三方增加它们。
现在,您应该使用模块来封装包。我们将在第二卷第9章详细讨论模块。
4.7.6 类路径
如您所见,类存储在文件系统的子目录中。类的路径必须与包名称匹配。
类文件也可以存储在JAR(Java归档)文件中。JAR文件以压缩格式包含多个类文件和子目录,节省了空间并提高了性能。当您在程序中使用第三方库时,通常会给您一个或多个JAR文件。您将在第11章中看到如何创建自己的JAR文件。
提示
JAR文件使用zip格式来组织文件和子目录。您可以使用任何zip工具查看JAR文件。
要在程序之间共享类,需要执行以下操作:
- 将类文件放在目录中,例如
/home/user/classdir
。请注意,此目录是包树的根目录。如果添加com.horstmann.corejava.Employee
类,那么Employee.class文件必须位于子目录/home/user/classdir/com/horstmann/corejava
中。 - 将任何JAR文件放在目录中,例如
/home/user/archives
。 - 设置类路径。类路径是可以包含类文件的所有位置的集合。
在UNIX中,类路径上的元素由冒号分隔:
/home/user/classdir:.:/home/user/archives/archive.jar
在Windows中,它们用分号分隔:
c:\classdir;.;c:\archives\archive.jar
在这两种情况下,句点表示当前目录。
此类路径包含
- 基本目录
/home/user/classdir
或c:\classdir
; - 当前目录(.);和
- jar文件
/home/user/archives/archive.jar
或c:\archives\archive.jar
。
从Java 6开始,您可以为JAR文件目录指定通配符,如下所示:
/home/user/classdir:.:/home/user/archives/'*'
或者
c:\classdir;.;c:\archives\*
在UNIX中,必须转义*
以防止shell扩展。
归档目录中的所有JAR文件(但不是.class
文件)都包含在这个类路径中。
Java API总是搜索类;不要在类路径中显式地包含它。
小心
Javac编译器总是在当前目录中查找文件,但是如果“.”目录在类路径上,那么Java虚拟机启动器只查看当前目录。如果没有类路径集,那么默认的类路径由“.”目录组成并不是问题。但是,如果您设置了类路径,忘记包含“.”目录,那么您的程序将毫无错误地编译,但不会运行。
假设虚拟机搜索com.horstmann.corejava.Employee类的类文件。它首先在Java API类中查找。它在那里找不到类文件,所以它转到类路径。然后查找以下文件:
/home/user/classdir/com/horstmann/corejava/Employee.class
- 从当前目录开始的
com/horstmann/corejava/Employee.class
- 在
/home/user/archives/archive.jar
中的com/horstmann/corejava/Employee.class
与虚拟机相比,编译器查找文件要困难得多。如果引用一个类而不指定其包,编译器首先需要找出包含该类的包。它将所有import
指令作为类的可能源进行查询。例如,假设源文件包含指令
import java.util.*;
import com.horstmann.corejava.*;
源代码引用一个类Employee
。然后,编译器尝试在当前包中查找java.lang.Employee
(因为java.lang
包在默认情况下总是被导入)、java.util.Employee
、com.horstmann.corejava.Employee
和Employee
。它在类路径的所有位置中搜索这些类中的每一个。如果找到多个类,则是编译时错误。(完全限定的类名必须是唯一的,因此import
语句的顺序无关紧要。)
编译器更进一步。它查看源文件以查看源文件是否比类文件新。如果是这样,源文件将自动重新编译。回想一下,您只能从其他包中导入公共类。源文件只能包含一个公共类,并且文件名和公共类的名称必须匹配。因此,编译器可以很容易地找到公共类的源文件。但是,可以从当前包导入非公共类。这些类可以在具有不同名称的源文件中定义。如果从当前包导入一个类,编译器将搜索当前包的所有源文件,以查看哪个源文件定义了类。
4.7.7 设置类路径
最好用选项-classpath(或-cp或Java 9的–class-path)指定类路径:
java -classpath /home/user/classdir:.:/home/user/archives/archive.jar MyProg
或者
java -classpath c:\classdir;.;c:\archives\archive.jar MyProg
整个命令必须键入一行。最好将如此长的命令行放入shell脚本或批处理文件中。
使用-classpath
选项是设置类路径的首选方法。另一种方法是CLASSPATH
环境变量。细节取决于你的shell。通过Bourne Again shell (bash),使用命令
export CLASSPATH=/home/user/classdir:.:/home/user/archives/archive.jar
通过Windows shell,使用
set CLASSPATH=c:\classdir;.;c:\archives\archive.jar
类路径一直设置着,直到shell退出。
小心
有些人建议永久设置CLASSPATH环境变量。这通常是个坏主意。人们忘记了全局设置,当他们的类没有正确加载时会感到惊讶。一个特别值得谴责的例子是苹果在Windows中的QuickTime安装程序。几年来,它全局性地将
CLASSPATH
设置为指向所需的JAR文件,但没有在classpath中包含当前目录。因此,无数的Java程序员在程序编译但无法运行时会分散注意力。
小心
在过去,有些人建议通过将所有JAR文件放到
jre/lib/ext
目录中,完全绕过类路径。该机制在Java 9中已经过时,但它总是坏建议。当从扩展目录加载早已遗忘的类时,很容易混淆。
注意
至于Java 9,也可以从模块路径加载类。我们将在第二卷第9章中讨论模块和模块路径。