程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> java class的兼容問題,javaclass兼容

java class的兼容問題,javaclass兼容

編輯:JAVA綜合教程

java class的兼容問題,javaclass兼容


    前不久在工作中,遇到了幾次編譯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配置source、target參數來編譯低版本class或打jar包時,必需用bootclasspath指定對應低版本的類庫,否則也可能產生不兼容。這也意味著:不要僅僅裝一個jdk8就期待編譯出一定能在jre1.6上正常運行的程序,你還需要一個1.6版本的java類庫來完成編譯。
  • 如果有替換個別class文件來打補丁的習慣,那麼也需要特別小心兼容問題,原理是一樣的。

上面所說的不兼容問題,會延後到真正調用問題方法時候才會暴露,所以值得加以重視。

相關鏈接:

javac官方文檔:http://docs.oracle.com/javase/8/docs/technotes/tools/windows/javac.html

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved