程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 診斷Java代碼: 平台相關性“gotcha問題”

診斷Java代碼: 平台相關性“gotcha問題”

編輯:關於JAVA

一次編寫,隨處運行。這是承諾,但 Java 語言有時候並不能做到。誠然,JVM 把跨平台互操作性的程度提到了前所未有的高度,然而,規范和實現級別上的一些小毛病卻使得程序無法在多平台上表現出正確的行為。

用 Java 編程的主要優點之一是它給您帶來的很大程度的平台無關性。您只要將您的產品編譯成字節碼,然後分發到任何帶有 JVM 的平台就行了,而不必為每個目標平台構建一個獨立的構建版。或者說,至少事情應該是這樣的。

但事情並沒有那麼簡單。盡管通過對多平台的支持,Java 編程能夠為開發者節約無數的時間,但是,不同的 JVM 版本之間存在許多兼容性問題。其中一些問題很容易就可以找到和糾正,例如:在構造路徑名的時候使用特定於平台的分隔符字符。但其它問題可能就很難或者不可能截查到。

因此,一些難以解釋的不正常的程序行為在某個特定的 JVM 中有可能是一個錯誤,記住這一點是很重要的。

與供應商相關的錯誤

當然,如果想看看存在於 JVM 中的眾多微妙的與平台相關的錯誤中的一些,您只需偶而查查 Sun 的 Java Bug Parade(請參閱 參考資料)。這裡所列的許多錯誤都是僅僅適用於某一特定平台上的 JVM 的 實現錯誤。如果碰巧不在該平台上進行開發,您甚至可能不知道您的程序會在那個平台上受阻。

但是,並非所有的 Java 平台相關性都是 JVM 實現錯誤的結果。顯著的平台相關性是 JVM 規范自身帶來的。當 JVM 的細節在規范級別上不受限制時,就可能在 JVM 之間產生與供應商相關的行為。

例如,正如我們回顧“ Improve the performance of your Java code”(2001 年 5 月)所看到的,JVM 規范對 尾遞歸調用(tail-recursive call)的優化不作要求。尾遞歸調用就是作為方法的最後一個操作出現的遞歸的方法調用。更一般地說,任何方法調用,不管是不是遞歸的,只要出現在方法的末尾就是 尾調用(tail call)。例如,考慮以下簡單的代碼:

清單 1. 一個尾遞歸的 factorial

public class Math {
  public int factorial(int n) {
   return _factorial(n, 1);
  }

  private int _factorial(int n, int result) {
   if (n <= 0) {
    return result;
   }
   else {
    return _factorial(n - 1, n * result);
   }
  }
}

在這個示例中,公共的 factorial方法和私有的 助手方法 _factorial 都包含尾調用; factorial 包含一個對 _factorial 的尾調用, _factorial 包含一個對它自身的尾遞歸調用。

如果您覺得用這種辦法編寫 factorial特別復雜,那您並不是唯一有這種感受的人。為什麼不用如下自然得多的形式編寫它呢?

清單 2. 一個純遞歸的 factorial

public class Math {
  int factorial(int n) {
   if (n <= 0) {
    return 1;
   }
   else {
    return n * factorial(n-1);
   }
  }
}

回答是尾遞歸考慮到了很強有力的優化 ― 尾遞歸讓我們用為 被調方法構建的堆棧幀來代替為 主調方法構建的堆棧幀。這可以極大地減小運行時的堆棧深度,從而避免堆棧溢出(尤其是如果尾調用是遞歸的話,例如清單 2 中對 _factorial 的尾調用)。

有些 JVM 實現這種優化;有些則不然。結果是有些程序在有些平台上會引起堆棧溢出,在其它平台上則不會。要是這種優化可以靜態地進行,我們就可以只將字節碼編譯成尾調用優化過的形式,這樣就能同時享有平台無關性和這種優化。不幸的是,正如我在上面所引用的討論這個主題的文章中所講解的那樣,這種優化無法靜態地進行。

與版本相關的錯誤

尾調用產生的平台相關性是 JVM 規范自身的產物。但是,平台相關性更常見的起因是 JVM 實現中的錯誤。對於 Swing 的情況,這種錯誤廣泛存在。

例如,JDK 1.4 中的 JOptionPane 組件就一個有關的錯誤。如果用戶把 JOptionPane 中的文本添加到緊跟在空白行後的一行中,然後按“下箭頭”鍵,什麼事也沒發生。自己試試看:

打開一個新的 JOptionPane。

在 OptionPane 中,接著按 Enter 鍵兩次。

輸入“test”。

按“上箭頭”鍵。

按“下箭頭”鍵。

看來,上述的操作序列(以及類似的操作序列)使 JOptionPane 進入了一種奇怪的狀態。如果您程序的某個用戶發現了這個錯誤,那麼它多半是通過瘋狂敲擊他的鍵盤從這種狀態恢復的。(從這樣一種狀態中恢復並不困難;按“右箭頭”鍵即可搞定。)一旦恢復後,他可能再不會將程序的凍結放在心上,甚至可能永遠也不會報告這個錯誤。用戶的可接受標准已經被幾十年來滿是錯誤的軟件大大降低了。

