黄子涵

4.7 包

Java允许使用(package)将类组织在一个集合中。借助包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。下面我们将介绍如何使用和创建包。

包名

使用包的主要原因是确保类名的唯一性。假如两个程序员不约而同地建立了Employee类。只要将这些类放置在不同的包中,就不会产生冲突。事实上,为了保证包名的绝对唯一性,要用一个因特网域名(这显然是唯一的)以逆序的形式作为包名,然后对于不同的工程使用不同的子包。例如,考虑域名horstmann.com。如果逆序来写,就得到了包名com.horstmann。然后可以追加一个工程名,如com.horstmann.corejava。如果再把Employee类放在这个包里,那么这个类的“完全限定”名就是com.horstmann.corejava.Employee

image

注释

从编译器的角度来看,嵌套的包之间没有任何关系。例如,java.util包与java.util.jar包毫无关系。每一个包都是独立的类集合。

image

类的导入

一个类可以使用所属包中的所有类,以及其他包中的公共类(public class)。

我们可以采用两种方式访问另一个包中的公共类。第一种方式就是使用完全限定名(fully qualified name);就是包名后面跟着类名。例如:

java.time.LocalDate today = java.time.LocalDate.now();

程序示例

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   {
	   java.time.LocalDate huangzihan_today = java.time.LocalDate.now();
	   System.out.println(huangzihan_today);
   }
}

运行结果

2021-07-19

import语句

这显然很烦琐。更简单且更常用的方式是使用import语句。import语句是一种引用包中各个类的简捷方式。一旦使用了import语句,在使用类时,就不必写出类的全名了。

可以使用import语句导入一个特定的类或者整个包。import语句应该位于源文件的顶部(但位于package语句的后面)。例如,可以使用下面这条语句导入java.util包中的所有类。

import java.time.*;

然后,就可以使用

LocalDate today = LocalDate.now();

而无须在前面加上包前缀。还可以导入一个包中的特定类:

import java.time.LocalDate;

java.time.*的语法比较简单,对代码的规模也没有任何负面影响。不过,如果能够明确地指出所导入的类,代码的读者就能更加准确地知道你使用了哪些类。

程序示例一

import java.time.LocalDate;

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   {
	   LocalDate huangzihan_today = LocalDate.now();
	   System.out.println(huangzihan_today);
   }
}

运行结果一

2021-07-19

程序示例二

import java.time.*;

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   {
	   LocalDate huangzihan_today = LocalDate.now();
	   System.out.println(huangzihan_today);
   }
}

运行结果二

2021-07-19

提示

在Eclipse中,可以使用莱单选项Source——>Organize Imports。诸如import java.util.*;等包语句将会自动扩展为一组特定的导入语句,如:

import java.util.Arraylist;
import java.util.Date;

image

这是一个十分便捷的特性。

但是,需要注意的是,只能使用星号(*)导入一个包,而不能使用import java.*import java.*.*导入以java为前缀的所有包。

包的命名冲突

在大多数情况下,可以只导入你需要的包,并不必过多地考虑它们。但在发生命名冲突的时候,就要注意包了。例如,java.utiljava.sql包都有Date类。如果在程序中导入了这两个包:

import java.util.*;
import java.sql.*;

在程序中使用Date类的时候,就会出现一个编译错误:

Date today;   // ERROR--java.util.Date or java.sql.Date?

程序示例一

import java.util.*;
import java.sql.*;

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   {
	   Date huangzihan_today;
   }
}

运行结果一

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
	The type Date is ambiguous

	at HuangZiHanTest.main(HuangZiHanTest.java:8)

此时编译器无法确定你想使用的是哪一个Date类。可以增加一个特定的import语句来解决这个问题:

import java.util.*;
import java.sql.*;
import java.util.Date;

程序示例二

import java.util.*;
import java.sql.*;
import java.util.Date;

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   {
	   Date huangzihan_today;
   }
}

运行结果二


如果这两个Date类都需要使用,又该怎么办呢?答案是,在每个类名的前面加上完整的包名。

var deadline = new java.util.Date();
var today = new java.sql.Date(...);

在包中定位类是编译器(compiler)的工作。类文件中的字节码总是使用完整的包名引用其他类。

