搞懂 Java 訪問權限,別再讓你的類別裸奔【Thinking in Java筆記(5-1)】
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 對於編寫安全、模組化的程式碼至關重要。利用 public
、protected
、預設(package-private)和 private
,您可以精確控制類別和成員的可見性,從而提高程式的封裝性和可維護性。在 Java 裡,每一個訪問修飾詞的存在都是為了讓你的程式更有結構,不只是動得起來,還能被安全地使用、維護、擴展
這裡快速幫你整理你該記住的幾件事:
public
:全世界都可以使用。用在你希望暴露給外部使用的類別與方法private
:只有自己類別裡能用。適合保護內部資料和邏輯,防止外部誤用或破壞封裝protected
:同一個 package 或繼承你的子類別可以用。給有「血緣關係」的類別一些特權- 預設(package-private):只有同一個 package 內的類別能存取,是 Java 預設的存取方式
還有:
package
是幫你整理類別、避免撞名的工具,就像幫你的類別取一個帶地址的姓氏- 編譯之後的
.class
檔案就像組裝好的零件,打包成.jar
才能交給別人用
訪問修飾詞不是裝飾品,而是你寫程式時對外界表態:「這個你可以碰,那個不准你摸」
所以拜託別再亂貼 public
了。封裝是寫好程式的第一步,不封就等著被 bug 追著跑
Comments ()