java编程思想(六): 访问权限控制

访问权限控制

访问权限控制主要是为了解决类库开发的问题,类库的代码往往需要重构,但是类库的使用者却需要代码保持不变,为此,对于类库中的代码,使用者的访问应该是受限制的,他应该只能访问部分需要公开的代码,比如所谓的API。

因此程序代码需要约定代码的访问权限。在java中,访问权限从大到小为:public, protected, 包访问权限(没有关键词), private. 其中包是库的基本单元。

包:库单元

包是通过名字空间组织的一组类。

通过包可以很方便的把实现某一功能的类组织起来,在类文件(.java)中通过 package package_name 指定类所在的包,通过import 来导入。比如import java.util.ArrayList 导入java.util包下的ArrayList类。

代码组织

编译一个.java文件时,生成一个同名的.class文件,其中.java文件必须保证只有一个public的类和任意数量的非public类。

  1. 通过package指定所在的包

    1
    2
    3
    4
    5
    // access/mypackage/MyClass.java
    package access.mypackage;
    Class MyClass{
    // ...
    }
  2. 通过import导入

    1
    2
    3
    4
    5
    6
    7
    // access/ImportedMyClass.java
    import access.mypackage.MyClass;
    Class ImportedMyClass{
    public static void main(String[] args){
    MyClass m = new MyClass();
    }
    }

包命名与包查找

上文通过package指定了包,并通过import导入包中的类,那么import是如何找到这个类呢?

首先,需要给包一个独一无二的名字,最好的办法是通过域名, 比如package com.ajm。而包文件所处的路径应当与域名相同,这样才能通过包的名字查找。package com.ajm对应的代码目录结构是{src}/com/ajm/ 其中src是包所在的系统路径。

import只是在.java文件中指明要包含的包,但是java是如何查找并导入的呢?实际上,在javacjava命令中,我们需要用-CLASSPATH指定包的路径。

java解释器的运行过程如下:首先找出环境变量CLASSPATH(通过操作系统设置), CLASSPATH包含了一个或多个目录,是查找.class文件的根目录;然后将.换成反斜杠,比如import access.mypackage.MyClass换成access/mypackage/, 在CLASSPATH中查找这个子目录,然后找出MyClass.class文件。

访问权限修饰词

public

所有人都可以访问,用于提供给类库使用者的接口。

1
2
3
4
5
6
7
8
// access/dessert/Cookie.java
package access.dessert;
public class Cookie{
public Cookie(){
System.out.println("Cookie Constructor");
}
void bite(){ System.out.println("bite"); } // 不加修饰词,默认包访问权限
}

1
2
3
4
5
6
7
8
9
10
11
// access/Dinner.java
package access;
import access.dessert.*;
public class Dinner{
public static void main(String[] args){
Cookie cookie = new Cookie();
}
}
/* output:
Cookie Constructor
*/

如上,Cookie在包access.dessert中,但是在这个包之外的Dinner却可以访问Cookie的public构造器。

包访问权限

包访问权限是默认的访问权限,不需要关键词修饰,同一个包内的各个类可以互相访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// access/Dinner1.java
package access;
import access.dessert.*;
public class Dinner1{
public static void main(String[] args){
Cookie cookie = new Cookie(); // 可以访问
cookie.bite(); // 不能访问
}
}

// access/dessert/Dinner2.java
package access.dessert;
public class Dinner2{
public static void main(String[] args){
Cookie cookie = new Cookie(); // 可以访问
cookie.bite(); // 可以访问
}
}

protected

包可以访问,包以外的继承类可以访问。有些方法或域想要让包外的派生类可以使用,但是如果用public修饰,所有人都可以使用,为了解决这个问题,引入protected.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//: access/ChocolateChip.java
// Can't use package-access member from another package.
import access.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();
}
}

例如ChocolateChip类继承自Cookie,想要使用bite()方法,但是由于在包access.dessert之外,无法访问。为了解决这个问题,可以将bite()方法改为protected,如下:

1
2
3
4
5
6
7
8
// access/dessert/Cookie.java
package access.dessert;
public class Cookie{
public Cookie(){
System.out.println("Cookie Constructor");
}
protected void bite(){ System.out.println("bite"); } // 包内类和派生类可以访问
}

private

只有类内部可以访问

注意

class 只能是public 或 包访问权限的,因为protected, private作用在class上没有意义。因此,在一个.java文件中,希望暴露给所有人使用的类,用public; 只是用于包内的辅助类,默认包权限。

单例模式

访问控制的一个很好的例子是单例设计模式。有时候我们想要某个类只能生成一个对象实例。在实际应用中,有时候系统只需要拥有一个全局对象,比如服务器程序中,服务器的配置信息存放在一个文件中,可以通过一个单例对象读取这个文件信息,其他对象可以通过这个单例对象获取这些信息。

为了实现这个目的,显然,该类的构造器只能是私有的,否则就可以通过new创建对象了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// singleton(饿汉模式:类加载时生成对象)
public class Singleton{
private static final Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}

// singleton(懒汉模式)
public class Singleton{
private static Singleton INSTANCE = null;
private Singleton(){}
public static Singleton getInstance(){
if(INSTANCE == null){
INSTANCE = new Singleton();
}
return INSTANCE;
}
}

但是,以上的单例实现不是线程安全的,getInstance方法如果被两个线程同时访问,都因为INSTANCE == null而创建实例,就会产生两个实例。