程序示例三

import java.util.*;
import java.sql.*;
import java.util.Date;

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   {
	   var huangzihan_deadline = new java.util.Date();
	   var huangzihan_today = new java.sql.Date(0);
	   
	   System.out.println(huangzihan_deadline);
	   System.out.println(huangzihan_today);
   }
}

运行结果三

Mon Jul 19 10:33:53 CST 2021
1970-01-01

静态导入

有一种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.*;

public class HuangZiHanTest
{  
   public static void main(String[] huangzihan_args)
   {
	   out.println("黄子涵");
	   exit(0);
   }
}

运行结果

黄子涵

另外,还可以导入特定的方法或字段:

import static java.lang.System.out;

实际上,是否有很多程序员想要用简写System.outSystem.exit,这一点很让人怀疑。这样写出的代码看起来不太清晰。不过,

sqrt(pow(x, 2) + pow(y, 2))

看起来比

Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))

清晰得多。

在包中增加类

要想将类放入包中,就必须将包的名字放在源文件的开头,即放在定义这个包中各个类的代码之前。例如,下面程序的文件Employee.java开头是这样的:

package com.horstmann.corejava;

public class Employee
{
    . . .
}

如果没有在源文件中放置package语句,这个源文件中的类就属于无名包(unnamed package)。无名包没有包名。到目前为止,我们定义的所有类都在这个无名包中。

将源文件放到与完整包名匹配的子目录中。例如,com.horstmann.corejava包中的所有源文件应该放置在子目录com/horstmann/corejava中(Windows中则是com\horstmann\corejava)。编译器将类文件也放在相同的目录结构中。

下面的程序分放在两个包中:PackageTest类属于无名包;Employee类属于com.horstmann.corejava包。因此,Employee.java文件必须包含在子目录com/horstmann/corejava中。换句话说,目录结构如下所示:

. (base directory)
  |__ PackageText.java
  |__ PackageText.class
  |__ com/
      |__ horstmann/
          |__ corejava/
              |__ Employee.java
              |__ Employee.class

要想编译这个程序,只需切换到基目录,并运行命令

javac PackageTest.java

编译器就会自动地查找文件com/horstmann/corejava/Employee.java并进行编译。

下面看一个更加实际的例子。在这里不使用无名包,而是将类分别放在不同的包中(com.horstmann.corejavacom.mycompany)。

. (base directory)
 |__ com/
     |__ horstmann/
     |   |__ corejava/
     |       |__ Employee.java
     |       |__ Employee.class
     |__ mycompany/
     |__ PayrollApp.java
     |__ PayrollApp.class

在这种情况下,仍然要从基目录编译和运行类,即包含com目录的目录:

javac com/mycompany/PayrollApp.java 
java com.mycompany.PayrolLApp

需要注意,编译器处理文件(带有文件分隔符和扩展名.java的文件),而Java解释器加载类(带有.分隔符)。

程序示例

/JavaCore/src/Huangizhan_PackageTest

package Huangizhan_PackageTest;

import com.huangzihan.corejava.*;

//Employee 类在该包中定义

import static java.lang.System.*;

/*
 * @功能:该程序演示了包的使用。
 * @版本:1.11
 * @时间:2021-07-19
 * @作者:黄子涵
 * 
 */

public class Huangizhan_PackageTest
{
	public static void main(String[] huangzihan_args) 
	{
		//由于导入语句,我们不必使用
		//com.huangzihan.corejava
		
		var huangzihan = new Huangzihan_Employee("黄子涵", 50000, 1989, 10, 1);
		
		huangzihan.huangzihan_raiseSalary(5);
		
		//由于静态导入语句,我们不必使用 System.在这里
		out.println("名字=" + huangzihan.huangzihan_getName() + ",工资=" + huangzihan.huangzihan_getSalary());
	}
}

/JavaCore/src/com/huangzihan/corejava

package com.huangzihan.corejava;

//这个文件中的类是这个包的一部分

import java.time.*;

//导入语句在包语句之后

/*
 * @版本:1.11
 * @时间:2015-05-08
 * @作者:黄子涵
 * 
 */

public class Huangzihan_Employee
{
	private String huangzihan_name;
	private double huangzihan_salary;
	private LocalDate huangzihan_hireDay;
	
