thinking in Java 之访问权限控制

时间:2022-09-26 15:29:53

如何将发生变化的东西与保持不变的东西分隔开—这一点对于库开说特别重要,库的创建者必须能*的修改和改进代码,同时客户程序员不受到那些变动的影响。例如,库程序员在修改库内的一个类是,必须保证不删除已有的方法,因为那样客户程序员代码会出现断点。然而,对于数据成员,库的创建者怎样才知道哪些数据成员已受到客户程序员的访问呢?若方法属于某个类唯一的一部分,而且不一定由客户程序员直接使用,那么这种痛苦的情况是真实的。如果库的创建者想删除一种旧的方案,并置入新的代码,此时该肿么办呢?对那些成员进行的任何改动都可能中断客户程序员的代码。
为了解决这个问题,Java推出了“访问指示符”的概念,声明哪些东西是客户程序员可以使用的,哪些是不可以使用的。访问指示符有public、”友好的“(无关键字)、protected和private。

1.包:库单元
当用import关键字导入一个完整的库时,就会获得”包“(package)。例如:import java.util.*;它的作用是导入完整的Utility库。通过导入特定的包,类成员的名字相互都隔离起来,这样位于A内的一个方法f()不会与位于B内的拥有相同自变量列表的f()发生冲突。但是类名会不会冲突呢?假设创建一个stack类,将它安装到已有一个stack类(别人编写)的机器上,类在运行的适合会自动下载。由于名字存在潜在冲突,所以必要对Java中命名空间进行完整控制,而且需要创建一个独一无二的名字。
为Java创建一个源码文件的时候,它通常叫做一个”编辑单元“。每个编辑单元都必须有一个以.java结尾的名字。而且在编辑单元的内部,可以有一个public类,它必须与文件相同名字(包括大小写形式,但排除.java文件扩展名),否则编译器报错。并且每个编译单元内都只能有一个public类,否则编译器会报错。在编译单元内如果有其他类,这些类不能够被编译单元之外的内容访问,因为它们非public。
编译一个.java文件时,我们会获得一个名字完全相同的输出文件,但对于.java中每一个类,它们都有一个.class扩展名。一个有效的程序就是一系列.class文件,它们可以封装和压缩到一个JAR文件里。
”库“也由一系列文件构成。每个文件都有一个public类,所以每个文件都有一个组件。如果想将这些组件(它们在各自独立的.java和.class文件里)都归纳到一起,那么package关键字就可以发挥作用。
如果在一个文件的开头使用package mypackage;那么package语句必须作为文件的第一个非注释语句出现,该语句指出这个编译单元属于名为mypackage的库的一部分。换句话说,它表明这个编译单元内的public类名位于mypackage这个名字下面。如果其他人想使用这个名字,要么之处完整的名字,要么与mypackage联合使用import关键字。注意包名必须小写。
例如,假定文件名是MyClass.java。它意味着在那个文件有一个、而且只能有一个public类。而且那个类的名字必须是MyClass(包括大小写形式):

