通常总是会有一些这样的情况:你想改变代码,而另一些人或者说客户,他们却想让代码保持不变。由此而产生了在面向对象设计中需要考虑的一个基本问题:“如何把变动的事物与保持不变的事物区分开来”。
这对类库(library)而言尤为重要。该类库的消费者必须依赖他所使用的那部分类库,并且能够知道如果类库出现了新版本,他们并不需要改写代码。从另一个方面来说,类库的开发者必须有权限进行修改和改进,并确保客户代码不会因为这些改动而受到影响。
为了解决这一问题,Java提供了访问权限修饰词,以供类库开发人员向客户端程序员指明哪些是可用的,哪些是不可用的。访问权限控制的等级,从最大权限到最小权限依次为: public、protected、包访问权限(没有关键词)和private。根据前述内容,我们尽可能将一切方法都定为private,而仅向客户端程序员公开你愿意让他们使用的方法。这样做是完全正确的。
不过,构件类库的概念以及对于谁有权取用该类库构件的控制问题都还是不完善的。其中仍旧存在着如何将构件捆绑到一个内聚的类库单元中的问题。对于这一点,Java用关键字package加以控制,而访问权限修饰词会因类是存在于一个相同的包,还是存在于一个单独的包而受到影响。为此,要先知道如何将类库构件置于包中,然后才会理解访访问权限修饰词的全部含义。
包:库单元
包内包含有一组类,它们在单一的名字空间之下被组织在了一起。如果你想使用某个类,可以用import关键字将该类导入,例如:
import java.util.*;
我们之所以要导入,就是要提供一个管理名字空间的机制。所有类成员的名称都是彼此隔离的。A类中的方法f()与B类中具有相同特征标记(参数列表)的方法f()不会彼此冲突。但是如果类名称相互冲突又该怎么办呢?假设你编写了一个Stack类并安装到了一台机器上,而该机器上已经有了一个别人编写的Stack类,我们该如何解決呢?由于名字之间的潜在冲突,在Java中对名称空间进行完全控制并为每个类创建唯一的标识符组合就成为了非常重要的事情。
当编写一个Java源代码文件时,此文件通常被称为编译单元(有时也被称为转译单元)。每个编译单元都必须有一个后缀名.java,而在编译单元内则可以有一个public类,该类的名称必须与文件的名称相同(包括大小写,但不包括文件的后缓名.java)。每个编译单元只能有一个public类,否则编译器就不会接受。如果在该编译单元之中还有额外的类的话,那么在包之外的世界是无法看见这些类的,这是因为它们不是public类。而且它们主要用来为主public类提供支持。
当编译一个java文件时,在.java文件中的每个类都会有一个输出文件,而该输出文件的名称与.java文件中每个类的名称相同,只是多了一个后缀名.class。因此,在编译少量.java文件之后,会得到大量的.class文件。
类库实际上是一组类文件。其中每个文件都有一个public类,以及任意数量的非public类。 因此每个文件都有一个构件。如果希望这些构件(每一个都有它们自已的独立的.java和.class文件)从属于同一个群组,就可以使用关键字package。它必须是文件中除注释以外的第一句程序代码。在文件起始处写:
package access;
就表示你在声明该编译单元是名为access的类库的一部分。
将所有的文件收入一个子目录还可以解决另外两个问题:怎样创建独一无二的名称以及怎样査找有可能隐藏于目录结构中某处的类。这些任务是通过将.class文件所在的路径位置编码成package的名称来实现的。按照惯例,package名称的第一部分是类的创建者的反顺序的Internet域名。此技巧的第二部分是把package名称分解为你机器上的一个目录。所以当Java程序通行并且需要加载.class文件的时候, 它就可以确定.class文件在目录上所处的位置。
Java解释器的运行过程如下:首先,找出坏境变量CLASSPATH(可以通过操作系统来设置, 有时也可通过安装程序——用来在你的机器上空装Java或基于Java的工具来设置) 。 CLASSPATH包含一个或多个目录,用作査找.class文件的根目录。从根目录开始,解释器获取包的名称并将每个句点替换成反斜杠。以从CLASSPATH根中产生一个路径名称(于是,package foo.bar.baz就变成为 foo\bar\baz或foo/bar/baz或其他,这一切取决于操作系统)。得到的路径会与CLASSPATH中的各个不同的项相连接。解解器就在这些目录中査找与你所要创建的类名称相关的.class文件。(解释器还会去査找某些涉及Java解解器所在位置的标准目录。)
对使用包的忠告
务必记住,无论何时创建包,都已经在给定包的名称的时候隐含地指定了日录结构。这个包必须位于其名称所指定的目录之中,而该目录必须是在以CLASSPATH开始的目录中可以査询到的。最初使用关键字package,可能会有一点不顺,因为除非遵守“包的名称对应目录路径”的规则,否则将会收到许多出乎意料的运行时信息,告知无法找到特定的类,哪怕是这个类就位于同一个目录之中。如果你收到类似信息,就用注释掉package语句的方法试一下,如果这样程序就能运行的话,你就可以知道问题出在哪里了 。
注意,编译过的代码通常放置在与源代码的不同目录中,但是必须保正JVM使用CLASSPATH 可以找到该路径。
java访问修饰词
public, protected和private这几个Java访问权限修饰词在使用时,是置于类中每个成员的定义之前的——无论它是一个域还是一个方法。每个访问权限修饰词仅控制它所修饰的特定定义的访问权。
类控制着哪些代码有权访问自己的成员。其他包内的类不能刚一上来就说:“嗨,我是Bob的朋友。”并且还想看到Bob的protected、包访问权限和private成员。取得对某成员的访问权的唯一途径是:
- 使该成员成为public。于是,无论是谁,无论在哪里,都可以访问该成员。
- 通过不加访同权限修饰词并将其他类放置于同一个包内的方式给成员赋予包访问权。于是包内的其他类也就可以访问该成员了 。
- 继承而来的类既可以访问public成员也可以访问protected成员(但访问private成员却不行)。只有在两个类都处于同一个包内时,它才可以访问包访问权限的成员。
- 提供访问器(accessor)和变异器(mutator)方法(也称作get/set方法),以读取和改变数值。对OOP而言,这是最优雅的方式,而且这也是JavaBeans的基本原理。
public:接口访问权限
使用关键字public,就意味着public之后紧跟着的成员声明自己对每个人都是可用的。例如:
package access.dessert;
public class Cookie{
public Cookie(){
System.out.print("Cookie constructor");
}
}
package access2.dessert2;
import access.dessert.*;
public class Dinner{
public static void main(String[] args){
Cookie c = new Cookie();
}
//输出:Cookie constructor
}
默认包
使用默认包(没有任何访问修饰关键字),就意味着改成员声明自己对在同一个包中的每个人都是可用的。使用类的客户端程序员是无法访问包访问权限成员的。这样做很好,因为默认访问权限是一种我们常用的权限,同时也是一种在忘记添加任何访问权限控制时能够自动得到的权限。例:
class Cookie{
public Cookie(){
System.out.print("Cookie constructor");
}
}
在第二个处于相同目录的文件中
class Pic{
public static void main(String[] args){
Cookie c = new Cookie();
}
//输出:Cookie constructor
}
private:你无法访问
关健字private的意思是,除了包含该成员的类之外,其他任何类都无法访问这个成员。由于处于同一个包内的其他类是不可以访问private成员的,因此这等于说是自己隔离了自己。从另一方面说,让许多人共同合作来创建一个包也是不大可能的,为此private就允许你随意改变该成员,而不必考虑这样做是否会影响到包内其他的类。 你可能不会认为自己经常会需要使用关键字private,因为没有它,照样可以工作。然而,事实很快就会证明,对private的使用是多么的重要,在多线程坏境下更是如此。
class Sundae{
private Sundae(){}
static Sundae makeASundae(){
return new Sundae();
}
}
public class IceCream{
public static void main(String[] args){
Sundae s = Sundae.makeAsundae();
}
}
这是一个说明private终有其用武之地的示例:可能想控制如何创建对象,并阻止别人直接访问某个特定的构造器(或全部构造器)。在上面的例子中,不能通过构造器来创建Sundae对象,而必须调用makeAsundae()方法来达到此目的。
任何可以肯定只是该类的一个“助手”方法的方法,都可以把它指定为private,以确保不会在包内的其他地方误用到它,于是也就防止了你会去改变或删除这个方法。将方法指定为private确保了你拥有这种选择权。
这对于类中的private域同样适用。除非必须公开底层实现细目(此种境况很少见),否则就应该将所有的域指定为private。然而,不能因为在类中某个对象的引用是private,就认为其他的对象无法拥有该对象的public引用。
protected:继承访问权限
关键字 protected处理的是继承的概念,如果创建了一个新包,并自另一个包中继承类,那么唯一可以访问的成员就是源包的public 成员。(当然,如果在同一个包内执行继承工作,就可以操纵所有的拥有包访问权限的成员。)有时,基类的创建者会希望有某个特定成员,把对它的访问权限赋予派生类而不是所有类。这就需要protected来完成这一工作。protected也提供包访问权限,也就是说,相同包内的其他类可以访问protected元素。 例:
package access.cookie;
public class Cookie{
protected void bite(){
System.out.print("Cookie.bite()");
}
}
import access.cookie.*;
public class CookieChilden extends Cookie{
public void chomp(){
System.out.print("CookieChilden.chomp()");
bite();
}
public static void main(String[] args){
}
}
接口和实现
访问权限的控制常被称为是具体实观的隐藏。把数据和方法包装进类中,以及具体实现的隐藏,常共同被称作是封装。其结果是一个同时带有特征和行为的数据类型。
出于两个很重要的原因,防问权限控制将权限的边界划在了数据类型的内部。第一个原因是要设定客户端程序员可以使用和不可以使用的界限。可以在结构中建立自己的内部机制,而不必担心客户端程序员会偶然地将内部机制当作是他们可以使用的接口的一部分。这个原因直接引出了第二个原因,即将接口和具体实现进行分离。如果结构是用于一组程序之中,而客户端程序员除了可以向接口发送信息之外什么也不可以做的话,那么就可以随意更改所有不是public的东西(例如有包访问权限、protected和 private的成员),而不会破坏客户端代码。
类的访问权限
在Java中,访问权限修饰符也可以用于确定库中的哪些类对于该库的使用者是可用的。如果希望某个类可以为某个客户端程序员所用,就可以通过把关键字public作用于整个类的定义来达到目的。这样做甚至可以控制客户端程序员是否能创造一个该类的对象。为了控制某个类的访问权限,修饰词必须出现于关键字class之前。因此可以像下面这样声明:
public class Widget{}
然而,这里还有一些额外的限制:
-
每个编译单元(文件)都只能有一个public类。这表示,每个编译单元都有单一的公共接口,用public类来表现。该接口可以按要求包含众多的支持包访问权限的类。如果在某个编译单元内有一个以上的public类,编译器就会给出出错信息。
-
public类的名称必须完全与含有该编译单元的文件名相匹配,包括大小写。如果不匹配,,同样将得到编译时错误。
-
虽然不是很常用,但编译单元内完全不带public类也是可能的。在这种情况下,可以随意对文件命名。(尽管随意命名会使得人们在阅读和维护代码时产生混淆。)
请注意,类既不可以是private的(这样会使得除该类之外,其他任何类都不可以访问它),也不可以是protected的。所以对于类的访问权限,仅有两个选择:包访问权限或public。如果不希望其他任何人对该类拥有访问权限,可以把所有的构造器都指定为private,从而阻止任何人创建该类的对象,但是有一个例外,就是你在该类的static成员内部可以创建。下面是一个示例:
class Sundae{
private Sundae(){}
static Sundae makeASundae(){
return new Sundae();
}
}
public class IceCream{
public static void main(String[] args){
Sundae s = Sundae.makeAsundae();
}
}
- 本文来源《Java编程思想(第四版)》