跳至主要内容

6.1 Java 訪問權限修飾詞及 package 用法

  • Java 提供了訪問權限修飾詞(access specifiers),以供 class library 的開發人員對客戶端的 programmer 指明哪些是可用的,哪些是不可用的
  • 權限由最大到最小依序為
    • public
    • protected
    • package access (沒有修飾詞)
    • private
  • 在理解這些修飾詞的作用前,需要先理解 Java 如何整合大量的程式碼檔案

Package 的使用

  • 在 Java 中,對於 library 內的元件,訪問控制的問題還不完善。例如,如何將元件打包到一個內聚的 library 單元仍是一個挑戰
    • 為了解決這點,Java 使用關鍵字 package 來做控制
    • 訪問修飾詞會因 class 存在於相同的 package 或不同的 package 而受到影響
  • package 內包含一組 class,在單一 namespace 下被組織在一起
  • 例如,有個 Java 標準工具 library,被組織在 java.util namespace 底下,其中有一個叫 ArrayList 的 class,一種使用方法是使用全名 java.util.ArrayList
public class FullQualification {
public static void main(String[] args) {
java.util.ArrayList list = new java.util.ArrayList();
}
}
  • 這樣使得 code 很冗長,可以使用 import 關鍵字來導入單個 class
public class SingleImport {
public static void main(String[] args) {
ArrayList list = new java.util.ArrayList();
}
}

如此一來可以不用限定的使用 ArrayList

  • 若想要導入所有的 class,只需要使用 *
import java.util.*;
  • 在這個管理 namespace 的機制下,所有 class 成員的名稱都是隔離的
  • 當編寫一份 Java 的 source code 檔案時,通常被稱為編譯單元(compilation unit),或稱轉譯單元(translation unit)
    • 每個編譯單元單必須有一個後綴名 .java
    • 編譯單元內可以有一個 public class,且該 class 名稱要與檔案相同
    • 每個編譯單元只能有一個 public class,否則編譯器不會接受,如果還有額外的 class,在 package 外的世界是無法看到這些 class 的,這是因為他們不是 public class,用來為主 public class 提供支持

code organization

  • 當編譯一個 .java 檔案時,.java 檔案中的每個 class 都會生成一個與 class 名稱相同的 .class 檔案
  • Java 的 working program 是由一組可以打包並壓縮成一個 Java ARchive (JAR).class 檔案
  • Java interpreter(解釋器) 負責查找(finding)、裝載(loading)、解釋(interpreting) 這些檔案
  • 程式庫 (library) 實際上是一組 class 檔案,其中每個檔案都有一個 public class 和任意數量的非 public class,因此每個檔案都有一個公開的元件 (public component)
  • 如果希望這些 component 都屬於同一個群組,就可以使用關鍵字 package
    • package 必須是檔案中除了註解以外的第一句程式碼
package access;
  • 這裡聲明這個編譯單元 (compilation unit) 是名為 access 的程式庫 (library) 的一部分,任何想要使用該程式庫的人都必須使用全名或使用關鍵字 import
  • Java package 的命名規則是全小寫字母,包括中間的字也不例外
// access/mypackage/MyClass.java
package access.mypackage;

public class MyClass {}

檔案名稱為 MyClass,則檔案中必須且只有一個 public class,且名稱必須要是 MyClass

public class QualifiedMyClass {
public static void main(String[] args) {
access.mypackage.MyClass m =
new access.mypackage.MyClass();
}
}
import access.mypackage.*;

public class ImportedMyClass {
public static void main(String[] args) {
MyClass m = new MyClass();
}
}

