轉(zhuǎn)自:http://blog.csdn.net/wangchengsi/article/details/2110647
Java的類型轉(zhuǎn)換異常(ClassCastException),恐怕是開發(fā)中最常見的異常之一,比如你把一個(gè)本身為String的對象強(qiáng)行轉(zhuǎn)換成List時(shí),就會拋出此異常。當(dāng)然,一般情況下這種錯(cuò)誤很容易就從異常信息中發(fā)現(xiàn)原因并糾正,通常對于此類問題我們的想法就是:class文件相同,即字節(jié)碼相同,那么實(shí)例化產(chǎn)生的對象肯定也會相同類型。但是,存在一些情況,會發(fā)生形如“把class1轉(zhuǎn)換成class1卻拋出類型轉(zhuǎn)換異?!钡那闆r
先看一個(gè)例子,包含三個(gè)源文件:MainClass,ClassOne,ClassTwo 。MainClass是程序入口,另外兩個(gè)類用于測試,代碼很簡單,如下
ClassOne.java
-----------------------------------------------
package test.jboss.jmx.classCastEx;
Class ClassOne{
public void doTest(Object obj){
ClassTwo c2 = (ClassTwo)obj;
}
}
ClassTwo只是一個(gè)空類
------------------------------------------------
package test.jboss.jmx.classCastEx;
Class ClassTwo{
}
MainClass.java
------------------------------------------------
public class MainClass {
/**
* @param args
* @throws MalformedURLException
* @throws ClassNotFoundException
* @throws IllegalAccessException
* @throws InstantiationException
* @throws NoSuchMethodException
* @throws InvocationTargetException
* @throws SecurityException
* @throws IllegalArgumentException
*/
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, SecurityException, InvocationTargetException, NoSuchMethodException {
// ClassOne 和 ClassTwo打包到一個(gè)jar中,名為“test.jar”,放在 MainClass 所在項(xiàng)目的根目錄下
// 注意,ClassOne 和 ClassTwo 不能和MainClass放在一個(gè)項(xiàng)目中,后面 有解釋
// 自定義一個(gè)類加載器,從外部導(dǎo)入ClassOne 和 ClassTwo
File jar = new File("test.jar");
System.out.println(jar.exists());
URL[] url = {jar.toURL()};
System.out.println(url[0]);
URLClassLoader ucl1 = new URLClassLoader(url);
Class classTwo = ucl1.loadClass("test.jboss.jmx.classCastEx.ClassTwo");
//查看是否成功地利用反射機(jī)制,將ClassTwo導(dǎo)入進(jìn)來,并顯示其在VM中的hash碼
// getClassLoader()正常情況返回classTwo的類加載器,也就是上面的 ucl1 ,如果不是,則有問題
System.out.println("hash of layout1:"+classTwo.hashCode()+"ucl "+classTwo.getClassLoader());
//創(chuàng)建一個(gè)ClassTwo的實(shí)例
Object c2_obj = classTwo.newInstance();
// 同理,用另一個(gè)類加載器(ucl2)載入ClassOne
File jar2 = new File("test.jar");
System.out.println(jar2.exists());
URL[] url2 = {jar2.toURL()};
URLClassLoader ucl2 = new URLClassLoader(url2);
Class classOne = ucl2.loadClass("test.jboss.jmx.classCastEx.ClassOne");
Object c1_obj = classOne.newInstance();
//利用反射,調(diào)用ClassOne的 doTest 方法,將上面創(chuàng)建的 c2_obj 作為方法的參數(shù)
Class[] type = {Object.class};
Method m = classOne.getMethod("doTest", type);
Object[] para = {c2_obj};
m.invoke(c1_obj, para);
}
如果按固有的思維,則 c2_obj 傳入 doTest方法后,執(zhí)行 ClassTwo c2 = (ClassTwo)obj; 是沒問題的,但是實(shí)際運(yùn)行則會拋出 ClassCastException ,并明確的告訴你 “ClassTwo c2 = (ClassTwo)obj”有問題,為什么呢?
錯(cuò)誤信息:
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at MainClass.main(MainClass.java:30)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Caused by: java.lang.ClassCastException: com.test.ClassTwo cannot be cast to com.test.ClassTwo
at com.test.ClassOne.doTest(ClassOne.java:8)
... 10 more
原因在于使用了不同的類加載器載入各個(gè)類。其中,main函數(shù)中,ucl1載入的是ClassTwo,而ucl2在載入ClassOne時(shí),由于ClassOne內(nèi)部引用了ClassTwo,ucl1會把ClassTwo也一起加載進(jìn)來,這樣VM就有了兩個(gè)ClassTwo,分別對應(yīng)不同的類加載器。對于ClassOne.doTest() 中的“ ClassTwo c2 = (ClassTwo)obj”這句代碼,c2 的類型全稱是“ucl2加載的test.jboss.jmx.classCastEx.ClassOne”
現(xiàn)在我們應(yīng)該明白了,之所以會有類型轉(zhuǎn)換異常,是由于類在VM中的簽名不僅僅是類的完整包名,還包括載入它的類加載器。上述例子中,由ucl1加載的ClassTwo,作為參數(shù)傳入由ucl2加載的ClassOne.doTest() 中,自然就與 c2 的類型不符合了,導(dǎo)致無法轉(zhuǎn)換
也許你一般不會用這種“古怪”的方式加載類,通常我們都是把需要的外部類寫入項(xiàng)目的classpath,現(xiàn)在的IDE也提供非常方便的手段導(dǎo)入外部類。但是想象一下在應(yīng)用服務(wù)器容器中,你部署的多個(gè)應(yīng)用都可能共享某個(gè)jar庫的類實(shí)例。當(dāng)重新部署包含該jar的應(yīng)用時(shí),所有相關(guān)的應(yīng)用都必須刷新一遍,否則很容易出現(xiàn)上述問題
PS:本文參考了《JBOSS 4.0 標(biāo)準(zhǔn)教材》中的內(nèi)容,書中提供了相應(yīng)源碼解釋這個(gè)問題,不過比較繁瑣,上面的代碼是我簡化過的,在我的機(jī)子上試驗(yàn)成功。請不要將ClassOne 和 ClassTwo? 與MainClass放在一個(gè)項(xiàng)目中,那樣在運(yùn)行之前就會預(yù)先加載項(xiàng)目中所有的類,等于ClassOne 和 ClassTwo都由VM先加載了,就不會出現(xiàn)預(yù)期的轉(zhuǎn)換異常了??梢詫lassOne 和 ClassTwo在另一個(gè)項(xiàng)目中編寫然后打包,放到MainClass所在項(xiàng)目的根目錄