許多 Java 開發者使用 import 多年,卻仍誤以為它類似 C/C++ 的 #include——將代碼物理拷貝進編譯產物。實際上,import 只是編譯器的「簡寫別名服務」,零代碼拷貝、零運行時開銷。本文從命名衝突本質出發,徹底講透 Package 與 Import 的設計初衷與運作機制。
類名如同「名字」,Package 如同「姓氏」。全球 Java 類數量遠超人口總量,若無「姓氏」,String、Date、Util 等通用名稱必發生災難性衝突。
java.lang.String → 復姓 java.lang,名 Stringjava.io.InputStream → 復姓 java.io,名 InputStreamSun 規範建議以反轉域名作為包名前綴(如 com.company.project),理論上保證全球唯一性。
java.io.InputStream is = java.lang.System.in;
java.io.InputStreamReader isr = new java.io.InputStreamReader(is);
java.io.BufferedReader br = new java.io.BufferedReader(isr);
import java.lang.System;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
// 之後可直接用簡寫名
InputStream is = System.in;
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
編譯器見到無包名類名,自動按 import 順序補全全限定名。若兩個包有同名類(如 java.util.Date vs java.sql.Date),需用全限定名消歧。
import java.lang.*;
import java.io.*;
含義:遇到無包名類名,先去 java.lang 找,再去 java.io 找。同樣受歧義限制。
import java.*;?包名層級固定:java.io 是包,java 只是前綴,非包。如同「諸葛」是復姓,不能叫人「諸先生」。
| 特性 | C/C++ #include | Java import |
|---|---|---|
| 代碼拷貝 | 是,物理展開頭文件內容 | 否,零拷貝 |
| 編譯單元膨脹 | 是,頭文件遞歸包含導致巨大 | 否,符號表查找即可 |
| 依賴傳遞 | 強依賴,修改頭文件觸發大面積重編譯 | 弱依賴,只依賴 .class 文件符號表 |
| 運行時存在 | 預處理階段展開,運行時無痕跡 | 純編譯期語法糖,字節碼無 import 痕跡 |
你完全可以不寫任何 import,全程使用全限定名,編譯出的 .class 字節碼逐字節相同。
java.lang 包含 String、System、Thread、Exception 等核心類,使用頻率極高。編譯器默認自動 import java.lang.*;,無需手寫。這也是為什麼 String、System.out.println() 可直接用的原因。
module-info.java,exports 替代包級可見性控制,import 仍工作於模塊內部。下次再見 import,請對自己說:「這只是讓編譯器幫我打字的語法糖,不生成任何額外代碼。」