建立唯一的 package name

  • 為了解決 package 並未單一檔案的問題,其中一個合乎邏輯的做法是將特定 package 的所有 .class 檔案都放在同一個目錄底下
    • 透過將 .class 路徑編碼成 package 名稱,此方法得以建立唯一的名稱,並查找隱藏於目錄結構中的 class
    • 照慣例 package 名稱的第一個部分是 class 建立者的 domain 反過來,因此在 domain 及路徑都唯一的情況下便不會出現名稱衝突問題
    • 當 Java 程式運行並需要載入 .class 文件時便可以確定檔案所在位置
  • Java 解釋器的運行過程:
    • 找出環境變數 CLASSPATH,作為查找 .class 檔案的根目錄
    • 從根目錄開始,解釋器獲取 package 的名稱並且將每個句點替換成反斜線 \ (Windows 系統) 或斜線 / (UNIX 系統)
      • foo.bar.baz 會成為 foo/bar/bazfoo\bar\baz (取決於操作系統)
    • 得到的路徑會與 CLASSPATH 中每個不同的項目相連接,解釋器就在這些目錄中尋找與要創建的 class 名稱相符的 .class 檔案
package com.example.simple;

public class Vector {
public Vector() {
System.out.println("net.mindview.simple.Vector");
}
}
package com.example.simple;

public class List {
public List() {
System.out.println("net.mindview.simple.List");
}
}
  • 假設這兩個文件被放在子目錄 C:\DOC\JavaT\com\example\simple 底下
  • CLASSPATHCLASSPATH=.;D\JAVA\LIB;C:\DOC\JavaT
    • CLASSPATH 可以包含多個可供選擇的查詢路徑
  • 使用 JAR 文件時需要將檔案實際名稱寫清楚
    • CLASSPATH=.;D\JAVA\LIB;C:\DOC\JavaT;C:\flavors\graph.jar
  • 一旦 class 的路徑可以正確的建立,底下的檔案可以放置任何目錄底下
import com.example.simple.*;

public class LibTest {
public static void main(String[] args) {
Vector v = new Vector();
List l = new List();
}
} /* Output:
com.example.simple.Vector
com.example.simple.List
*/

VectorList 中的 class 以及要使用的方法都必須是 public

  • 當編譯器碰到 simple library 中的 import 時,就會開始在 CLASSPATH 所指定的目錄開始尋找,找到子目錄 com\example\simple 後,從已編譯的文件中找出名稱相符的 (Vector.classList.class)

Build a custom tool library

  • 利用這些機制可以創建自己的工具庫,減少重複的 code
package com.example.util;
import java.io.*;

public class Print {
// Print with a newline:
public static void print(Object obj) {
System.out.println(obj);
}
// Print a newline by itself:
public static void print() {
System.out.println();
}
// Print with no line break:
public static void printnb(Object obj) {
System.out.print(obj);
}
// The new Java SE5 printf() (from C):
public static PrintStream
printf(String format, Object... args) {
return System.out.printf(format, args);
}
}
  • 在使用這個 class 時可以運用更具可讀性的靜態 import 來導入
import static com.example.util.Print.*;

public class PrintTest {
public static void main(String[] args) {
print("Available from now on!");
print(100);
print(100L);
print(3.14159);
}
} /* Output:
Available from now on!
100
100
3.14159
*/

訪問權限修飾詞 (access specifiers)

  • 修飾詞
    • public
    • protected
    • private
  • 使用時放置在 class 每個成員的定義之前—無論是 field 還是 method
  • 如果不提供任何修飾詞,就代表是 package access

package access

  • 沒有任何訪問權限修飾詞時的預設權限
  • 訪問範圍
    • 相同 package 中的所有其他 class 成員都有訪問權限
    • 對於 package 以外的 class 都是 private
  • 每個編譯單元 (即一個檔案) 只能隸屬於一個 package,屬於同一個編譯單元中的所有 class 彼此都是自動可訪問的
  • “只有你擁有的程式代碼,才能訪問你所擁有的其他程式代碼(only code that you own should have package access to other code that you own)”
  • 取得對某個成員的訪問權的途徑
    1. 使該成員成為 public,無論是誰、哪裡,都能訪問該成員
    2. 透過不加訪問權限修飾詞,與其他 class 放在同一個 package 內來賦予成員訪問權
    3. 繼承來的 class 可以訪問 publicprotected 的成員,但只有兩個 class 在同一個 package 內才能訪問 package access 的成員
    4. 提供訪問器(accessor)、變異器(murator) 方法 (get/set 方法),以讀取或改變值