	public Huangzihan_Employee(String huangzihan_name, double huangzihan_salary, int huangzihan_year, int huangzihan_month, int huangzihan_day) 
	{
		this.huangzihan_name = huangzihan_name;
		this.huangzihan_salary = huangzihan_salary;
		huangzihan_hireDay = LocalDate.of(huangzihan_year, huangzihan_month, huangzihan_day);
	}
	
	public String huangzihan_getName() 
	{
		return huangzihan_name;
	}
	
	public double huangzihan_getSalary() 
	{
		return huangzihan_salary;
	}
	
	public LocalDate huangzihan_getHireDay() 
	{
		return huangzihan_hireDay;
	}
	
	public void huangzihan_raiseSalary(double huangzihan_byPercent) 
	{
		double huangzihan_raise = huangzihan_salary * huangzihan_byPercent / 100;
		huangzihan_salary += huangzihan_raise;
	}
}

运行结果

名字=黄子涵,工资=52500.0

警告

编译器在编译源文件的时候检查目录结构。例如,假定一个源文件开头有以下指令:

package com.mycompany;

即使这个源文件不在子目录com/mycompany下,也可以进行编译。如果它不依赖于其他包,就可以通过编译而不会出现编译错误。但是,最终的程序将无法运行,除非先将所有类文件移到正确的位置上。如果包与目录不匹配,虚拟机就找不到类。

包访问

前面已经接触过访问修饰符publicprivate。标记为public的部分可以由任意类使用;标记为private的部分只能由定义它们的类使用。如果没有指定publicprivate,这个部分(类、方法或变量)可以被同一个中的所有方法访问。

如果我们没有将Employee类定义为公共类,因此只有在同一个包(在此是无名的包)中的其他类可以访问,例如EmployeeTest。对于类来说,这种默认方式是合乎情理的。但是,对于变量来说就有些不适宜了,变量必须显式地标记为private,不然的话将默认为包可访问。显然,这样做会破坏封装性。问题是人们经常忘记键入关键字privatejava.awt包中的Window类就是一个典型的示例。java.awt包是JDK提供的部分源代码:

public class Window extends Container 
{
    String warningString;
    . . .
}    

