在實際開發中,常遇到這樣的依賴結構:兩個獨立的 JAR 包 a.jar 和 b.jar,其中 b.jar 依賴 a.jar。具體來說,b.jar 中的類 B 繼承自 a.jar 中的類 A。類 A 定義了 getName() 和 getAge() 兩個方法,類 B 重寫了 getName() 但未重寫 getAge()。
當新建一個 Java 項目 C 僅引用 b.jar,未引用 a.jar 時,會出現兩個令人困惑的問題:
getAge() 方法;B b = (B) map.get("key"))時,編譯器報錯:The type A cannot be resolved. It is indirectly referenced from required .class files。前提條件是:項目 C 不能 引用 a.jar(否則問題不存在)。已知引入 a.jar 可解決問題,但想探究根本原因:JVM 加載 b.jar 中的類並創建對象時,難道不是已準備好所有方法嗎?為何仍會出現父類方法不可見?
這其實是 Java 類加載器 與 符號引用解析 機制共同作用的結果。
JVM 規範定義了類的「主動引用」(必須觸發類初始化)與「被動引用」(不觸發初始化)。當項目 C 使用類 B 時,屬於主動引用,類加載器會加載 B.class。然而,B.class 文件的常量池中存儲著對父類 A 的 符號引用(如繼承關係、方法簽名等),而非父類的具體字節碼。
b.getAge() 時,編譯器需確認方法簽名。由於 B 未重寫 getAge(),編譯器必須查閱父類 A 的定義以驗證方法是否存在、訪問權限等。若缺少 A.class,符號引用無法解析為直接引用,導致「方法不可見」。(B) obj 要求 JVM 確認對象確實是 B 或其子類的實例。這涉及檢查繼承鏈,必須加載父類 A 以驗證類型兼容性。缺少 A.class 時,拋出 Type A cannot be resolved。對象實例化(new B())確實會在堆中分配內存並初始化字段,但 方法調用的動態綁定依賴於方法表。方法表的構建需要完整的類層級結構信息,包括父類的方法元數據。缺少父類字節碼,方法表無法完整構建,繼承方法自然不可用。
a.jar 設為 compile 或 api 依賴,確保傳遞性依賴被消費者獲取。module-info.java 導出包,強制依賴傳遞。「子類對象包含父類方法」是語言層面的語義,但在 JVM 實現層面,類加載是按需、分階段進行的。缺少父類字節碼會阻斷符號引用解析,導致編譯期/運行期檢查失敗。這正是 Java「編譯期類型檢查」與「運行期動態加載」結合的體現。