第六章 访问权限控制(上)
访问控制与最初的实现并不恰当有关。
类库的开发者必须要有权限对代码进行修改和改进,并确保客户代码不会因为这些改动而受到影响。这一目标可以通过约定来达到,但如果程序开发者想要一处旧的实现而要添加新的实现时,改动任何一个成员都有可能破坏客户端程序员的代码,于是类库开发者会被辅助手脚,无法对任何事物进行改动。
为解决这一问题,Java提供了访问权限修饰词,以供类库开发人员向客户端程序员指明哪些是可用的,哪些是不可用的。访问权限控制的等级,从最大到最小权限依次为:public、protected、包访问权限(没有关键词)、private。
不过,构件类库的概念以及对于谁有权取用该类库构件的控制问题都还是不完善的,其中依旧存在着如何将构件捆绑到一个内聚的类库的类库单元中的问题。对于这一点,Java用关键字package加以控制,而访问权限修饰词会因类是存在于一个相同的包,还是存在于一个单独的包而受到影响。
6.1 包:库单元
包内包含有一组类,它们在单一的名字空间下被组织在了一起。
例如,在Java的标准发布中有一个工具库,它被组织在java.util名字空间之下。java.util中有一个叫做ArrayList的类,使用ArrayList的一种方式是用其全名java.util.ArrayList来指定。
public class FullQualification { public static void main(String[] args) { java.util.ArrayList list = new java.util.ArrayList(); } }
这立刻就让程序变得很冗长,因此我们使用import关键字:
import java.util.ArrayList; public class SingleImport { public static void main(String[] args) { ArrayList list = new ArrayList(); } }
现在就可以不用限定地使用ArrayList了,要想导入其中所有类,只需要使用“*”:
import java.util.*;
我们之所以要导入,就是要提供一个管理名字的机制,所有类成员的名称都是彼此隔离的,A类中的方法f()与B类中具有相同参数列表的方法f()不会彼此冲突。但是类名冲突了怎么办?在Java中对名称空间进行完全控制并为每个类创建唯一的标识符组合就成为了非常重要的事情。
当编写一个Java源代码文件时,此文件通常被称为编译单元(转译单元),每个编译单元都必须有一个后缀名.java,而在编译单元内则可以有一个public类,该类的名称必须与文件的名称相同(包括大小写)。每个编译器单元只能有一个public类,否则编译器就不会接受。
6.1.1 代码组织
当编译一个.java文件时,在.java文件中的每个类都会有一个输出文件,而该输出文件的名称与.java文件中每个类的名称相同,只是多了一个后缀名.class,因此在编译少量.java文件之后会得到大量的.class文件。Java可运行程序时一组可以打包并压缩为一个Java文档文件(JAR)的.class文件,Java解释器负责这些文件的查找、装在和解释。
类库实际上是一组类文件,每个文件都有一个public类,以及任意数量的非public类,因此每个文件都有一个构件,如果希望这些构件从属于一个群组,就可以使用关键字package。
使用package语句,它必须是文件中除注释意外的第一句代码,在文件起始处写:
package access;
就表明你在声明该编译单元是名为access类库的一部分。假设文件名称是MyClass.java,就意味着在该文件中有且只有一个public类,该类的名称必须是MyClass:
package access.mypackage; public class MyClass{ //... }
身为一名类库设计员要牢记,package和import关键字允许你做的僵尸单一的全局名字空间分隔开,使得无论多少人使用Internet以及Java开始编写类,都不会出现名称冲突的问题。
6.1.2 创建独一无二的包名
既然一个包从未真正将被打包的佛哪个系包装成单一的文件,并且一个包可以由许多的.class文件构成,那么情况就有点复杂。为避免这种情况的发生,一种合乎逻辑的方法就是将特定包的所有.class文件都放在一个目录下,也就是用操作系统的层次化文件结构来解决这个问题。
将所有文件收入一个子目录还可以解决另外两个问题:怎样创建独一无二的名称?怎样查找有可能隐藏于目录结构中某处的类。这些任务是通过.class文件所在的路径位置编码成package的名称来实现的。
Java解释器的运行过程如下:
首先,找出环境变量CLASSPATH,CLASSPATH可以通过操作系统来设置,包含一个或多个目录用作查找.class文件的根目录。从根目录开始,解释器获取包的名称并将每个句点替换成反斜杠,从CLASSPATH根中产生一个路径名称,得到的路径会与CLASSPATH中的各个不同的项相连接,解释器就在这些目录中查找与你所想要创建的类名称相关的.class文件。
冲突:
如果将两个含有相同名称的类库以”*”的形式同时导入:
import net.mindview.simple.*;
import java.util.*;
由于两个类都含有Vector类,这样就存在潜在的冲突。如果现在要创建一个Vector类的话就会产生冲突:
Vector v = new Vector();
这到底是取用那个Vector类呢?编译器不知道,于是编译器就会提出错误信息,强制你明确指明:
java.util.Vector v = new java.util.Vector();
由于这样可以完全指明该Vector类的位置,所以除非还要使用import java.util中的其他东西,否则没有必要写import java.util.*语句了。
6.1.3 定制工具库
现在我们可以创建自己的工具库,例如我们已经用到的System.out.println()的别名可以减少输出负担,这种机制可以用于名为Print的类中,这样我们在使用该类的时候可以用一个更具可读性的静态import语句来导入:
package com.example.demo; import java.io.*; public class Print { public static void print(Object obj) { System.out.println(obj); } public static void print() { System.out.println(); } public static void printnb(Object obj) { System.out.print(obj); } public static PrintStream print(String format, Object... args) { return System.out.printf(format, args); } }
现在,你可以创建有空的工具,并将其添加到自己的类库中。
6.1.4 用import改变行为
Java没有C的条件编译功能,该功能可以使你不必更改任何程序代码,就能切换开关并产生不同的行为。然而条件编译还有其他一些有价值的用途,例如说调试。调试功能在开发过程中是开启的,而在发布的产品中是禁用的。
6.1.5 对使用包的忠告
无论何时创建包,都已经在给定包名的时候隐含地指定了目录结构,这个包必须位于其名称所指定的目录之中,而该目录必须是在以CLASSPATH开始的目录中可以查询到的。