搞懂 Java 訪問權限,別再讓你的類別裸奔【Thinking in Java筆記(5-1)】

搞懂 Java 訪問權限,別再讓你的類別裸奔【Thinking in Java筆記(5-1)】
Photo by orbtal media / Unsplash
Java 初學者最容易忽略的「誰可以碰誰」規則

很多人學 Java 的時候,最先關注的是語法怎麼寫、變數怎麼取名、跑出來有沒有錯。但如果你真的想寫出不讓自己未來抓狂的程式碼,你需要面對一個事實:你寫的東西遲早會被別人用(或你自己半年後忘光要重看)

這時候,如果你不搞清楚誰能碰哪些類別、方法、欄位,程式就像裸奔一樣毫無防護──別人能亂用、你也會亂碰,然後 bug 像煙火一樣炸開

本篇會用比較人話的方式帶你理解 Java 裡的訪問權限(access specifiers)跟 package 結構,讓你不只是「會寫」,還能「知道為什麼這樣寫」

為什麼需要訪問權限?

想像你寫了一個咖啡機的程式庫。你希望使用者只按「開啟咖啡機」這個按鈕,而不是進去亂動咖啡機裡面磨豆、加熱的細節對吧?

Java 的訪問權限修飾詞(Access Specifiers)就是為了這個目的而生:幫你決定「哪些功能是對外公開的、可以使用的」,哪些則是「內部使用,不准碰」。這對於程式的封裝性、安全性都非常重要

四種訪問修飾詞從大開放到嚴封閉:

  • public:大家都能用,就像電梯的開門按鈕
  • protected:只有自己人(同 package 或子類別)才能用
  • (default)(沒寫修飾詞):只限同一個 package 裡使用,像公司內部網路一樣
  • private:只有你自己能用,像手機解鎖密碼一樣不外傳

Package 是什麼?為什麼你需要關心?

你可能會想:我只是寫個類別,幹嘛管這些 package ?

事實上,Java 是靠 package 來管理類別的,就像你把每張紙分類放進不同的資料夾。這樣你就不會出現兩張紙都叫 List 卻搞不清是哪張的窘境

使用 package 關鍵字

package com.example.myapp;

public class MyClass {
    // 類別內容
}

這段的意思就像:嘿,這個類別屬於 com.example.myapp 這個資料夾!

使用全名 vs import

你可以這樣用類別:

java.util.ArrayList list = new java.util.ArrayList();

這看起來很老實,但說實話就像每次叫你朋友都要連爸媽是誰一起喊免得重名一樣蠢

所以我們可以用 import

import java.util.ArrayList;
ArrayList list = new ArrayList();

更輕鬆一點對吧?甚至你可以:

import java.util.*; // 把整個工具包都搬進來
import java.util.*;

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

編譯單位與檔案命名規則

Java 的每個原始碼檔案,就是一個所謂的「編譯單位(compilation unit)」。它的副檔名一定要是 .java,這就像你寫 Markdown 不能亂取成 .txt 一樣,編譯器才知道這檔案怎麼處理

一個編譯單位裡只能有一個 public 類別,而且這個類別的名稱必須跟檔案名稱一模一樣

// 檔名必須是 MyClass.java
public class MyClass {}

這樣的命名規則其實就是在幫我們避免混亂。如果你讓一個檔案裡的 public 類別叫 Pizza,檔名卻是 Burger.java,Java 編譯器會馬上報錯,因為它根本找不到該去哪裡找那個 Pizza 類別

那如果你想要在同一個檔案裡塞兩個類別呢?也可以,但只有一個能是 public,其他的只能是「包內私有的」非 public 類別,這些類別只對同一個 package 中的其他類別可見。

這在實作上一種常見用法是: 你定義一個 public 類別做為 API 出口,然後讓那些輔助用的小工具類藏在同一個檔案裡但不公開,好像一台咖啡機裡面藏著一堆內部齒輪,但只把按鈕露出來給人按

編譯之後,每個類別都會變成一個 .class 檔案。例如:

// MyClass.java
public class MyClass {}
class Helper {}

編譯完會出現:

MyClass.class
Helper.class

程式碼組織(Code Organization)

