corejava11(4.7 包)

本文围绕Java的包展开,介绍了包名用于保证类名唯一,类导入可使用import语句简化引用。还提及static import可导入静态方法和字段,将类添加到包需匹配目录结构。此外,阐述了包访问规则、类路径概念及设置方法,同时提醒了相关注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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.outSystem.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.corejavacom.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 包访问

您已经遇到了访问修饰符publicprivate。标记为public的特性可以被任何类使用。私有特性只能由定义它们的类使用。如果不指定publicprivate,那么同一个包中的所有方法都可以访问特性(即类、方法或变量)。

考虑清单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文件。

要在程序之间共享类,需要执行以下操作:

  1. 将类文件放在目录中,例如/home/user/classdir。请注意,此目录是包树的根目录。如果添加com.horstmann.corejava.Employee类,那么Employee.class文件必须位于子目录/home/user/classdir/com/horstmann/corejava中。
  2. 将任何JAR文件放在目录中,例如/home/user/archives
  3. 设置类路径。类路径是可以包含类文件的所有位置的集合。

在UNIX中,类路径上的元素由冒号分隔:

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

在Windows中,它们用分号分隔:

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

在这两种情况下,句点表示当前目录。

此类路径包含

  • 基本目录/home/user/classdirc:\classdir
  • 当前目录(.);和
  • jar文件/home/user/archives/archive.jarc:\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.Employeecom.horstmann.corejava.EmployeeEmployee。它在类路径的所有位置中搜索这些类中的每一个。如果找到多个类,则是编译时错误。(完全限定的类名必须是唯一的,因此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章中讨论模块和模块路径。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值