public

  • 使用 public 關鍵字,代表後面的成員對所有人都是可用的
//: access/dessert/Cookie.java
package access.dessert;

public class Cookie {
public Cookie() {
System.out.println("Cookie constructor");
}
void bite() { System.out.println("bite"); }
}
  • Cookie.java 檔案必須位於名為 dessert 的子目錄中,並且在目錄 access 底下
  • access 目錄必須位於 CLASSPATH 指定的眾多路徑之一的下面
//: access/Dinner.java
import access.dessert.*;

public class Dinner {
public static void main(String[] args) {
Cookie x = new Cookie();
//! x.bite(); // Can't access
}
} /* Output:
Cookie constructor
*/
  • 這邊能建立一個 Cookie object,因為 constructor 是 public 且 class 也是 public
  • 由於 bite() 只向在 dessert package 內的 class 提供訪問權,所以 bite()Dinner.java 中無法訪問,因此被編譯器禁止

The default package

//: access/Cake.java
class Cake {
public static void main(String[] args) {
Pie x = new Pie();
x.f();
}
} /* Output:
Pie.f()
*/

//: access/Pie.java
class Pie {
void f() { System.out.println("Pie.f()"); }
}
  • 這裡看起來破壞了一般的權限規則,通常會認為 Pief() 是 package access,因此不能被 Cake 使用
    • 這只有部分正確。他們確實享有 package access,但 PieCake 同屬相同的目錄且沒有設置任何 package name。Java 將這樣的檔案自動視為隸屬於此目錄的 default package 中,因此該目錄底下的檔案都互相提供了 package access

private

  • private 的意思為,除了包含該成員的 class 以外,任何其他 class 都無法訪問這個成員
  • 處於同一個 package 的 class 也不可以訪問 private 成員,等於把自己隔離開了,因此 private 的成員修改就不會影響到 package 內的其他 class
class Sundae {
private Sundae() {}
static Sundae makeASundae() {
return new Sundae();
}
}

public class IceCream {
public static void main(String[] args) {
//! Sundae x = new Sundae();
Sundae x = Sundae.makeASundae();
}
}
  • 透過 private 阻止直接使用 constructor 建立 Sundae,只能調用 makeASundae() 來創建 object
  • 任何可以肯定只是該 class 的助手的方法都可以指定為 private,確保不會在 package 內其他地方誤用,也防止會改變或刪除這個方法

protected

  • 關鍵字 protected 處理的是繼承的概念
  • 通過繼承可以利用現有的 class (稱為 base class),將新成員添加到現有的 class 而不必修改這個現有的 class,還能改變該 class 現有成員的行為
  • 為了從現有的 class 中繼承,需要宣告新的 class 拓展 (extends) 了一個現有的 class:
class Foo extends Bar {}
  • 如果建立了一個新的 package,並且從另一個 package 中繼承 class,唯一可以訪問的成員是來源 package 的 public 成員
  • 有時 base class 的建立者會希望某個特定成員的訪問權只給予衍生 (derived) class 存取權而非所有 class,就需要 protected 來完成這個工作
  • protected 也提供 package access,也就是說相同 package 的其他 class 可以訪問 protected 元素
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();
}
} /* Output:
Cookie constructor
ChocolateChip constructor
*/

在這個 class 中無法使用 bite(),雖然可以指定為 public,但這樣所有人都會得到訪問權限,這很可能不是你所希望的

//: access/cookie2/Cookie.java
package access.cookie2;

public class Cookie {
public Cookie() {
System.out.println("Cookie constructor");
}
protected void bite() {
System.out.println("bite");
}
}
//: access/ChocolateChip2.java
import access.cookie2.*;

public class ChocolateChip2 extends Cookie {
public ChocolateChip2() {
System.out.println("ChocolateChip2 constructor");
}
public void chomp() { bite(); } // Protected method
public static void main(String[] args) {
ChocolateChip2 x = new ChocolateChip2();
x.chomp();
}
} /* Output:
Cookie constructor
ChocolateChip2 constructor
bite
*/

現在對於所有 Cookie 的繼承者都能使用 bite()