原文鏈接:http://www-128.ibm.com/developerworks/cn/java/j-dclp2.html
ClassNotFoundException
是最常見的類裝入異常類型。它發生在裝入階段。Java 規范對 ClassNotFoundException
的描述是這樣的:
當應用程序試圖通過類的字符串名稱,使用以下三種方法裝入類,但卻找不到指定名稱的類定義時拋出該異常。
- 類
Class
中的forName()
方法。 - 類
ClassLoader
中的findSystemClass()
方法。 - 類
ClassLoader
中的loadClass()
方法。
所以,如果顯式地裝入類的嘗試失敗,那么就拋出 ClassNotFoundException
。清單 1 中的測試用例提供的示例代碼拋出了一個 ClassNotFoundException
:
清單 1. ClassNotFoundExceptionTest.java
|
這個測試用例定義了一個類裝入器(MyClassLoader
),用于裝入一個不存在的類(DoesNotExist
)。當它運行時,會出現以下異常:
|
因為這個測試試圖使用對 loadClass()
的顯式調用來進行裝入,所以拋出 ClassNotFoundException
。
通過拋出 ClassNotFoundException
,類裝入器提示,定義類時所需要的字節碼在類裝入器所查找的位置上不存在。這些異常修復起來通常比較簡單??梢杂?IBM 的 verbose 選項檢查類路徑,確保使用的類路徑設置正確(要獲得 verbose 的更多信息,請參閱本系列的 第一篇文章)。如果類路徑設置正確,但是仍然看到這個錯誤,那么就是需要的類在類路徑中不存在。要修復這個問題,可以把類移動到類路徑中指定的目錄或 JAR 文件中,或者把類所在的位置添加到類路徑中。
![]() ![]() |
![]() |
NoClassDefFoundError
是類裝入器在裝入階段拋出的另一個常見異常。JVM 規范對 NoClassDefFoundError
的定義如下:
如果 Java 虛擬機或ClassLoader
實例試圖裝入類定義(作為正常的方法調用的一部分,或者作為使用 new 表達式創建新實例的一部分),但卻沒有找到類定義時拋出該異常。
當目前執行的類已經編譯,但是找不到它的定義時,會存在 searched-for 類定義。
實際上,這意味著 NoClassDefFoundError
的拋出,是不成功的隱式類裝入的結果。
清單 2 到清單 4 的測試用例產生了 NoClassDefFoundError
,因為類 B
的隱式裝入會失敗:
清單 2. NoClassDefFoundErrorTest.java
|
清單 3. A.java
|
清單 4. B.java
|
這幾個清單中的代碼編譯好之后,刪除 B
的類文件。當代碼執行時,就會出現以下錯誤:
|
類 A
擴展了類 B
;所以,當類 A
裝入時,類裝入器會隱式地裝入類 B
。因為類 B
不存在,所以拋出 NoClassDefFoundError
。如果顯式地告訴類裝入器裝入類 B
(例如通過 loadClass("B")
調用),那么就會拋出 ClassNotFoundException
。
顯然,要修復這個特殊示例中的問題,在對應的類裝入器的類路徑中,必須存在類 B
。這個示例看起來可能價值不大、也不真實,但是,在復雜的有許多類的真實系統中,會因為類在打包或部署期間的遺失而發生這類情況。
在這個例子中,A
擴展了 B
;但是,即使 A
用其他方式引用 B
,也會出現同樣的問題 —— 例如,以方法參數引用或作為實例字段。如果兩個類之間的關系是引用關系而不是繼承關系,那么會在第一次使用 A
時拋出錯誤,而不是在裝入 A
時拋出。
![]() ![]() |
![]() |
類裝入器能夠拋出的另一個異常是 ClassCastException
。它是在類型比較中發現不兼容類型的時候拋出的。JVM 規范指定 ClassCastException
是:
該異常的拋出,表明代碼企圖把對象的類型轉換成一個子類,而該對象并不是這個子類的實例。
清單 5 演示的代碼示例會產生一個 ClassCastException
:
清單 5. ClassCastException.java
|
在清單 5 中,調用了 storeItem()
方法,使用一個 Integer
數組、一個 int
和一個字符串作為參數。但是在內部,該方法做了兩件事:
- 隱式地把
String
對象類型轉換成Object
類型(用于參數列表)。 - 顯式地把這個
Object
類型轉換成Integer
類型(在方法定義中)。
當程序運行時,會出現以下異常:
|
這個異常是由顯式類型轉換拋出的,因為測試用例試圖把類型為 String
的東西轉換成 Integer
。
當檢查對象(例如清單 5 中的 item
)并把類型轉換成目標類(Integer
)時,類裝入器會檢查以下規則:
-
對于普通對象(非數組):對象必須是目標類的實例或目標類的子類的實例。如果目標類是接口,那么會把它當作實現了該接口的一個子類。
-
對于數組類型:目標類必須是數組類型或
java.lang.Object
、java.lang.Cloneable
或java.io.Serializable
。
如果違反了以上任何一條規則,那么類裝入器就會拋出 ClassCastException
。修復這類異常的最簡單方式就是仔細檢查對象要轉換到的類型是否符合以上提到的規則。在某些情況下,在做類型轉換之前用 instanceof
進行檢查是有意義的。
![]() ![]() |
![]() |
在把本機調用鏈接到對應的本機定義時,類裝入器扮演著重要角色。如果程序試圖裝入一個不存在或者放錯的本機庫時,在鏈接階段的解析過程會發生 UnsatisfiedLinkError
。JVM 規范指定 UnsatisfiedLinkError
是:
對于聲明為 native
的方法,如果 Java 虛擬機找不到和它對應的本機語言定義,就會拋出該異常。
當調用本機方法時,類裝入器會嘗試裝入定義了該方法的本機庫。如果找不到這個庫,就會拋出這個錯誤。
清單 6 演示了拋出 UnsatisfiedLinkError
的測試用例 :
清單 6. UnsatisfiedLinkError.java
|
這段代碼調用本機方法 call_A_Native_Method()
,該方法是在本機庫 myNativeLibrary
中定義的。因為這個庫不存在,所以在程序運行時會發生以下錯誤:
|
本機庫的裝入由調用 System.loadLibrary()
方法的類的類裝入器啟動 —— 在清單 6 中,就是 UnsatisfiedLinkErrorTest
的類裝入器。根據使用的類裝入器,會搜索不同的位置:
- 對于由 bootstrap 類裝入器裝入的類,搜索
sun.boot.library.path
。 - 對于由擴展類裝入器裝入的類,先搜索
java.ext.dirs
,然后是sun.boot.library.path
,然后是java.library.path
。 - 對于由系統類裝入器裝入的類,搜索
sun.boot.library.path
,然后是java.library.path
。
在清單 6 中,UnsatisfiedLinkErrorTest
類是由系統類裝入器裝入的。要裝入所引用的本機庫,這個類裝入器先查找 sun.boot.library.path
,然后查找 java.library.path
。因為在兩個位置中都沒有需要的庫,所以類裝入器拋出 UnsatisfiedLinkageError
。
一旦理解了庫裝入過程所涉及的類裝入器,就可以通過把庫放在合適位置來解決這類問題。
![]() ![]() |
![]() |
JVM 規范指定 ClassCircularityError
的拋出條件是:
類或接口由于是自己的超類或超接口而不能被裝入。
這個錯誤是在鏈接階段的解析過程中拋出的。這個錯誤有點奇怪,因為 Java 編譯器不允許發生這種循環情況。但是,如果獨立地編譯類,然后再把它們放在一起,就可能發生這個錯誤。請設想以下場景。首先,編譯清單 7 和清單 8 中的類:
清單 7. A.java
|
清單 8. B.java
|
然后,分別編譯清單 9 和清單 10 中的類:
清單 9. A.java
|
清單 10. B.java
|
最后,采用清單 7 的類 A
和清單 10 的類 B
,并運行一個應用程序,試圖裝入 A
或者 B
。這個情況看起來可能不太可能,但是在復雜的系統中,在把不同部分放在一起的時候,可能會發生類似的情況。
顯然,要修復這個問題,必須避免循環的類層次結構。
![]() ![]() |
![]() |
JVM 規范指出,拋出 ClassFormatError
的條件是:
負責指定所請求的編譯類或接口的二進制數據形式有誤。
這個異常是在類裝入的鏈接階段的校驗過程中拋出。如果字節碼發生了更改,例如主版本號或次版本號發生了更改,那么二進制數據的形式就會有誤。例如,如果對字節碼故意做了更改,或者在通過網絡傳送類文件時現出了錯誤,那么就可能發生這個異常。
修復這個問題的惟一方法就是獲得字節碼的正確副本,可能需要重新進行編譯。
![]() ![]() |
![]() |
根據 JVM 規范,拋出 ExceptionInInitializer
的情況是:
- 如果初始化器突然完成,拋出一些異常
E
,而且E
的類不是Error
或者它的某個子類,那么就會創建ExceptionInInitializerError
類的一個新實例,并用E
作為參數,用這個實例代替E
。 - 如果 Java 虛擬機試圖創建類
ExceptionInInitializerError
的新實例,但是因為出現Out-Of-Memory-Error
而無法創建新實例,那么就拋出OutOfMemoryError
對象作為代替。
清單 8 中的代碼拋出 ExceptionInInitializerError
:
清單 8. ExceptionInInitializerErrorTest.java
|
當靜態代碼塊中發生異常時,會被自動捕捉并用 ExceptionInInitializerError
包裝該異常。在下面的輸出中可以看到這點:
|
這個錯誤在類裝入的初始化階段拋出。修復這個錯誤的方法是檢查造成 ExceptionInInitializerError
的異常(在堆棧跟蹤的 Caused by:
下顯示)并尋找阻止拋出這個異常的方式。