當你寫一個 .java 檔案並編譯它,每個類別都會被轉換成一個 .class 檔案,類別的名字會決定這些 .class 檔的檔名。例如:

// MyClass.java
public class MyClass {}
class HelperClass {}

編譯後你會得到:

MyClass.class
HelperClass.class

這些 .class 檔案構成一個 Java 程式的基礎。它們不是單獨存在的,而是會被整合、打包成一個 Java ARchive 檔案,也就是 .jar 檔。你可以把 .jar 檔想成一個裝滿零件的工具箱,其他人只需要打開蓋子(透過 import),就能使用你打包好、設計好的工具類別

程式庫(Library)的內部構造

程式庫實際上就是一組類別檔案,通常每個檔案裡會有一個 public 類別,也可能有一到多個非 public 的類別作為內部輔助角色。這讓每個類別檔看起來像一個對外的接口(interface)+ 一些藏在背後的實作細節。像一個門市部門跟一堆在後台跑單的人一樣分工清楚

package 的用途

如果你希望這些類別都有組織、有歸屬,不是散落各地的野孩子,那就要使用 package 關鍵字。package 必須寫在原始碼中除了註解以外的第一行

package com.example.myapp;

public class MyClass {
    // 類別內容
}

這代表這個 .java 檔案屬於 com.example.myapp 這個命名空間。日後別人要用你寫的 MyClass,就要寫:

import com.example.myapp.MyClass;

這個命名系統就像是把所有類別都塞進明確的資料夾路徑裡,避免同名衝突,也讓專案架構更乾淨、維護更容易

Package 名稱怎麼取才不會撞名?

你可能會想:「我只是寫個檔名叫 MyClass.java,幹嘛還要煩惱什麼 com.example.utils 這種看起來像網址的鬼東西?」

這是因為當 Java 執行時,它會需要從你的程式檔案中「找出正確的類別」,而它找的方法就是看你的類別所屬的 package 名稱

但假如你寫了一個叫 Toolbox 的類別,別人也寫了一個叫 Toolbox 的類別,那 JVM 就會崩潰:「你到底是要我用哪一個啦?」

為了避免這種撞名災難,Java 的做法很聰明——直接要求每個人用「你自己的網域名稱反過來」來命名開頭

比方說:

如果你擁有網站 example.com,那你的 package 名稱就可以這樣寫:

package com.example.myapp;

這就像是在你的類別前面貼上身份證,保證全球唯一,這樣別人用 import com.example.myapp.Toolbox 時,Java 才不會跟隔壁家的 Toolbox 搞混

也就是說 package 名稱不只是裝可愛用的,它會被翻譯成實際的資料夾路徑,用來幫助 Java 虛擬機(JVM)在你電腦裡找到正確的 .class 檔案

Java 怎麼知道要去哪裡找你寫的類別?

當你寫好 Java 程式後,會發現一個神祕的問題浮現:我寫了一堆類別,Java 到底是怎麼知道要去哪裡把它們找出來、載進來的?

我們不講理論,直接用送快遞來比喻

想像你寫了一個類別叫 Vector,它住在 com.example.simple 這個 package 裡。那 Java 解譯器就像一個快遞員,它會

  • 先看一下地圖(CLASSPATH)
    「好,我今天負責送貨的範圍是 C:\MyProject
  • 把地址(package 名稱)轉成路線
    「這個 Vector 類別的地址是 com.example.simple,那我就要走到 com/example/simple 這個資料夾」
  • 找你要的東西
    「進到這個資料夾後,我找到 Vector.class,任務完成,載入它」

這整個過程就是 JVM 的日常。你寫好的類別,經過這個 package 命名 + 資料夾對應的機制,讓 Java 知道該去哪裡找到正確的檔案

範例:

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

假設你把這兩個 .java 檔放在:

C:\MyProject\com\example\simple

並且 CLASSPATH 包含了 C:\MyProject

C:\MyProject

那麼在你的主程式這樣寫:

import com.example.simple.*;

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

JVM 會說:「這些類別住在 com.example.simple,我就從 C:\MyProject\com\example\simple.class 檔來載入」

沒有魔法,只有路徑跟命名規則

使用自訂的工具程式庫