请注意,这里的warningString变量不是private!这意味着java.awt包中的所有类的方法都可以访问该变量,并将它设置为任意值(例如,“Trust me!")。实际上,只有Window类的方法访问这个变量,因此本应该将它设置为私有变量才合适。可能是程序员敲代码时匆忙之中忘记private修饰符了?也可能是没有人关心这个问题?已经20多年了,这个变量仍然不是私有变量。不仅如此,这个类还陆续增加了一些新的字段,而其中大约有一半也不是私有的。

这可能会成为一个问题。在默认情况下,包不是封闭的实体。也就是说,任何人都可以向包中添加更多的类。当然,有恶意或低水平的程序员很可能利用包的可见性添加一些能修改变量的代码。例如,在Java程序设计语言的早期版本中,只需要将以下这条语句放在类文件的开头,就可以很容易地在java.awt包中混入其他类:

package java.awt;

然后,把得到的类文件放置在类路径上某处的java/awt子目录下,这样就可以访问java.awt包的内部了。使用这一手段,完全可以设置警告字符串。

从1.2版开始,JDK的实现者修改了类加载器,明确地禁止加载包名以“java.”开头的用户自定义的类!当然,用户自定义的类无法从这种保护中受益。另一种机制是让JAR文件声明包为密封的(sealed),以防止第三方修改,但这种机制已经过时。现在应当使用模块封装包。

类路径

在前面已经看到,类存储在文件系统的子目录中。类的路径必须与包名匹配。

另外,类文件也可以存储在JAR(Java归档)文件中。在一个JAR文件中,可以包含多个压缩形式的类文件和子目录,这样既可以节省空间又可以改善性能。在程序中用到第三方的库文件时,你通常要得到一个或多个需要包含的JAR文件。

提示

JAR文件使用ZIP格式组织文件和子目录。可以使用任何ZIP工具查看JAR文件。

类能够被多个程序共享

为了使类能够被多个程序共享,需要做到下面几点:

  1. 把类文件放到一个目录中,例如/home/user/classdir。需要注意,这个目录是包树状结构的目录。如果希望增加com.horstmann.corejava.Employee类,那么Employee.class类文件就必须位于子目录
    /home/user/classdir/com/horstmann/corejava中。
  2. 将JAR文件放在一个目录中,例如:/home/user/archives
  3. 设置类路径(class path)。类路径是所有包含类文件的路径的集合。

在UNIX环境中,类路径中的各项之间用冒号(:)分隔:

/home/user/classdir:.:/home/user/archives/archive.jar

而在Windows环境中,则以分号(;)分隔:

c:\classdir;.;c:\archives\archive.jar

不论是UNIX还是Windows,都用句点(.)表示当前目录。

类路径包括:

  • 基目录/home/user/classdirc:\classes;
  • 当前目录(.);
  • JAR文件/home/user/archives/archive.jarc:\archives\archive.jar

从Java 6开始,可以在JAR文件目录中指定通配符,如下:

/home/user/classdir:.:/home/user/archives/'*'

或者

C:\classdir;.;c:\archives\*

在UNIX中,*必须转义以防止shell扩展。

archives目录中的所有JAR文件(但不包括.class文件)都包含在这个类路径中。

由于总是会搜索Java API的类,所以不必显式地包含在类路径中。

警告

javac编译器总是在当前的目录中查找文件,但java虚拟机仅在类路径中包含“.”目录的时候才查看当前目录。如果没有设置类路径,那么没有什么问题,因为默认的类路径会包含“.”目录。但是如果你设置了类路径却忘记包含“.”目录,那么尽管你的程序可以没有错误地通过编译,但不能运行。

类路径所列出的目录和归档文件是搜寻类的起始点。下面看一个类路径示例:

/home/user/classdir:.:/home/user/archives/archive.jar

假定虚拟机要搜寻com.horstmann.corejava.Employee类的类文件。它首先要查看Java API类。显然,在那里找不到相应的类文件,所以转而查看类路径。然后查找以下文件:

  • /home/user/classdir/com/horstmann/corejava/Employee.class
  • com/horstmann/corejava/Employee.class(从当前目录开始)
  • com/horstmann/corejava/Employee.class(/home/user/archives/archive.jar中)

编译器查找文件要比虚拟机复杂得多。如果引用了一个类,而没有指定这个类的包,那么编译器将首先查找包含这个类的包。它会查看所有的import指令,确定其中是否包含这个类。例如,假定源文件包含指令:

import java.util.*;
import com.horstmann.corejava.*;

并且源代码引用了Employee类。编译器将尝试查找java.lang.Employee(因为java.lang包总是会默认导入)、java.util.Employeecom.horstmann.corejava.Employee和当前包中的Employee。它会在类路径所有位置中搜索以上各个类。如果找到了一个以上的类,就会产生编译时错误(因为完全限定类名必须是唯一的,所以import语句的次序并不重要)。

编译器的任务不止这些,它还要查看源文件是否比类文件新。如果是这样的话,那么源文件就被自动地重新编译。在前面已经知道,只可以导入其他包中的公共类。一个源文件只能包含一个公共类,并且文件名与公共类名必须匹配。因此,编译器很容易找到公共类的源文件。不过,还可以从当前包中导入非公共类。这些类有可能在与类名不同的源文件中定义。如果从当前包中导入一个类,编译器就要搜索当前包中的所有源文件,查看哪个源文件定义了这个类。

设置类路径

最好使用-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中Apple的QuickTime安装程序。很多年来,它都将CLASSPATH全局设置为指向它需要的一个JAR文件,而没有在类路径中包含当前路径。因此,当程序编译后却不能运行时,无数Java程序员不得不花费很多精力去解决这个问题。

警告

过去,有人建议完全绕开类路径,将所有的文件都放在jre/lib/ext目录中。这种机制在Java 9中已经过时,不过不管怎样这都是一个不好的建议。很可能会从扩展目录加载一些已经遗忘很久的类,这会让人非常困惑。

注释

在Java9中,还可以从模块路径加载类。

posted @ 2021-08-24 21:54  黄子涵  阅读(65)  评论(0编辑  收藏  举报