前不久在工作中,遇到了幾次編譯class引起的NoSuchMethodError,經過分析與測試驗證,也算是搞清楚了中間的來龍去脈,現在把一些結論性的東西(附帶一些過程性的分析)分享出來。
在使用javac -source 1.6 -target 1.6來編譯低版本的(這裡為1.6)class時,記得要使用-bootclasspath參數來指定1.6版本的類庫(一般是rt.jar),不指定的話,會產生一個警告:
警告: [options] 未與 -source 1.6 一起設置引導類路徑
或者英文版的
warning: [options] bootstrap class path not set in conjunction with -source 1.6
如果忽視這個警告(當時我在網上搜索上述中文警告時,沒有任何資料說需要引起注意,以及該如何解決),編譯出來的class可能無法在低版本的jre中運行,假如源碼中調用了一些特殊方法,則會在執行時拋出NoSuchMethodError。比如ConcurrentHashMap的keySet方法,在jdk1.6中,該方法返回的是Set,在jdk1.8中,該方法返回的是KeySetView,它是jdk1.8中新增的一個類,為Set的一個實現。當把這樣編譯出來的class放到jre1.6中去運行時,會因為找不到返回類型為KeySetView的keySet方法而拋出NoSuchMethodError,雖然編譯後的class的版本是1.6。
基於上面的認知,來討論一下如下場景
現在有apiA_1.0.jar與apiB_1.0.jar,apiB_1.0.jar依賴apiA_1.0.jar,前者是基於後者編譯的,也就是這兩個版本之間不存在兼容問題。
然後假如apiA進行了修改,升級為apiA_1.1.jar,其中某個類的某個方法的返回值由Object改為了String(從源碼上來講,這樣改是兼容的,因為String是一個Object,這應該就是裡氏替換吧),此時apiB_1.0.jar就不兼容apiA_1.1.jar了,如果單方面把apiA升級到1.1,apiB在調用apiA中的那個返回值為Object的方法時,會因為找不到方法而拋出NoSuchMethodError(如果對此有異議,請看後文),因為現在在apiA中,只有那個返回值為String的方法了,並且,你也不可能保留返回值為Object的那個方法,它們是互相沖突的。
當然,此時也可以重新發布一個apiB_1.1.jar,基於apiA_1.1.jar編譯出來的版本。但這樣,也就意味著,apiB依賴了apiA特定的版本,這樣非常不利於依賴維護,使用過程中很容易出問題,而且這種問題只有在運行時,調用了有問題的方法時才會發現,應用程序的編譯過程中是不會報錯的(apiA和apiB是已經編譯的jar了)。
也許此時你已經注意到了,難道jdk也不向前兼容了?為什麼我用jdk1.6編譯出來的程序能在jre1.8中正常的調用ConcurrentHashMap.keySet?它不是也存在上面所說的問題嗎?它為什麼不會因為找不到返回值為Set的keySet方法而拋異常?
這裡就需要介紹一下class中的橋接方法(bridge method)了,它不報錯,是因為1.8中確實也存在一個返回值為Set的keySet方法,只不過不是存在於源文件中,而是存在於class文件中,通過javap -v java.util.concurrent.ConcurrentHashMap反編譯1.8的ConcurrentHashMap,可以看到一個返回值為java.util.Set的keySet方法:
public java.util.Set keySet(); descriptor: ()Ljava/util/Set; flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokevirtual #838 // Method keySet:()Ljava/util/concurrent/ConcurrentHashMap$KeySetView; 4: areturn LineNumberTable: line 267: 0
ps:flag參數中的ACC_BRIDGE表明了這是一個橋接方法
雖然java語法層面不允許存在僅返回值不同的兩個方法,但在class文件中,並沒有此限制,在此橋接方法中,調用了返回值為KeySetView的keySet方法。另外java.lang.reflect.Method.isBridge()就是指的這個。
那為什麼ConcurrentHashMap.keySet會有橋接方法呢?其實也不是jdk給自己搞的特殊化,是因為keySet是一個重寫方法(接口方法也有此效果),重寫了父類AbstractMap的public Set<K> keySet()方法,這個大致可以理解為,父類或接口已經對外宣稱了該方法(也就是返回Set),那如果子類或實現者自己返回了其它子類型,那麼編譯器就得來做這個兼容性工作,即創建橋接方法。如果直接改了頂層方法,編譯器自然不可能去做這個事情,它怎麼知道要跟誰兼容?同理,靜態方法也會有問題。
最後總結一下:
上面所說的不兼容問題,會延後到真正調用問題方法時候才會暴露,所以值得加以重視。
相關鏈接:
javac官方文檔:http://docs.oracle.com/javase/8/docs/technotes/tools/windows/javac.html