4.7 包
Java 允许使用 包(package) 将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
标准的 Java 类库分布在多个包中,包括 java.lang、java.util 和java.net等。标准的 Java包具有一个层次结构。如同硬盘的目录嵌套一样,也可以使用嵌套层次组织包。所有标准的Java 包都处于java 和 javax 包层次中。
使用包的主要原因是确保类名的唯一性。假如两个程序员不约而同地建立了Employee
类。只要将这些类放置在不同的包中, 就不会产生冲突。事实上,为了保证包名的绝对唯一性, Sun 公司建议将公司的因特网域名(这显然是独一无二的) 以逆序的形式作为包名,并且对于不同的项目使用不同的子包。例如,horstmann.com 是本书作者之一注册的域名。逆序形式为com.horstmann。这个包还可以被进一步地划分成子包,如 com.horstmann.corejava。
从编译器的角度来看, 嵌套的包之间没有任何关系。例如,java.util 包与java.util.jar 包毫无关系。每一个都拥有独立的类集合。
4.7.1 类的导入
一个类可以使用所属包中的所有类,以及其他包中的公有类(public class)。我们可以采用两种方式访问另一个包中的公有类。
第一种方法是在每个类名之前添加完整的包名。例如:
java.time.LocalDate today = java.time.LocalDate.now();
这显然很繁琐。更简单且更常用的方式是使用 import
语句。import
语句是一种引用包含在包中的类的简明描述。一旦使用了 import
语句,在使用类时,就不必写出包的全名了。
可以使用 import
语句导入一个特定的类或者整个包。import
语句应该位于源文件的顶部(但位于 package 语句的后面)。例如, 可以使用下面这条语句导入 java.util
包中所有的类。
import java.util.*;
然后就可以使用:
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 today; // Error--java.util.Date or java.sql.Date?
此时编译器无法确定程序使用的是哪一个 Date
类。可以采用增加一个特定的 import
语句来解决这个问题:
import java.util.*;
import java.sql.*;
import java.util.Date;
如果这两个 Date
类都需要使用,又该怎么办呢? 答案是,在每个类名的前面加上完整的包名。
java.util.Date deadline = new java.util.Date();
java.sql.Date today = new java.sql.Date(...);
在包中定位类是编译器 (compiler) 的工作。 类文件中的字节码肯定使用完整的包名来引用其他类。
4.7.2 静态导入
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;
但是有时候,这样的编写形式不利代码的清晰度。
4.7.3 将类放入包中
要想将一个类放人包中, 就必须将包的名字放在源文件的开头,包中定义类的代码之前。
package com.horstmann.corejava;
public class Employee {
...
}
如果没有在源文件中放置 package
语句, 这个源文件中的类就被放置在一个**默认包(defaulf package)**中。默认包是一个没有名字的包。在此之前,我们定义的所有类都在默认包中。
将包中的文件放到与完整的包名匹配的子目录中。例如,com.horstmann.corejava
包中的所有源文件应该被放置在子目录 com/horstmann/corejava
(Windows 中 com\horstmann\corejava
) 中。编译器将类文件也放在相同的目录结构中。
如果Employee类的开头是 package com/horstmann/corejava
,那么Employee.java
源文件必须放在子目录com/horstmann/corejava
中。例如:
PackageTest
类放置在默认包中;Employee
类放置在 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解释器加载类(带有 .
分隔符 )。
警告⚠️:编译器在编译源文件的时候不检查目录结构。例如,假定有一个源文件开头有下列语句:
package com.mycompany;
即使这个源文件没有在子目录
com/mycompany
下, 也可以进行编译。如果它不依赖于其他包, 就不会出现编译错误。但是, 最终的程序将无法运行, 除非先将所有类文件移到正确的位置上。 如果包与目录不匹配, 虚拟机就找不到类。
4.7.4 包的作用域
前面已经接触过访问修饰符 public
和 private
。标记为 public
的部分可以被任意的类使用;标记为 private
的部分只能被定义它们的类使用。如果没有指定 public
或 private
, 这个部分(类、方法或变量)可以被同一个包中的所有方法访问。
4.9 文档注释
4.9.2 类注释
类注释必须放在import
语句之后,类定义之前
例如:
/**
* A {©code Card} object represents a playing card, such
* as "Queen of Hearts. A card has a suit (Diamond, Heart,
* Spade or Club) and a value (1 = Ace, 2 . . . 10, 11 = Jack,
* 12 = Queen, 13 = King)
*/
public class Card {
...
}
4.9.3 方法注释
每一个方法注释必须放在所描述的方法之前。除了通用标记之外, 还可以使用下面的标记:
-
@param
变量描述这个标记将对当前方法的
param
(参数)部分添加一个条目。这个描述可以占据多行, 并可以使用 HTML 标记。一个方法的所有@param
标记必须放在一起。 -
@return
描述这个标记将对当前方法添加
return
(返回)部分。这个描述可以跨越多行, 并可以使用 HTML 标记。 -
@throws
类描述这个标记将添加一个注释,用于表示这个方法有可能抛出异常。
下面是一个方法注释的示例:
/**
* Raises the salary of an employee.
* @param byPercent the percentage by which to raise the salary (e.g. 10 means 10%)
* ©return the amount of the raise
*/
public double raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
return raise;
}
4.9.4 域注释
只需要对公有域(通常指的是静态常量)建立文档。例如:
/**
* The "Hearts" card suit
*/
public static final int HEARTS = 1;
4.9.5 通用注释
下面的标记可以用在类文档的注释中:
-
@author name
这个标记将产生一个
author
(作者)条目。可以使用多个@author
标记,每个@author
标记对应一个作者 -
@version text
这个标记将产生一个
version
(版本)条目。这里的text
可以是对当前版本的任何描述。
下面的标记可以用于所有的文档注释中:
-
@since text
这个标记将产生一个
since
(始于)条目。这里的text
可以是对引人特性的版本描述。例如,@since version 1.7.10
-
@deprecated text
这个标记将对类、 方法或变量添加一个不再使用的注释。
text
中给出了取代的建议。例如:
@deprecated Use <code> setVisible(true) </code> instead
通过@see
和@link
标记,可以使用超级链接,链接到javadoc文档的相关部分或外部文档。
-
@see reference
这个标记将在“ see also” 部分增加一个超级链接。它可以用于类中,也可以用于方
法中。这里的
reference
可以选择下列情形之一:package.class#feature label
<a href="...">label</a>
"text"
第一种情况是最常见的。只要提供类、 方法或变量的名字,javadoc 就在文档中插入一个超链接。例如,
@see com.horstmann.corejava.Employee#raiseSalary(double)
建立一个链接到 com.horstmann.corejava.Employee
类的 raiseSalary(double)
方法的超链接。可以省略包名, 甚至把包名和类名都省去,此时,链接将定位于当前包或当前类。
需要注意,一定要使用井号(#), 而不要使用句号(.)分隔类名与方法名,或类名与变量名。
第二种情况,如果@see
后面跟着<
字符,就需要指定一个超链接。可以链接到任何URL,例如:
@see <a href="www.horstmann.com/corejava.html">The Core Java home page</a>
在上述各种情况下, 都可以指定一个可选的label
作为链接锚(link anchor) 如果省略了 label
, 用户看到的锚的名称就是目标代码名或 URL。
第三种情况,如果@see
后面跟着"
双引号字符,text
就会显示在 ”see also“部分。例如:
@see "Core Java 2 volume 2"
可以为一个特性添加多个 @see
标记,但必须将它们放在一起。
-
可以在注释中的任何位置放置指向其他类或方法的超级链接, 以及插人一个专用的标记, 例如:
{@link package.class#feature label}
这里的特性描述规则与
@see
标记规则一样。
4.10 类设计技巧
-
一定要保证保证数据私有
-
一定要对数据初始化
-
不要在类中使用过多的基本类型
就是说,用其他的类代替多个相关的基本类型的使用。这样会使类更加易于理解且易于修改。例如, 用一个称为
Address
的新的类替换一个Customer
类中以下的实例域:private String street; private String city; private String state; private int zip;
-
不是所有的域都需要独立的域访问器和域更改器
或许,需要获得或设置雇员的薪金。而一旦构造了雇员对象,就应该禁止更改雇用日期,并且在对象中,常常包含一些不希望别人获得或设置的实例域。例如,在
Address
类中,存放州缩写的数组。 -
将职责过多的类进行分解
-
类名和方法名要能够体现它们的职责
-
优先使用不可变类