Thinking in Java(5-1) 訪問權限修飾詞與 Package 的使用

Thinking in Java(5-1) 訪問權限修飾詞與 Package 的使用
Photo by orbtal media / Unsplash

在 Java 中,訪問權限修飾詞(access specifiers) 讓程式庫的開發者能夠明確指示哪些類別和成員對外部可見,哪些則不可見。這對於維護程式的封裝性和安全性非常重要。

訪問權限修飾詞的層級

從最高權限到最低權限,Java 提供了以下修飾詞:

  1. public
  2. protected
  3. 預設(package-private,沒有修飾詞)
  4. private

在理解這些修飾詞的作用之前,我們需要先了解 Java 如何整合大量的程式檔案。

Package 的使用

在 Java 中,package 關鍵字用於組織和控制程式庫內的類別。訪問權限修飾詞的效果會因為類別位於相同或不同的 package 而有所不同。

一個 package 包含一組類別,這些類別在同一個命名空間(namespace)下組織。例如,Java 標準工具程式庫 java.util 包含了許多常用的類別,如 ArrayList

使用全名引用類別

您可以使用類別的全名來引用它:

public class FullQualification {
  public static void main(String[] args) {
    java.util.ArrayList list = new java.util.ArrayList();
  }
}

這種方式雖然明確,但程式碼會顯得冗長。

使用 import 關鍵字

為了讓程式碼更簡潔,可以使用 import 關鍵字:

import java.util.ArrayList;

public class SingleImport {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
    }
}

或者,導入整個 package 下的所有類別:

import java.util.*;

public class ImportAll {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        HashMap map = new HashMap();
    }
}

在這個管理命名空間的機制下,所有類別成員的名稱都是隔離的,不會發生衝突。

編譯單元與命名規則

每個 Java 原始碼檔案稱為編譯單元(compilation unit),必須以 .java 為副檔名。每個編譯單元只能有一個 public 類別,且檔案名稱必須與該類別名稱相同。其他非 public 的類別僅在同一個 package 內可見,通常用來支持主要的 public 類別。

程式碼組織(Code Organization)

當您編譯一個 .java 檔案時,該檔案中的每個類別都會生成一個相同名稱的 .class 檔案。例如,如果您有一個名為 MyClass.java 的檔案,且其中包含兩個類別 MyClassHelperClass,那麼編譯後會生成 MyClass.classHelperClass.class

一個工作中的 Java 程式是由一組 .class 檔案組成的,這些檔案可以打包並壓縮成一個 Java ARchive(JAR) 檔案。Java 解譯器負責查找、載入和解譯這些檔案。

程式庫(Library) 實際上是一組類別檔案,每個檔案都有一個 public 類別和任意數量的非 public 類別。因此,每個檔案都有一個公開的元件。

如果希望這些元件都屬於同一個群組,可以使用關鍵字 packagepackage 必須是檔案中除了註解以外的第一行程式碼。

package com.example.myapp;

public class MyClass {
    // 類別內容
}

這表示這個編譯單元屬於 com.example.myapp 這個程式庫。任何想要使用這個程式庫的人都必須使用全名或透過 import 關鍵字。

唯一的 package 名稱

為了避免 package 名稱的衝突,一個常見的做法是將特定 package 的所有 .class 檔案都放在同一個目錄下。透過將 .class 檔案的路徑編碼到 package 名稱中,可以建立唯一的名稱,並查找隱藏於目錄結構中的類別。

慣例上,package 名稱的第一部分是類別建立者的網域名稱反轉。 例如,如果您的網域是 example.com,那麼您的 package 可以命名為 com.example。在網域和路徑都唯一的情況下,名稱衝突的問題就不會發生。

當 Java 程式執行並需要載入 .class 檔案時,它會透過以下方式確定檔案的位置:

  1. 查找 CLASSPATH 環境變數: 這是用來指定查找 .class 檔案的根目錄。
  2. 解析 package 名稱: 從根目錄開始,解譯器將 package 名稱中的每個句點 . 替換為系統相依的目錄分隔符號(在 Windows 系統中為 \,在 UNIX 系統中為 /)。例如,com.example.simple 會被解析為 com/example/simplecom\example\simple
  3. 尋找類別檔案: 生成的路徑會與 CLASSPATH 中的每個路徑結合,解譯器會在這些目錄中尋找與要創建的類別名稱相符的 .class 檔案。

範例:

package com.example.simple;

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

public class List {
    public List() {
        System.out.println("com.example.simple.List");
    }
}

假設這兩個檔案位於目錄 C:\MyProject\com\example\simple 下,並且 CLASSPATH 包含了 C:\MyProject

當您在程式中使用這些類別時:

import com.example.simple.*;

public class LibTest {
    public static void main(String[] args) {
        Vector v = new Vector();
        List l = new List();
    }
}

解譯器會根據 CLASSPATH 和 package 名稱找到相應的 .class 檔案,然後載入並執行。

使用自訂的工具程式庫

您可以利用上述機制建立自己的工具程式庫,減少重複的程式碼。

建立工具類別:

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);
  }
}

使用工具類別:

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
*/

透過靜態 import,您可以直接使用工具類別的方法,而不需要指定類別名稱。

訪問權限修飾詞的詳細解說

預設(package-private)

沒有任何訪問修飾詞的成員僅對同一個 package 中的類別可見,對其他 package 則不可見。

範例:

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

public class Cookie {
  public Cookie() {
   System.out.println("Cookie constructor");
  }
  void bite() { System.out.println("bite"); }
}

在其他 package 中:

//: 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
*/

public

public 修飾的類別和成員對所有其他類別都可見。

private

private 修飾的成員僅對自身類別可見,對同一個 package 的其他類別也不可見。

範例:

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();
  }
}

protected

protected 修飾的成員對同一個 package 的類別和所有子類別可見,即使子類別在不同的 package 中。

範例:

在 package com.example.cookie2 中:

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

public class Cookie {
  public Cookie() {
    System.out.println("Cookie constructor");
  }
  protected void bite() {
    System.out.println("bite");
  }
} 

在其他 package 中繼承並使用:

//: 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
*/

小結

理解 Java 中的訪問權限修飾詞和正確使用 package 對於編寫安全、模組化的程式碼至關重要。利用 publicprotected、預設(package-private)和 private,您可以精確控制類別和成員的可見性,從而提高程式的封裝性和可維護性。