Java 提供了访问修饰符(access specifier)供类库开发者指明哪些对于客户端程序员是可用的,哪些是不可用的。访问控制权限的等级,从“最大权限”到“最小权限”依次是:public,protected,包访问权限(package access)(没有关键字)和 private。
Java 中通过 package关键字加以控制,类在相同包下还是在不同包下,会影响访问修饰符。所以在这章开始,你将会学习如何将类库组件置于同一个包下,之后你就能明白访问修饰符的全部含义。
1. 包的概念
包内包含一组类,它们被组织在一个单独的命名空间(namespace)下。
例如,标准 Java 发布中有一个工具库,它被组织在 java.util 命名空间下。java.util 中含有一个类,叫做 ArrayList。使用 ArrayList 的一种方式是用其全名 java.util.ArrayList。
// hiding/FullQualification.java
public class FullQualification {
public static void main(String[] args) {
java.util.ArrayList list = new java.util.ArrayList();
}
}
这种方式使得程序冗长乏味,因此你可以换一种方式,使用 import 关键字。如果需要导入某个类,就需要在 import 语句中声明:
// hiding/SingleImport.java
import java.util.ArrayList;
public class SingleImport {
public static void main(String[] args) {
ArrayList list = new ArrayList();
}
}
现在你就可以不加限定词,直接使用 ArrayList 了。但是对于 java.util 包下的其他类,你还是不能用。要导入其中所有的类,只需使用 * ,就像本书中其他示例那样:
import java.util.*
1.1 代码组织
当编译一个 .java 文件时,.java 文件的每个类都会有一个输出文件。每个输出的文件名和 .java 文件中每个类的类名相同,只是后缀名是 .class。因此,在编译少量的 .java 文件后,会得到大量的 .class 文件。如果你使用过编译型语言,那么你可能习惯编译后产生一个中间文件(通常称为“obj”文件),然后与使用链接器(创建可执行文件)或类库生成器(创建类库)产生的其他同类文件打包到一起的情况。这不是 Java 工作的方式。在 Java 中,可运行程序是一组 .class 文件,它们可以打包压缩成一个 Java 文档文件(JAR,使用 jar 文档生成器)。Java 解释器负责查找、加载和解释这些文件。
类库是一组类文件。每个源文件通常都含有一个 public 类和任意数量的非 public 类,因此每个文件都有一个public组件。如果把这些组件集中在一起,就需要使用关键字package。
如果你使用了package语句,它必须是文件中除了注释之外的第一行代码。当你如下这样写:
package hiding;
意味着这个编译单元是一个名为 hiding 类库的一部分。换句话说,你正在声明的编译单元中的 public 类名称位于名为 hiding 的保护伞下。任何人想要使用该名称,必须指明完整的类名或者使用 import 关键字导入 hiding 。(注意,Java 包名按惯例一律小写,即使中间的单词也需要小写,与驼峰命名不同)
例如,假设文件名是 MyClass.java ,这意味着文件中只能有一个 public 类,且类名必须是 MyClass(大小写也与文件名相同):
// hiding/mypackage/MyClass.java
package hiding.mypackage;
public class MyClass {
// ...
}
现在,如果有人想使用 MyClass 或 hiding.mypackage 中的其他 public 类,就必须使用关键字 import 来使 hiding.mypackage 中的名称可用。还有一种选择是使用完整的名称:
// hiding/QualifiedMyClass.java
public class QualifiedMyClass {
public static void main(String[] args) {
hiding.mypackage.MyClass m = new hiding.mypackage.MyClass();
}
}
关键字 import 使之更简洁:
// hiding/ImportedMyClass.java
import hiding.mypackage.*;
public class ImportedMyClass {
public static void main(String[] args) {
MyClass m = new MyClass();
}
}
package 和 import 这两个关键字将单一的全局命名空间分隔开,从而避免名称冲突。
1.2 创建独一无二的包名
按照惯例,package 名称是类的创建者的反顺序的 Internet 域名。如果你遵循惯例,因为 Internet 域名是独一无二的,所以你的 package 名称也应该是独一无二的,不会发生名称冲突。
1.3 冲突
如果通过 * 导入了两个包含相同名字类名的类库,会发生什么?例如,假设程序如下:
import com.mindviewinc.simple.*;
import java.util.*;
因为 java.util.* 也包含了 Vector 类,这就存在潜在的冲突。但是只要你不写导致冲突的代码,就不会有问题——这样很好,否则就得做很多类型检查工作来防止那些根本不会出现的冲突。
现在如果要创建一个 Vector 类,就会出现冲突:
Vector v = new Vector();
这里的 Vector 类指的是谁呢?编译器不知道,读者也不知道。所以编译器报错,强制你明确指明。对于标准的 Java 类 Vector,你可以这么写:
java.util.Vector v = new java.util.Vector();
这种写法完全指明了 Vector 类的位置(配合 CLASSPATH),那么就没有必要写 import java.util.* 语句,除非使用其他来自 java.util 中的类。
1.4 定制工具库
具备了以上知识,现在就可以创建自己的工具库来减少重复的程序代码了。
一般来说,我会使用反转后的域名来命名要创建的工具包,比如 com.mindviewinc.util ,但为了简化,这里我把工具包命名为 onjava。
比如,下面是“控制流”一章中使用到的 range() 方法,采用了 for-in 语法进行简单的遍历:
// onjava/Range.java
// Array creation methods that can be used without
// qualifiers, using static imports:
package onjava;
public class Range {
// Produce a sequence [0,n)
public static int[] range(int n) {
int[] result = new int[n];
for (int i = 0; i < n; i++) {
result[i] = i;
}
return result;
}
// Produce a sequence [start..end)
public static int[] range(int start, int end) {
int sz = end - start;
int[] result = new int[sz];
for (int i = 0; i < sz; i++) {
result[i] = start + i;
}
return result;
}
// Produce sequence [start..end) incrementing by step
public static int[] range(int start, int end, int step) {
int sz = (end - start) / step;
int[] result = new int[sz];
for (int i = 0; i < sz; i++) {
result[i] = start + (i * step);
}
return result;
}
}
从现在开始,无论何时你创建了有用的新工具,都可以把它加入到自己的类库中。在本书中,你将会看到更多的组件加入到 onjava 库。
1.5 使用 import 改变行为
Java 没有 C 的条件编译(conditional compilation)功能,该功能使你不必更改任何程序代码而能够切换开关产生不同的行为。Java 之所以去掉此功能,可能是因为 C 在绝大多数情况下使用该功能解决跨平台问题:程序代码的不同部分要根据不同的平台来编译。而 Java 自身就是跨平台设计的,这个功能就没有必要了。
但是,条件编译还有其他的用途。调试是一个很常见的用途,调试功能在开发过程中是开启的,在发布的产品中是禁用的。可以通过改变导入的 package 来实现这一目的,修改的方法是将程序中的代码从调试版改为发布版。这个技术可用于任何种类的条件代码。
1.6 使用包的忠告
当创建一个包时,包名就隐含了目录结构。这个包必须位于包名指定的目录中,该目录必须在以 CLASSPATH 开始的目录中可以查询到。 最初使用关键字 package 可能会有点不顺,因为除非遵守“包名对应目录路径”的规则,否则会收到很多意外的运行时错误信息如找不到特定的类,即使这个类就位于同一目录中。如果你收到类似信息,尝试把 package语句注释掉,如果程序能运行的话,你就知道问题出现在哪里了。
2. 访问权限修饰符
Java 访问权限修饰符 public,protected 和 private 位于定义的类名,属性名和方法名之前。每个访问权限修饰符只能控制它所修饰的对象。
如果不提供访问修饰符,就意味着"包访问权限"。
2.1 包访问权限
默认访问权限没有关键字,通常被称为包访问权限(package access)(有时也称为 friendly)。这意味着当前包中的所有其他类都可以访问那个成员。对于这个包之外的类,这个成员看上去是 private 的。由于一个编译单元(即一个文件)只能隶属于一个包,所以通过包访问权限,位于同一编译单元中的所有类彼此之间都是可访问的。
类控制着哪些代码有权访问自己的成员。其他包中的代码不能一上来就说"嗨,我是 Bob 的朋友!",然后想看到 Bob 的 protected、包访问权限和 private 成员。取得对成员的访问权的唯一方式是:
- 使成员成为 public。那么无论是谁,无论在哪,都可以访问它。
- 赋予成员默认包访问权限,不用加任何访问修饰符,然后将其他类放在相同的包内。这样,其他类就可以访问该成员。
- 在"复用"这一章你将看到,继承的类既可以访问 public 成员,也可以访问 protected 成员(但不能访问 private 成员)。只有当两个类处于同一个包内,它才可以访问包访问权限的成员。但现在不用担心继承和 protected。
- 提供访问器(accessor)和修改器(mutator)方法(有时也称为"get/set" 方法),从而读取和改变值。
2.2 public: 接口访问权限
当你使用关键字 public,就意味着紧随 public 后声明的成员对于每个人都是可用的,尤其是使用类库的客户端程序员更是如此。假设定义了一个包含下面编译单元的 dessert 包:
// hiding/dessert/Cookie.java
// Creates a library
package hiding.dessert;
public class Cookie {
public Cookie() {
System.out.println("Cookie constructor");
}
void bite() {
System.out.println("bite");
}
}
现在,使用 Cookie 创建一个程序:
// hiding/Dinner.java
// Uses the library
import hiding.dessert.*;
public class Dinner {
public static void main(String[] args) {
Cookie x = new Cookie();
// -x.bite(); // Can't access
}
}
输出:
Cookie constructor
你可以创建一个 Cookie 对象,因为它构造器和类都是 public 的。(后面会看到更多 public 的概念)但是,在 Dinner.java 中无法访问到 Cookie 对象中的 bite() 方法,因为 bite() 只提供了包访问权限,因而在 dessert 包之外无法访问,编译器禁止你使用它。
2.3 默认包
你可能惊讶地发现,以下代码尽管看上去破坏了规则,但是仍然可以编译:
// hiding/Cake.java
// Accesses a class in a separate compilation unit
class Cake {
public static void main(String[] args) {
Pie x = new Pie();
x.f();
}
}
输出:
Pie.f()
同一目录下的第二个文件:
// hiding/Pie.java
// The other class
class Pie {
void f() {
System.out.println("Pie.f()");
}
}
Cake.java 可以访问它们是因为它们在相同的目录中且没有给自己设定明确的包名。Java 把这样的文件看作是隶属于该目录的默认包中,因此它们为该目录中所有的其他文件都提供了包访问权限。
2.4 private: 你无法访问
关键字 private 意味着除了包含该成员的类,其他任何类都无法访问这个成员。同一包中的其他类无法访问private成员,因此这等于说是自己隔离自己。另一方面,让许多人合作创建一个包也是有可能的。使用private,你可以自由地修改那个被修饰的成员,无需担心会影响同一包下的其他类。
以下是一个使用 private 的例子:
// hiding/IceCream.java
// Demonstrates "private" keyword
class Sundae {
private Sundae() {}
static Sundae makeASundae() {
return new Sundae();
}
}
public class IceCream {
public static void main(String[] args) {
//- Sundae x = new Sundae();
Sundae x = Sundae.makeASundae();
}
}
以上展示了 private 的用武之地:控制如何创建对象,防止别人直接访问某个特定的构造器(或全部构造器)。例子中,你无法通过构造器创建一个 Sundae 对象,而必须调用 makeASundae() 方法创建对象。
任何可以肯定只是该类的"助手"方法,都可以声明为 private,以确保不会在包中的其他地方误用它,也防止了你会去改变或删除它。将方法声明为 private 确保了你拥有这种选择权。
2.5 protected: 继承访问权限
关键字 protected 处理的是继承的概念,通过继承可以利用一个现有的类——我们称之为基类,然后添加新成员到现有类中而不必碰现有类。我们还可以改变类的现有成员的行为。为了从一个类中继承,需要声明新类 extends 一个现有类,像这样:
class Foo extends Bar {}
如果你创建了一个新包,并从另一个包继承类,那么唯一能访问的就是被继承类的 public 成员。(如果在同一个包中继承,就可以操作所有的包访问权限的成员。)有时,基类的创建者会希望某个特定成员能被继承类访问,但不能被其他类访问。这时就需要使用 protected。protected 也提供包访问权限,也就是说,相同包内的其他类可以访问 protected 元素。
回顾下先前的文件 Cookie.java,下面的类不能调用包访问权限的方法 bite():
// hiding/ChocolateChip.java
// Can't use package-access member from another package
import hiding.dessert.*;
public class ChocolateChip extends Cookie {
public ChocolateChip() {
System.out.println("ChocolateChip constructor");
}
public void chomp() {
//- bite(); // Can't access bite
}
public static void main(String[] args) {
ChocolateChip x = new ChocolateChip();
x.chomp();
}
}
输出:
Cookie constructor
ChocolateChip constructor
如果类 Cookie 中存在一个方法 bite(),那么它的任何子类中都存在 bite() 方法。但是因为 bite()具有包访问权限并且位于另一个包中,所以我们在这个包中无法使用它。你可以把它声明为public,但这样一来每个人都能访问它,这可能也不是你想要的。如果你将 Cookie 改成如下这样:
// hiding/cookie2/Cookie.java
package hiding.cookie2;
public class Cookie {
public Cookie() {
System.out.println("Cookie constructor");
}
protected void bite() {
System.out.println("bite");
}
}
这样,bite() 对于所有继承 Cookie 的类,都是可访问的:
// hiding/ChocolateChip2.java
import hiding.cookie2.*;
public class ChocolateChip2 extends Cookie {
public ChocoalteChip2() {
System.out.println("ChocolateChip2 constructor");
}
public void chomp() {
bite(); // Protected method
}
public static void main(String[] args) {
ChocolateChip2 x = new ChocolateChip2();
x.chomp();
}
}
输出:
Cookie constructor
ChocolateChip2 constructor
bite
2.6 包访问权限 Vs Public 构造器
当你定义一个具有包访问权限的类时,你可以在类中定义一个 public 构造器,编译器不会报错:
// hiding/packageaccess/PublicConstructor.java
package hiding.packageaccess;
class PublicConstructor {
public PublicConstructor() {}
}
有一个 Checkstyle 工具,你可以运行命令 gradlew hiding:checkstyleMain 使用它,它会指出这种写法是虚假的,而且从技术上来说是错误的。实际上你不能从包外访问到这个 public 构造器:
// hiding/CreatePackageAccessObject.java
// {WillNotCompile}
import hiding.packageaccess.*;
public class CreatePackageAcessObject {
public static void main(String[] args) {
new PublicConstructor();
}
}
如果你编译下这个类,会得到编译错误信息:
CreatePackageAccessObject.java:6:error:
PublicConstructor is not public in hiding.packageaccess;
cannot be accessed from outside package
new PublicConstructor();
^
1 error
因此,在一个具有包访问权限的类中定义一个public的构造器并不能真的使这个构造器成为 public,在声明的时候就应该标记为编译时错误。
3. 接口和实现
访问控制通常被称为隐藏实现(implementation hiding)。将数据和方法包装进类中并把具体实现隐藏被称作是封装(encapsulation)。其结果就是一个同时带有特征和行为的数据类型。
出于两个重要的原因,访问控制在数据类型内部划定了边界:
- 第一个原因是确立客户端程序员可以使用和不能使用的边界。
- 将接口与实现分离。
为了清晰起见,你可以采用一种创建类的风格:public 成员放在类的开头,接着是 protected 成员,包访问权限成员,最后是 private 成员。这么做的好处是类的使用者可以从头读起,首先会看到对他们而言最重要的部分(public 成员,因为可以从文件外访问它们),直到遇到非 public 成员时停止阅读。
4. 类访问权限
访问权限修饰符也可以用于确定类库中的哪些类对于类库的使用者是可用的。如果希望某个类可以被客户端程序员使用,就把关键字 public 作用于整个类的定义。这甚至控制着客户端程序员能否创建类的对象。
为了控制一个类的访问权限,修饰符必须出现在关键字 class 之前:
public class Widget {
如果你的类库名是 hiding,那么任何客户端程序员都可以通过如下声明访问 Widget:
import hiding.Widget;
或者:
import hiding.*;
这里有一些额外的限制:
- 每个编译单元(即每个文件)中只能有一个 public 类。
- public 类的名称必须与含有该编译单元的文件名相同,包括大小写。
- 虽然不是很常见,但是编译单元内没有 public 类也是可能的。
注意,类既不能是 private 的(这样除了该类自身,任何类都不能访问它),也不能是 protected 的。所以对于类的访问权限只有两种选择:包访问权限或者 public。为了防止类被外界访问,可以将所有的构造器声明为 private,这样只有你自己能创建对象(在类的 static 成员中):
// hiding/Lunch.java
// Demonstrates class access specifiers. Make a class
// effectively private with private constructors:
class Soup1 {
private Soup1() {}
public static Soup1 makeSoup() { // [1]
return new Soup1();
}
}
class Soup2 {
private Soup2() {}
private static Soup2 ps1 = new Soup2(); // [2]
public static Soup2 access() {
return ps1;
}
public void f() {}
}
// Only one public class allowed per file:
public class Lunch {
void testPrivate() {
// Can't do this! Private constructor:
//- Soup1 soup = new Soup1();
}
void testStatic() {
Soup1 soup = Soup1.makeSoup();
}
void testSingleton() {
Soup2.access().f();
}
}
Soup1 和 Soup2 展示了如何通过将你所有的构造器声明为 private 的方式防止直接创建某个类的对象。记住,如果你不显式地创建构造器,编译器会自动为你创建一个无参构造器(没有参数的构造器)。如果我们编写了无参构造器,那么编译器就不会自动创建构造器了。将构造器声明为 private,那么谁也无法创建该类的对象了。
Soup2 用到了所谓的设计模式(design pattern)。这种模式叫做单例模式(singleton),因为它只允许创建类的一个对象。Soup2 类的对象是作为 Soup2 的 static private 成员而创建的,所以有且只有一个,你只能通过 public 修饰的 access() 方法访问到这个对象。
网友评论