有時候你寫程式會發現自己一直重複寫某些操作,例如 System.out.println()。你想要簡化它,又不想每次打那麼長,這時候就是建立自訂工具類別的好時機

我們可以自己做一個 Print 類別,把常用的印出方法包裝起來:

建立工具類別:

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

這樣做的最大好處是:你可以在任何地方直接呼叫 print()printnb(),不用每次都寫 Print.print() 那麼冗長

這是因為我們用了 import static 語法,它的意思是:「我想直接使用某個類別裡的靜態方法或變數,不用再每次打類別名稱」

一般情況下你會這樣寫:

Print.print("Hello");

加上 import static com.example.util.Print.*; 後,就可以簡化成:

print("Hello");

這讓程式碼更乾淨易讀,特別是當你經常呼叫某個工具類別的靜態方法時,非常實用

更棒的是,如果你把這個工具類別放在自己的工具包(utility package)裡,那它就能在整個專案裡重複使用,減少你每次都複製貼上的痛苦

這種方法不只適用於印東西,你可以根據實際需求封裝任何常用邏輯,讓程式更模組化、易維護,也更容易與他人分享

四種訪問修飾詞範例

了解 Java 的訪問權限修飾詞不能只背定義,你還需要知道:

  • 它們實際上是拿來「控制存取範圍」的工具
  • 選對修飾詞可以防止其他開發者誤用你的類別或方法
  • 對維護大型專案、撰寫函式庫、甚至考 Java 認證都很重要

這裡我們用具體範例來展示四種不同修飾詞的行為

package-private(預設)

這是你什麼都不寫的情況,Java 預設的存取權限

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

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

bite() 方法是預設(package-private)權限,這代表只有在 access.dessert 這個 package 裡的其他類別才能叫用它。換句話說,這是「僅限公司內部人員通行證」,外人不能用

其他 package 就無法調用 bite()

//: 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 class Hello {}

只要是 public,就是「誰都能來用」。不管你是哪個 package、在哪個專案裡,只要能 import 到它,就能直接使用。這通常用在你希望開放給大家的 API 或工具類別

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

這裡的建構子是 private,所以外部不能直接 new 出 Sundae 物件

這通常是用在「你不想讓人亂造這個物件」的情境,例如:

  • 使用工廠模式(Factory Pattern)強制透過某個靜態方法來建立物件
  • 控制建立流程,保留彈性以後可以改變實作

這就像你不讓別人自己做冰淇淋,只能從你的冰淇淋機輸出口拿成品。

自己造自己用,誰都別來

protected

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

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

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

當你想要讓自己的子類別能用這個方法,但又不想整個世界都能用的時候,protected 就派上用場

如果你在另一個 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
*/

在這個例子裡,ChocolateChip2 雖然和 Cookie 不在同一個 package,但因為它是子類別,所以可以存取 bite() 方法。這就是 protected 的魔力——只有「繼承你的人」能進來你家客廳,其他人站門外

小結:你該記住什麼

理解 Java 中的訪問權限修飾詞和正確使用 package 對於編寫安全、模組化的程式碼至關重要。利用 publicprotected、預設(package-private)和 private,您可以精確控制類別和成員的可見性,從而提高程式的封裝性和可維護性。在 Java 裡,每一個訪問修飾詞的存在都是為了讓你的程式更有結構,不只是動得起來,還能被安全地使用、維護、擴展

這裡快速幫你整理你該記住的幾件事:

  • public:全世界都可以使用。用在你希望暴露給外部使用的類別與方法
  • private:只有自己類別裡能用。適合保護內部資料和邏輯,防止外部誤用或破壞封裝
  • protected:同一個 package 或繼承你的子類別可以用。給有「血緣關係」的類別一些特權
  • 預設(package-private):只有同一個 package 內的類別能存取,是 Java 預設的存取方式

還有:

  • package 是幫你整理類別、避免撞名的工具,就像幫你的類別取一個帶地址的姓氏
  • 編譯之後的 .class 檔案就像組裝好的零件,打包成 .jar 才能交給別人用

訪問修飾詞不是裝飾品,而是你寫程式時對外界表態:「這個你可以碰,那個不准你摸」

所以拜託別再亂貼 public 了。封裝是寫好程式的第一步,不封就等著被 bug 追著跑