package mypackage;
public class MyClass {
// …

现在,如果有人想使用MyClass,或者想使用mypackage内的其他任何public类,他们必须用import关键字激活mypackage内的名字,使它们能够使用。另一个办法则是指定完整的名称:

mypackage.MyClass m = new mypackage.MyClass();

import关键字则可将其变得简洁得多:

import mypackage.*;
// …
MyClass m = new MyClass();

作为一名库设计者,一定要记住package和import关键字允许我们做的事情就是分割单个全局命名空间,保证我们不会遇到名字的冲突——无论有多少人使用因特网,也无论多少人用Java编写自己的类。

1.1 创建独一无二的包名
由于一个包永远不会真的”封装“到单独一个文件里面,它可由多个.class文件构成,我们可以利用操作系统的分级文件避免文件结构混乱。同时也解决了另两个问题:创建独一无二的包名以及找出那些深藏于目录结构某处的类。
Java解析器的工作程序如下:首先找到环境变量CLASSPATH,CLASSPATH包含一个或多个目录,它们作为特殊的“根”使用,从这里展开对.class文件的搜索。从那个根开始,解析器会寻找包名,并将每个点号替换成一个斜杠,从而生成CLASSPATH根开始的一个路径名。以后搜索.class文件时,就可从这些地方开始查找与准备创建的类名对应的名字。
下面举个栗子,bruceeckel.com。将其反转过来后,com.bruceeckel就为我的类创建了独一无二的全局名称(com,edu,org,net)。由于决定创建一个名为util的库,我可以进一步地分割它,所以最后得到的包名如下:

package com.bruceeckel.util;
package com.bruceeckel.util;
public class Vector {
pubilc Vector() { System.out.println("com.bruceeckel.util.Vector");
}
}
package com.bruceeckel.util;
public class List {
pubilc List() {
System.out.println("com.bruceeckel.util.List");
}
}

这两个文件置于我自己系统的一个子目录中:C:\DOC\JavaT\com\bruceeckel\util。可以看出我的包名为com.bruceeckel.util,但路径的第一部分是什么呢?这是由CLASSPATH环境变量决定的。在我的机器上,它是CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT 可以看出,CLASSPATH包含大量备用的搜索路径。然而,使用JAR文件时需要注意,必须将JAR文件的名字置于类路径里,而不仅仅是它所在的路径。所以对一个名为grape.jar的JAR文件来说,我们的类路径需要包括:CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar
针对上面代码,编译器遇到import语句后,它会搜索由CLASSPATH指定的目录,查找子目录com\bruceckel\util,然后查找名称适当的已编译文件(对于Vector是Vector.class,对于List则是List.class)。
1.自动编译
为导入的类首次创建一个对象或访问一个类的static成员时,编译器会在适当地目录里寻找同名的.class文件(如果创建类X的一个对象,就应该是X.class)。若只发现X.class,它就是必须使用的那一个类。如果在相同的目录中还发现了一个X.java,编译器会比较两个文件的日期标记。如果X.java比X.class新,就会自动编译X.java,生成一个最新的X.class。对于一个特定的类,或在与它同名的.java文件中没有找到它,就会对那个类采取上述的处理。
2.冲突
若通过*导入了两个库,而且它们包括相同的名字,这时会出现什么情况呢?例如
import com.bruceeckel.util.*;
import java.util.*;
由于java.util.也包含了一个Vector类,所以这会有潜在冲突。然而,只要冲突并不真的发生,那么就不会产生任何问题。如果现在试着生成一个Vector,就肯定发生冲突。如Vector Vector = new Vector();它的引用到底是指向哪个Vector呢?编译器不知道,所以它会报告一个错误,强迫我们进行明确说明。例如我们想使用标准的Java Vector,那么就必须这样编程:java.util.Vector v = new java.util.Vector();由于它(与CLASSPATH一起)完整指定了那个Vector的位置,所以不再需要import java.util.语句,除非还想使用来自java.util的其他东西。

3.CLASSPATH的陷阱
假如我们新建了一个P.java,编写程序的时候我引入了P.java,它最初看起来似乎工作正常,但是某些情况下却开始中断,在很长时间我都觉得是Java或其他什么在实现时的一个错误。但最后发现在一个地方引入了一个程序,使用了一个不同的类P。由于它作为一个工具使用,所以有时候会进入类路径里;另一些时候则不会这样。但只要它进入类路径,那么假若执行的程序需要寻找com.bruceeckel.tools中的类,Java首先发现的就是CodePackager.java中的P。此时,编译器会报告一个特定的方法没有找到。乍一看来,这似乎是编译器的一个错误,但假若考察import语句,就会发现它只是说:“在这里可能发现了P”。然而,我们假定的是编译器搜索自己类路径的任何地方,所以一旦它发现一个P,就会使用它;若在搜索过程中发现了“错误的”一个,它就会停止搜索。这与我们在前面表述的稍微有些区别,因为存在一些讨厌的类,它们都位于包内。而这里有一个不在包内的P,但仍可在常规的类路径搜索过程中找到。
如果您遇到象这样的情况,请务必保证对于类路径的每个地方,每个名字都仅存在一个类。

2.2 Java访问指示符
针对类内每个成员的每个定义,Java访问指示符public、protected、private都可置于它们的最前面–无论它们是一个数据成员还是一个方法。
2.2.1 “友好的”
如果不指定访问指示符,通常被称为”友好“(Friendly)访问。意味着当前包内的其他所有类都能访问”友好的“成员,但对包外的所有类来说,这些成员却是”私有“(Private)的,外界不得访问。由于一个编译单元只能从属于单个包,所以单个编译单元内的所有类相互间都市自动”友好“的。因此我们也说友好元素拥有”包访问“权限。
友好访问允许我们将相关的类都组合到一个包里,使它们互相间方便地进行沟通。将类组合到一个包内以后,我们便”拥有“了那个包内的代码。

2.2.2 public
使用public关键字,意味着紧随在public后面的成员声明适用于所有人,特别是适用于使用库的客户程序员。

2.2.3 private
private关键字意味着只有从那个类里,其他没有人能访问这个成员。

2.3.4 protected
protected关键字为我们引入了”继承“的概念,它以现有的类为基础,其他类”扩展“(extends)现有类,同时不会对现有的类产生影响,现有的类被称为”基本类“(Base Class)。
若新建一个包,并从另一个包内的某个类里继承,则唯一能够访问的成员就是那个原来那个包的public成员。如果在相同的包里进行继承,那么继承获得的包能够访问所有”友好“的成员。有时候,基础类的创建者喜欢提供一个特殊的成员,并允许访问衍生类。这正是protected的工作。
对于继承,值得注意的一件事情,若方法foo()存在于类Cookie中,那么也会存在于从Cookie继承的所有类中。

2.3 接口与实现
访问权限的控制常被称为具体实现的隐藏。把数据和方法包装进类中,以及具体实现的隐藏,常被称为封装。其结果是一个同时带有特征和行为的数据类型。
处于两个重要原因,访问权限控制将权限的边界划在了数据类型的内部。第一个原因是要设定客户端程序员可以使用和不可以使用的界限。可以在结构中建立自己的内部机制,而不必担心客户端程序员会偶然的将内部机制当作是他们可以使用的接口的一部分。这个原因直接引出了第二个原因,即将接口和具体实现进行分离。如果结构是用于一组程序之中,而客户端程序员出了可以向接口发送信息之外什么也不可以做的话,那么就可以随意更改所有不是public的东西(例如包访问权限、protected和private成员),而不会破坏客户端代码。
为了清楚起见,可能会采用一种将public成员置于开头,后面跟着protected、包访问权限和private成员的创建类的形式。这样做的好处是类的使用者可以从头读起,首先阅读对他们而言最为重要的部分(即public成员,因为可以从文件外部调用),等到遇见作为内部实现细节的非public成员时停止阅读。
这样仅可以使程序阅读起来稍微容易一些,因为接口和具体实现仍旧混在一起。也就是说仍能看到源码代码–实现部分,因为它就在类中。另外,javadoc所提供的注释文档功能降低了程序代码的可读性对客户端程序员的重要性。

2.4 类的访问权限
在Java中,访问权限修饰词也可以用于确定库中的哪些类对于该库中使用者是可用的。如果希望某个类可以为某个客户端程序员所用,就可以通过把关键字public作用于整个类的定义来达到目的。这样做甚至可以控制客户端程序员是否能创建一个该类的对象。
然而,这里还有一些额外的限制:
1.每个编译单元都只能有一个public类。这表示,每个编译单元都有单一的公共接口,用public类来表示。该接口可以按要求包含众多的支持包访问权限的类。如果在某个编译单元内有一个以上的public类,编译器就会给出出错信息。
2.public类的名称必须完全与含有该编译单元的文件名相匹配,包括大小写。
3.虽然不是很常用,但编译单元内完全不带public类也是有可能的。在这种情况下,可以随意对文件命名,但不建议。