而肇事者在這裡。這種錯誤在針對我所測試的每種平台 ― Windows、Solaris 和 Linux ― 的所有 Sun JDK 1.4 版本中都存在。所以,這很可能是 Sun 的 JDK 中的一個與操作系統相關的錯誤。

這個示例說明,平台相關性不是僅僅與操作系統相關性有關,也不是僅僅與供應商相關性有關 ― 它與 JVM 版本相關性有關,包括向前的和向後的。

各小組通常對提供向後兼容性很關心,但他們也希望他們的代碼在後來的版本中能保持其行為。理想情況下,這種期望可能是合理的,但在現實中卻不然。事實上,考慮到 Sun 在提高版本 1.4 的 Swing 的性能方面付出了大量努力,給其中帶來一個錯誤就不那麼令人驚詫了。

順便說一下,並不是只有 Sun 一家對 Swing 的性能不滿意。Eclipse 項目是一個開放源代碼的項目,它旨在為開發高度集成的工具提供健壯的、開放源代碼的、功能全面的並且是商業級別的平台,它實現了一個全新的窗口小部件工具箱,稱為 Standard Widget Toolkit(SWT)。SWT 是極輕量級的,因為,不同於 Swing,它利用了底層的特定於平台的窗口系統(windowing system)(請參閱 參考資料)。這個 API 在所有實現它的平台上是相同的,但它的觀感則完全取決於平台。所以,我們可以預期,這個工具箱會有一大堆新的與平台相關的問題。

與操作系統相關的錯誤

作為最後一個示例,這個示例是關於您可以在 Java 平台上體會到的平台相關性的某些潛伏形式的,假設我們正在為一個編輯器編寫代碼,這個編輯器將打開文件並將它們讀入到編輯器視窗。剛開始,我們可能編寫如下代碼:

FileReader reader = new FileReader(file);
_editorKit.read(reader, tempDoc, 0);

對 _editorKit.read 的調用把文件的內容讀入到一個臨時文檔,這個文檔稍後將被添加到打開的文檔的集合中。但是在這兩行之後,我們再也沒有引用 reader。

這段代碼取自 DrJava IDE ― Rice 大學的免費的、開放源代碼的 Java IDE ― 的早期版本(請參閱 參考資料)。現在,如果您熟悉 Split Cleaner 錯誤模式,那您可能已經注意到這段代碼是該模式的一個很好的示例。

FileReader 被構造來讀取文件的內容,但這個 FileReader 從未被關閉。當然,與 Split Cleaner 的其它實例一樣,這個錯誤直到試圖對這個文件進行其它訪問為止才會出現症狀。但是,盡管那樣,依據平台,它有可能不出現任何症狀!

假設用戶後來試圖刪除這個文件。在 UNIX 上,打開的文件可以被刪除,所以,這個殘留的、未被關閉的 FileReader 不會引起任何問題。但如果用戶是在 Windows 上,則打開的文件無法被刪除,所以將會有異常被拋出。我們是在我們的一個單元測試在 UNIX 上成功通過了,而在 Windows 上卻通不過時發現前面代碼清單中的錯誤的。一旦診斷出了這個問題,修正它並不難:

FileReader reader = new FileReader(file);
_editorKit.read(reader, tempDoc, 0);
reader.close(); // win32 needs readers closed explicitly!

跨平台並不是毫無代價的

正如本專欄的示例所演示的那樣,Java 語言並不能不受潛伏的與平台相關的錯誤的影響。這些錯誤的症狀多種多樣,但說不定什麼時候,某些錯誤就會咬您一口。

誠然,用 Java 語言比用許多其它語言編寫跨平台的代碼,其代價要小得多,但並不是毫無代價的。我能給出的最好建議是在盡可能多的平台上、使用盡可能多的 JVM 運行您的單元測試。還有,跟往常一樣,避免編寫易於出錯的代碼。易於出錯的代碼與平台相關性的結合是致命的。這裡是我們這個月講述的內容的總結:

模式:與供應商相關的錯誤。

症狀:錯誤可能出現在某些 JVM 上,但在其它 JVM 上則不出現。

起因:JVM 規范未加以指定的某些方面(例如,未對尾遞歸調用的優化作出要求)。這類起因比 與版本相關的錯誤少見。

處方和預防措施:隨所碰到問題的不同而不同。

模式:與版本有關的錯誤。

症狀:錯誤可能出現在 JVM 的某些版本上,但在其它版本上則不出現。

起因:某些 JVM 實現中的錯誤,例如 Swing。這是比 與供應商相關的錯誤更常見的起因。

處方和預防措施:隨所碰到問題的不同而不同。

模式:與操作系統相關的錯誤。

症狀:錯誤可能出現在某些操作系統上,但在其它操作系統上則不出現。

起因:系統行為的規則在不同操作系統上有所不同(例如:在 Unix 上,打開的文件可以被刪除;在 Windows 上則不能)。

處方和預防措施:隨所碰到問題的不同而不同。

我要感謝 DrJava 開發人員 Brian Stoler 和 John Garvin,謝謝他們協助找出本文所討論的後兩個錯誤。

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