jdk版本不同引起的問題分析
JDK版本不同導致的執行時錯誤
最近有一同事編寫的java程式在本地開發環境中能夠正常執行,但是複製到實際環境中執行時報錯(開發環境作業系統windows,程式實際執行環境linux),異常資訊如下:
java.lang.NoSuchMethodError: java.lang.StringBuffer: method insert(ILjava/lang/CharSequence;)Ljava/lang/StringBuffer; not found
at FirstApp.caozuoqueren$5.actionPerformed(caozuoqueren.java:393)
at javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:1815)
at javax.swing.AbstractButton$ForwardActionEvents.actionPerformed(AbstractButton.java:1868)
同事不知問題出在哪裡,讓我幫除錯一下。從異常資訊來看提示資訊非常明確,即StringBuffer類中不存在方法insert(ILjava/lang/CharSequence;),既然程式從本地能正常編譯和執行,而換一個環境就不能正常執行,很直觀的就想到是不是由於JDK版本差異導致的問題,於是向同事詢問情況瞭解到開發時使用的JDK版本為1.5,而實際生成環境中使用的是JDK1.4,由此基本上可以斷定是由於JDK版本不同引起的該問題,但具體原因是什麼哪?當然還要看程式程式碼是如何寫的,程式中有這麼一句程式碼:
sbDispInfor.insert(str.length(),sbInput.toString());
其中sbDispInfor和sbInput的型別都為StringBuffer,問題就出在對insert方法的呼叫。於是開啟JDK1.4的幫助手冊,StringBuffer類的所有insert方法定義如下:
而在JDK1.5中StringBuffer類的所有insert方法定義如下:
對比兩個版本StringBuffer類insert方法定義會發現1.5版本中比1.4版本中多了兩個方法:
程式碼中呼叫insert方法時第2個引數為sbInput,型別為StringBuffer,但是1.4和1.5版本中都沒有定義第2個引數型別為StringBuffer的insert方法,程式是如何編譯通過的哪?別忘了java支援物件型別之間的自動轉換,在1.5版本中StringBuffer的宣告如下:
public final class StringBuffer
可以看到StringBuffer類實現了CharSequence介面,因此一個StringBuffer物件其實也可以當作一個CharSequence物件來看待(java的多型性),在使用jdk1.5版本編譯程式時由於該版本中StringBuffer類中存在方法的定義,因此javac編譯程式會把引數sbInput的型別由StringBuffer自動轉換成
CharSequence,這在1.5版本中執行時是沒問題的,但是移植到1.4版本中執行時由於StringBuffer類沒有定義引數為CharSequence型別的insert方法,因此會報本文開頭出給出的異常。
類似的BigDecimal類也存在類似問題,編寫一測試程式如下:
import java.math.*;
public class TestBigDecimal{
public static void main( String[] args){
try{
BigDecimal bd1 = new BigDecimal("1");
System.out.println(" bd1 , BigDecimal(/"1/")=" + bd1 );
}catch(Exception e){
System.out.println(e);
}
try{
BigDecimal bd2 = new BigDecimal(2);
System.out.println(" bd2 , BigDecimal(2)=" + bd2 );
}catch(Exception e){
System.out.println(e);
}
}
}
A. 使用jdk1.5進行編譯, javac TestBigDecimal.java
然後在1.4版本下執行會丟擲一下異常:
Exception in thread "main" java.lang.UnsupportedClassVersionError: TestBigDecimal (Unsupported major.minor version 49.0)
。。。
B.使用jdk1.5進行編譯,新增source引數,javac –source 1.4 TestBigDecimal.java
然後在1.4版本下執行會丟擲以下異常:
bd1 , BigDecimal("1")=1
Exception in thread "main" java.lang.NoSuchMethodError: java.math.BigDecimal.<init>(I)V
at TestBigDecimal.main(TestBigDecimal.java:13)
。。。
C.使用jdk1.5進行編譯,新增source和target引數,javac –source 1.4 –target 1.4 TestBigDecimal.java
然後在1.4版本下執行會丟擲以下異常:
bd1 , BigDecimal("1")=1
Exception in thread "main" java.lang.NoSuchMethodError: java.math.BigDecimal.<init>(I)V
at TestBigDecimal.main(TestBigDecimal.java:13)
。。。
分析上面的各種情況產生的結果,在A這種情況下由於是在1.5版本中編譯成的 class檔案,放到1.4版本下執行時不受支援,即1.4版本不認可1.5版本生成的 class檔案 類結構(很容易理解,軟體版本的向下相容性)。在B這種情況下,通過新增引數source對於生成的class檔案提供與1.4版本的源相容性,可以看到位元組碼檔案可以在1.4下執行,第1個輸出語句成功執行,第2個再構造BigDecimal bd2 = new BigDecimal(2); 物件時丟擲異常。在C這種情況下除了新增source引數外還添加了target引數用於生成特定 VM 版本的類檔案,但是這種情況下和B報錯一樣(這地方有些疑惑,按照javac中對target引數的說明,編譯時就應該和使用1.4編譯器的效果是完全一樣的,但從結果看並非如此)。產生這種錯誤的原因是兩個版本中BigDecimal類構造器的差異導致,BigDecimal類在1.4版本中建構函式其中的兩個:BigDecimal(double val)和 BigDecimal(String val),在1.5版本當中除了上述的兩個之外又新增加了BigDecimal(int val),程式中BigDecimal bd2 = new BigDecimal(2); 宣告在1.5版本下引數‘2’實際上被當成了int型處理,如果在1.4版本下編譯檔案‘2’會被轉化成‘2.0’即double型。如果用1.5編譯器編譯實際上只是標記該類可以在1.4版本中執行,編譯成的位元組碼仍然是1.5編譯器的位元組碼(2並沒有轉化成double型2.0),因此在1.4下面執行會出錯。
從以上兩個例子的分析可以得出一下結論:
1)軟體版本一般是向下相容的,java虛擬機器也不例外,即低版本虛擬機器生成的class檔案可以在高版本虛擬機器中執行,反之則未必可以(向上相容)。
2)在1.5版本下編譯的class檔案要想在1.4版本中執行,使用javac編譯時需要新增額外的引數,如上例在1.5版本下編譯時使用命令:javac -source 1.4 TestBigDecimal.java,這樣生成的class檔案能夠被1.4版本接受,但並不代表一定能夠執行(如上例就丟擲異常)。
3)為了提高軟體的可移植性,儘量使用低版本編譯類檔案,這樣既可以在相同版本虛擬機器中執行也可以在高版本虛擬機器中執行(當然如果想使用高版本提供的新特性情況除外)。能明確型別的最好明確型別,不要完全依賴於java的自動型別轉換,比如第1個例子中把引數sbInput改成sbInput.toString()在兩個版本中就都可以正常執行。
附:檢視class檔案支援可以執行的jdk最低版本的方法,可以通過文字編輯器(如UtralEdit)開啟class位元組碼檔案,察看第8個位元組的值,版本對應關係如下:
第8位值(16進位制) |
10進位制 |
對應jdk版本 |
2E |
46 |
1.4.2 |
30 |
48 |
1.4 |
31 |
49 |
1.5 |
32 |
50 |
1.6 |
該值僅僅說明該class檔案能夠被該版本或更高版本的虛擬機器接受,但是能不能正常執行並不能得到保證(比如上面例子中的錯誤),如果使用javac編譯時未新增任何引數那麼該值說明class位元組碼檔案是由該版本的虛擬機器編譯生成,當然能夠確在該版本中正常執行。