第六章 访问权限控制
包:库单元
我们之所以要导入包名,就是要提供一个管理名字空间的机制。所有类成员的名称都是彼此隔离开的。A类中的方法f()
与B类中具有相同特征标记(参数列表)的方法f()
不会彼此冲突。
当编写一个Java源代码文件时,此文件通常被称为编译单元(有时也被称为转译单元)。每个编译单元都必须有一个后缀名 . java
,而在编译单元内则可以有一个 public
类,该类的名称必须与文件的名称相同(包括大小写,但不包括文件的后缀名.java
)。每个编译单元只能有一个public
类,否则编译器就不会接受。如果在该编译单元之中还有额外的类的话,那么在包之外的世界是无法看见这些类的,这是因为它们不是 public
类,而且它们主要用来为主 public
类提供支持。
代码组织
当编译一个.java
文件时,在.java
文件中的毎个类都会有一个输出文件,而该输出文件的名称与.java
文件中毎个类的名称相同,只是多了一个后缀名 .class
因此,在编译少量.java
文件之后,会得到大量的.class
文件。如果用编译型语言编写过程序,那么对于编译器产生一个中间文件(通常是一个obj
文件),然后再与通过链接器(用以创建一个可执行文件)或类库产生器( librarian,用以创建一个类库)产生的其他同类文件捆绑在一起的情况,可能早已司空见惯。但这并不是Java的工作方式。Java可运行程序是一组可以打包并压缩为ー个Java文档文件(JAR,使用Java的jar文档生成器)的.class
文件。Java解释器负责这些文件的查找、装载和解释。
Java解释器的运行过程如下:
首先,找出环境变量 CLASSPATH
(可以通过操作系统来设置,有时也可通过安装程序一用来在你的机器上安装Java或基于Java的工具一来设置)。CLASSPATH
包含一个或多个目录,用作査找.class
文件的根目录。
从根目录开始,解释器获取包的名称并将每个句点替换成反斜杠,以从 CLASSPATH
根中产生一个路径名称(于是, packagefoo.bar.baz
就变成为 foo\bar \baz
或 foo/bar/baz
或其他,这一切取决于操作系统)。得到的路径会与 CLASSPATH
中的各个不同的项相连接,解释器就在这些目录中査找与你所要创建的类名称相关的.class
文件。(解释器还会去査找某些涉及Java解释器所在位置的标准目录。)
练习1:(1)在某个包中创建一个类,在这个类所处的包的外部创建该类的一个实例。
包类
package access.mypackage;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/3 21:03
*/
public class MyPackagedClass {
public MyPackagedClass(){
System.out.println("This is MyPackagedClass().");
}
}
引用包类
import access.mypackage.*;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/3 21:04
*/
public class UnpackagedMyClass {
public static void main(String[] args) {
MyPackagedClass my = new MyPackagedClass();
}
}
制定工具库
具备了这些知识以后,现在就可以创建自己的工具库来减少或消除重复的程序代码了。例如,我们已经用到的 System.out. println()
的别名可以减少输入负担,这种机制可以用于名为 Print
的类中,这样,我们在使用该类时可以用一个更具可读性的静态 import
语句来导入:
包类Print
package net.tangwenbo.util;
import java.io.PrintStream;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/3 21:10
*/
public class Print {
/**
* print with a newline;
*/
public static void print(Object object){
System.out.println(object);
}
/**
* print a newline by itself
*/
public static void print(){
System.out.println();
}
/**
* print with no line break
*/
public static void printb(Object object){
System.out.print(object);
}
/**
* The new Java SE5 print() (from C)
*/
public static PrintStream printf(String format,Object... args){
return System.out.printf(format,args);
}
}
测试包类Print
import static net.tangwenbo.util.Print.*;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/3 21:17
*/
public class PrintTest {
public static void main(String[] args) {
print("Available from now on!");
print(100);
print(100L);
print(3.14159);
}
}
这个类库的第二个构件可以是在第4章中引入的 range()
方法,它使得 foreach
语法可以用于简单的整数序列:
包类Range
package net.tangwenbo.util;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/3 21:22
*/
public class Range {
/**
* Produce a sequence [0...n)
* @param n 数组长度
* @return 按照顺序赋值好的数组
*/
public static int[] range(int n){
int[] result = new int[n];
for (int i = 0; i < n; i++) {
result[i] = i;
}
return result;
}
/**
*
* @param start 开始的数组下标
* @param end 结束的数组下标
* @return 数组长度为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;
}
/**
*
* @param start 开始的数组下标
* @param end 结束的数组下标
* @param step 跳步
* @return 数组长度为start-end的数组,并且进行了step的跳步,按照顺序赋好了对应的值
*/
public static int[] range(int start,int end,int step){
int sz = end -start;
int[] result = new int[sz];
for (int i = 0; i < sz; i++) {
result[i] = start + (i * step);
}
return result;
}
}
测试类
import static net.tangwenbo.util.Range.*;
import static net.tangwenbo.util.Print.*;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/3 21:29
*/
public class RangeTest {
public static void main(String[] args) {
int[] a = range(5);
printArray(a);
int[] b = range(0,10);
printArray(b);
int[] c =range(0,10,3);
printArray(c);
}
public static void printArray(int[] array){
for (int i = 0; i < array.length; i++) {
printb(array[i]+" ");
}
print();
}
}
练习3:(2)创建两个包: debug和 debuoff,它们都包含一个相同的类,该类有一个 debug()方法。第一个版本显示发送给控制台的 String参数,而第二个版本什么也不做。使用静态 import语句将该类导入到一个测试程序中,并示范条件编译效果。
//debug包类
package access.debug;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/3 21:48
*/
public class Debug {
public static void debug(String s){ }
}
//debugoff包类
package access.debugoff;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/3 21:48
*/
public class Debug {
public static void debug(String s){
System.out.println(s);
}
}
测试类:
import access.debugoff.Debug;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/3 21:50
*/
public class DebugTest {
public static void main(String[] args) {
Debug.debug("debugoff Test");
}
}
Java访问权限修饰词
包访问权限
在Java中如果一个类没有使用任何访问权限修饰词。默认访问权限没有任何关键字,但通常是指包访问权限(有时也表示成为 friendly)。这就意味着当前的包中的所有其他类对那个成员都有访问权限,但对于这个包之外的所有类,这个成员却是 private
。由于一个编译单元(即一个文件),只能隶属于一个包,所以经由包访问权限,处于同一个編译单元中的所有类彼此之间都是自动可访问的。
public:接口访问权限
使用关键字 public
,就意味着 public
之后紧跟着的成员声明自己对每个人都是可用的,尤其是使用类库的客户程序员更是如此。
private:你无法访问
关键字 private
的意思是,除了包含该成员的类之外,其他任何类都无法访问这个成员。由于处于同一个包内的其他类是不可以访问 private
成员的,因此这等于说是自己隔离了自己。从另一方面说,让许多人共同合作来创建一个包也是不大可能的,为此 private
就允许你随意改变该成员,而不必考虑这样做是否会影响到包内其他的类。
protected:继承访问权限
关键字 protected
处理的是继承的概念,通过继承可以利用一个现有类—我们将其称为基类,然后将新成员添加到该现有类中而不必碰该现有类。还可以改变该类的现有成员的行为。为了从现有类中继承,需要声明新类 extends
(扩展)了一个现有类,就像这样:
class Foo extends Bar{}
类定义中的其他部分看起来都是一样的。
如果创建了一个新包,并自另一个包中继承类,那么唯一可以访问的成员就是源包的 public
成员。(当然,如果在同一个包内执行继承工作,就可以操纵所有的拥有包访问权限的成员。)有时,**基类的创建者会希望有某个特定成员,把对它的访问权限赋予派生类而不是所有类。**这就需要 protected
来完成这一工作。 protected
地提供包访问权限,也就是说,相同包内的其他类可以访问 protected
元素。
练习4:(2)展示 protected方法具有包访问权限,但不是 public.
package access.cookie2;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/4 21:35
*/
public class Cookie {
public Cookie(){
System.out.println("Cookie contstructor");
}
protected void bite(){
System.out.println("bite.");
}
}
package access.cookie2; // 与Cookie在同一个包下
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/4 21:36
*/
public class CookieMonster {
public static void main(String[] args) {
Cookie x = new Cookie();
x.bite(); // package access to protected method
}
}
import access.cookie2.*;// 与Cookie不在同一个包下
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/4 21:38
*/
public class CookieThief {
public static void main(String[] args) {
Cookie cookie = new Cookie();
// cookie.bite(); // access protected
}
}
练习5:(2)创建一个带有 public, private, protected和包访问权限域以及方法成员的类。创建该类的一个对象,看看在你试图调用所有类成员时,会得到什么类型的编译信息。请注意,处于同一个目录中的所有类都是默认包的一部分。
package access;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/4 21:43
*/
public class FourWays {
int a = 0;
public int b = 1;
protected int c = 2;
private int d = 3;
FourWays(){
System.out.println("FourWays() constructor");
}
void showa(){
System.out.println(a);
}
public void showb(){
System.out.println(b);
}
protected void showc(){
System.out.println(c);
}
private void showd(){
System.out.println(d);
}
}
package access;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/4 21:45
*/
public class AccessTest {
public static void main(String[] args) {
FourWays fw = new FourWays();
fw.showa();
fw.showb();
fw.showc();
fw.a = 10;
fw.b = 20;
fw.c = 30;
fw.showa();
fw.showb();
fw.showc();
//! fw.showd(); // private access, compiler can't touch
}
}
练习6:(1)创建一个带有 protected数据的类。运用在第一个类中处理 protected数据的方法在相同的文件中创建第二个类。
package access;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/4 21:48
*/
class SomeData{
protected int a = 13;
}
class DataChanger{
static void change(SomeData sd,int i){
sd.a = i;
}
}
public class ProtectedData {
public static void main(String[] args) {
SomeData sd = new SomeData();
System.out.println(sd.a);
DataChanger.change(sd,99);
System.out.println(sd.a);
}
}
类的访问权限
在Java中,访问权限修饰词也可以用于确定库中的哪些类对于该库的使用者是可用的。如果希望某个类可以为某个客户端程序员所用,就可以通过把关键字 public
作用于整个类的定义来达到目的。这样做甚至可以控制客户端程序员是否能创建一个该类的对象。然而,这里还有一些额外的限制:
-
每个编译单元(文件)都只能有一个
public
类。这表示,每个编译单元都有单一的公共接口,用public
类来表现。该接口可以按要求包含众多的支持包访问权限的类。如果在某个编译单元内有一个以上的public
类,编译器就会给出出错信息。 -
public
:类的名称必须完全与含有该编译单元的文件名相匹配,包括大小写。所以对于Widget
而言,文件的名称必须是Widget.java,
而不是widget.java
或WIDGET. java
。如果不匹配,同样将得到编译时错误。 -
虽然不是很常用,但编译单元内完全不带
public
类也是可能的。在这种情况下,可以随意对文件命名。(尽管随意命名会使得人们在阅读和维护代码时产生混淆。)练习7:(1)根据描述 access和 Widget的代码片段创建类库。在某个不属于 access类库的类中创建一个 Widget实例。
package access; // access包下有Widget类
import static net.tangwenbo.util.Print.*;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/7 15:49
*/
public class Widget {
public Widget(){
print("Widget()");
}
}
package Test; // 测试包下有MakeWidget类
import access.*; // 引入access类
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/7 15:50
*/
public class MakeWidget {
public static void main(String[] args) {
Widget widget = new Widget();
}
}
在创建一个包访问权限的类时,仍旧是在将该类的域声明为 private
时才有意义——应尽可能地总是将域指定为私有的,但是通常来说,将与类(包访问权限)相同的访问权限赋予方法也是很合理的。既然一个有包访问权限的类通常只能被用于包内,那么如果对你有强制要求,在此种情况下,编译器会告诉你,你只需要将这样的类的方法设定为 public
就可以了。
请注意,类既不可以是 private
的(这样会使得除该类之外,其他任何类都不可以访问它),也不可以是 protected
的。所以对于类的访问权限,仅有两个选择:包访问权限或 public
。如果不希望其他任何人对该类拥有访问权限,可以把所有的构造器都指定为 private
,从而阻止任何人创建该类的对象,但是有一个例外,就是你在该类的 static
成员内部可以创建。下面是一个示例:
package access;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/7 15:58
*/
class Soup1{
private Soup1(){}
/**
* 只允许该类的静态成员内部创建
* @return 该类的实例
*/
public static Soup1 makeSoup(){
return new Soup1();
}
}
class Soup2{
private Soup2(){}
/**
* 创建该类的一个静态对象,并且返回该类的引用
*/
private static Soup2 ps1 = new Soup2();
public static Soup2 access(){
return ps1;
}
public void f(){
System.out.println("f()");
}
}
public class Lunch {
public static void main(String[] args) {
// Soup1 soup1 = new Soup1(); // 不能够创建该类对象,因为其构造器被设置为了私有
}
void testStatic(){
Soup1 soup = Soup1.makeSoup();
}
void testSingleton(){
Soup2.access().f();
}
}
初看起来可能有点令人迷惑不解。方法名称( makeup)前面的词Soup1
告知了该方法返回的东西。本书到目前为止,这里经常是void
,意思是它不返回任何东西。但是也可以返回一个对象引用,示例中就是这种情况。这个方法返回了一个对Soup1
类的对象的引用。
Soup1
类和Soup2
类展示了如何通过将所有的构造器指定为 private
来阻止直接创建某个类的实例。请一定要牢记,如果没有明确地至少创建一个构造器的话,就会帮你创建一个默认构造器(不带有任何参数的构造器)。如果我们自己编写了默认的构造器,那么就不会自动创建它了。如果把该构造器指定为 private
,那么就谁也无法创建该类的对象了。但是现在别人该怎样使用这个类呢?上面的例子就给出了两种选择:在 Soup1
中,创建一个 static
方法,它创建一个新的Soup1
对象并返回一个对它的引用。如果想要在返回引用之前在Soup1
上做一些额外的工作,或是如果想要记录到底创建了多少个Soup1
对象(可能要限制其数量),这种做法将会是大有裨益的。
Soup2
用到了所谓的设计模式,这种特定的模式被称为 singleton
(单例),这是因为你始终只能创建它的一个对象。Soup2
类的对象是作为Soup2
的一个 static private
成员而创建的,所以有且仅有一个,而且除非是通过 public
方法 access()
,否则是无法访问到它的。
练习8:(4)效仿示例 Lunch.java的形式,创建一个名为 ConnectionManager的类,该类管理一个元素为 Connection对象的固定数组。客户端程序员不能直接创建 Connection对象,而只能通过 ConnectionManager中的某个 static方法来获取它们。当 ConnectionManager之中不再有对象时,它会返回null引用。在main()之中检测这些类。
package access;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/7 16:27
*/
class Connection{
private static int count =0;
private int i = 0;
private Connection(){
System.out.println(" Connection()");
}
static Connection makeConnection(){
count++;
return new Connection();
}
public static int howMany(){
return count;
}
public String toString(){
return ("Connection "+count);
}
}
public class ConnectionManager {
static int howManyLeft = 3;
static Connection[] ca = new Connection[3];
{
for(Connection x:ca){
x = Connection.makeConnection();
}
}
public static Connection getConnection(){
if(howManyLeft>0){
return ca[--howManyLeft];
}else {
System.out.println("No more connections.");
}
return null;
}
public static void main(String[] args) {
ConnectionManager cm = new ConnectionManager();
System.out.println(cm.howManyLeft);
getConnection();
System.out.println(howManyLeft);
getConnection();
System.out.println(howManyLeft);
getConnection();
System.out.println(getConnection());
System.out.println(howManyLeft);
}
}
Connection()
Connection()
Connection()
3
2
1
No more connections.
null
0
练习9:(2)在 access/local目录下编写以下文件(假定 access/local目录在你的 CLASSPATH中):
package access.local;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/7 16:24
*/
class PackagedClass {
public PackagedClass(){
System.out.println("创建一个PackagedClass类");
}
}
然后在 accesss/local之外的另一个目录中创建下列文件:
package access.Foregin;
import access.local.*;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/7 16:26
*/
public class Foregin {
public static void main(String[] args) {
PackagedClass pc = new PackagedClass(); // 编译报错
}
}
解释一下为什么编译器会产生错误。如果将 Foreign类置于 access local包之中的话,会有所改变吗?
报错的原因是PackagedClass
不是public
的,所以在foreign
包不能对local
包内的PackagedClass
类进行访问,如果将其移动到同一个包下,则编译可以通过。