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/baz
或foo\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
底下 CLASSPATH
為CLASSPATH=.;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
*/
Vector
和List
中的 class 以及要使用的方法都必須是public
的
- 當編譯器碰到
simple
library 中的import
時,就會開始在CLASSPATH
所指定的目錄開始尋找,找到子目錄com\example\simple
後,從已編譯的文件中找出名稱相符的 (Vector.class
、List.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)”
- 取得對某個成員的訪問權的途徑
- 使該成員成為
public
,無論是誰、哪裡,都能訪問該成員 - 透過不加訪問權限修飾詞,與其他 class 放在同一個 package 內來賦予成員訪問權
- 繼承來的 class 可以訪問
public
和protected
的成員,但只有兩個 class 在同一個 package 內才能訪問 package access 的成員 - 提供訪問器(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()"); }
}
- 這裡看起來破壞了一般的權限規則,通常會認為
Pie
和f()
是 package access,因此不能被Cake
使用- 這只有部分正確。他們確實享有 package access,但
Pie
與Cake
同屬相同的目錄且沒有設置任何 package name。Java 將這樣的檔案自動視為隸屬於此目錄的 default package 中,因此該目錄底下的檔案都互相提供了 package access
- 這只有部分正確。他們確實享有 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()