4.7 包

4.7 包

java使用包将类组织在一个集合中。借助包可以方便的组织自己的代码,将自己的代码与别人提供的代码库分开管理。

4.7.1 包名

使用包的主要原因是确保类名的唯一性。假如两个程序员不约而同地建立起了两个Employee类。只要将这些类放置到不同包中,就不会产生冲突。事实上,为了保证包名的绝对唯一性,要用一个因特网域名逆序的形式作为包名,然后对于不同的工程使用不同的子包。例如,考虑域名horstmann.com。如果逆序来写,就得到包名com.horstmann。然后可以追加一个子工程名,如:com.horstmann.corejava。如果再把Employee类放到这个包里,那么这个类的“完全限定”名就是 com.horstmann.corejava.Employee。
注释:从编译器的角度来看,嵌套的包之间没有任何关系。例如:java.util包和java.util.jar包毫无关系。每一个包都是独立的类集合。

4.7.2 类的导入

一个类可以使用所属包中的所有类,以及其他包中的公共类。
我们可以采用两种方式去访问另一个包中的公共类。第一种方式就是完全限定名。就是包名后面跟着类名。例如:

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

这显然很繁琐。更简单更常用的方式是使用import语句。import语句是一种引入包中各个类的简洁方式。一旦使用了import语句,使用类时,就不必写出类的全名。
可以使用import语句导入一个特定类或者整个包。import语句应该位于源文件的顶部。例如,可以使用下面的这条语句导入java.util包中的所有类。

import java.time.*;

然后就可以使用:

LocalDate today=LocalDate.now();

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

import java.time.LocalDate;

java.time.*的语法比较简单,对代码的规模也没有任何负面影响。不过,如果能够明确的指出所导入的类,代码的读者就能更加准确的知道你使用了哪些类。
但是,需要注意的是,只能使用星号导入一个包,而不能使用import java.* 或者 import java.*.* 导入以java为前缀的所有包。
大多数情况下,可以只导入你需要的包,并不必过多的考虑。但是,在发生命名冲突的时候就要注意包了。例如,java.util和java.sql包都有Date类。如果在程序中导入了这两个包:

import java.util.*;
import 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(...);

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

4.7.3 静态导入

有一种import语句允许导入静态方法和静态字段,而不只是类。
例如,如果在源文件顶部添加一条指令:

import static java.lang.System.*;

就可以使用System类的静态方法和静态字段,而不必增加类前缀:

out.println("Good Bye");
exit(0);

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

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/mycomyany下,也可以进行编译。如果它不依赖于其他包,就可以通过编译而不出现编译错误。但是,最终的程序将无法运行,除非先将所有文件移到正确的位置上。如果包与目录不匹配,虚拟机就找不到类。

在IDEA中建立包结构时,不显示包的结构。解决方法:点击小齿轮,取消:compact middle packages

package PackageTest;
import PackageTest.com.horstmann.corejava.*;
import static java.lang.System.*;
public class PackageTest {
    public static void main(String[] args) {
        var harry=new Employee("harry", 50000, 1989, 10, 1);
        harry.raiseSalary(5);
        out.println("name="+harry.getName()+",salary="+harry.getSalary());
    }
}
package PackageTest.com.horstmann.corejava;
import java.time.*;

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=salary+raise;
    }
}

4.7.5 包访问

前面已经接触过访问修饰符public和private。标记为public的部分可以由任意类使用;标记为private的部分只能由定义它们的类使用。如果没有指定public或者private,这个部分(类,方法或变量)可以被同一个包中的所有方法访问。
下面再来考虑程序清单4-2,这个程序中,没有将Employee类定义为公共类,因此只有在同一个包中的其他类可以访问,例如:EmployeeTest。对于类来说,这种默认方式时合乎情理的。但是,对于变量来说就有些不适宜了,变量必须显式地标记为private,不然的话将默认为包可访问。显然,这样做会破坏封装性。问题是人们经常忘记键入关键字private。java.awt包中的window类就是一个典型的示例。
java.awt包是JDK提供的部分源代码:

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

请注意,这里的warningString变量不是private。这意味着java.awt包中所有类的方法都可以访问该变量,并将它设置为任意值。实际上,只有Window类的方法访问这个变量,因此本应该将它设置为私有变量才合适。可能是程序员敲代码时匆忙之中忘记private修饰符了?也可能是没有人关心这个问题?已经20多年了,这个变量仍然不是私有变量。不仅如此,这个类还陆续增加了一些新的字段,而其中大约有一半也不是私有的。
这可能会称为一个问题。在默认情况下,包不是封闭的实体。也就是说,任何人都可以包中添加更多的类。当然,有恶意或低水平的程序员很可能利用包的可见性添加一些能修改变量的代码。例如,在java程序设计语言的早期版本中,只需要将以下这条语句放在类文件的开头,就可以很容易的在java.awt包中混入其他类:

package java.awt;

然后,把得到的类文件放置在类路径上某处的java/awt子目录下,这样就可以访问Java.awt包的内部了。使用这一手段,完全可以设置警告字符串。
从1.2版开始,JDK的实现者修改了类加载器,明确禁止加载包名以“java.”开头的用户自定义的类。当然,用户自定义的类无法从这种保护中受益。另一种机制是让JAR文件声明包为密封的(sealed),以防止第三方修改,但这种机制已经过时。现在应当使用模块封装包。

4.7.6 类路径

在前面已经看到,类存储在文件系统的子目录中。类的路径必须与包名匹配。
另外,类文件也可以存储在JAR(java归档)文件中。在一个JAR文件中,可以包含多个压缩形式的类文件和子目录,这样既可以节省空间又可以改善性能。在程序中用到第三方的库文件时,你通常要得到一个或多个需要包含的JAR文件。第11章将介绍如何创建你自己的JAR文件。(这就很尴尬,第11章是Swing图形界面,不看的。。。)
提示: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

不论是UNIX还是windows,都用句点 . 表示当前目录。
类路径包括:

  • 基目录 /home/user/classdir 或者 c:\classes;
  • 当前目录 . ;
  • 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扩展。
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.Employee、com.horstmann.corejava.Employee.java和当前包中的Employee。它会在类路径所有位置中搜索以上各个类。如果找到了一个以上的类,就会产生编译时错误(因为完全限定类名是唯一的,所以import的语句次序并不重要)。
编译器的任务不止这些,他还要查看源文件是否比类文件新。如果是这样的话,那么源文件就被自动地 重新编译。在前面你已经知道,只可以导入其他包中的公共类。一个源文件只能包含一个公共类,并且文件名和类名必须匹配。因此,编译器很容易找到公共类的源文件。不过,还可以从当前包中导入非公共类。这些类有可能在与类名不同的源文件中定义。如果从当前包中导入一个类,编译器就要搜索当前包中的所有源文件,查看哪个原文件定义了这个类。

4.7.7 设置类路径

最好使用-classpath 或者 -cp 选项指定类路径:
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的QiuckTime安装程序。很多年来,它都将CLASSPATH全局设置为指向它需要的一个JAR文件,而没有在类路径中包含当前路径。因此,当程序员编译后却不能运行时,无数java程序员不得不花费很多精力去解决这个问题。
警告: 过去,有人建议完全绕开类路径,将所有的文件都放在jie/lib/ext目录中。这种机制已经过时,不过不管怎样这都是一个不好的建议。很可能会从扩展目录加载一些已经遗忘很久的类,着会让人很困惑。
注释:java 9 中,还可以从模块路径加载类。卷二的第九章讨论模块和模块路径。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值