程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 診斷Java代碼: 輕松掌握Java泛型類型,第2部分

診斷Java代碼: 輕松掌握Java泛型類型,第2部分

編輯:關於JAVA

J2SE 1.5 ― 代號為“Tiger” ― 計劃在 2003 年年底發布,它將包括泛型類型(如在 JSR-14 原型編譯器中預先展示的那樣,現在可下載獲得)。在 第 1 部分中,我們討論了泛型類型的基礎知識,以及為什麼它們是對 Java 語言的一個重要且迫切需要的補充。我們還說明了為 Tiger 制定的泛型類型的實現怎麼會包含數個“缺陷”,這些缺陷限制了可以使用泛型類型的上下文。

為了幫助新程序員有效地使用泛型類型,我將詳細說明到底泛型類型的哪些用法在 Tiger 和 JSR-14 中是被禁止的,並將說明為什麼這些限制是 JSR-14(理所當然還有 Tiger)為了在 JVM 上兼容地實現泛型類型所使用的實現策略的必然結果。

泛型類型的限制

讓我們先查閱一下 Tiger 和 JSR-14 中泛型類型的使用限制:

不應在靜態成員中引用封閉類型參數。

不能用基本類型實例化泛型類型參數。

不能在數據類型轉換或 instanceof 操作中使用“外露”類型參數。

不能在 new 操作中使用“外露”類型參數。

不能在類定義的 implements 或 extends 子句中使用“外露”類型參數。

為什麼會有這些限制呢?這要歸因於 Tiger 和 JSR-14 為在 JVM 上實現泛型類型所使用的機制。由於 JVM 根本不支持泛型類型,所以這些編譯器“耍了個花招”,使得似乎存在對泛型類型的支持 ― 它們用泛型類型信息檢查所有的代碼,但隨即“擦除”所有的泛型類型並生成只包含普通類型的類文件。

例如,將象 List<T> 這樣的泛型類型擦除得只剩下 List 。“外露”類型參數 ― 單獨出現而不是位於某個類型中的類型參數(如類 List<T> 中的類型參數 T )― 被簡單地擦除成它們的上界(就 T 而言,其上界就是 Object )。

這一技術的功能極其強大;我們可以使幾乎所有泛型類型的精度得到增強,但又與 JVM 保持兼容。事實上,我們甚至可以交替地使用非泛型的舊類(比如 List )和其對應的泛型類( List<T> );兩者在運行時看起來是一樣的。

遺憾的是,正如以上的限制所示,獲得這一功能是有代價的。以這種方式進行擦除在類型系統中引入了缺陷,這些缺陷限制我們使用泛型類型的安全性。

為了幫助說明每種限制,我們查閱會出現這些限制的示例。在本文中,我們將討論前三個限制。與後兩個限制有關的問題過於復雜,因而需要更深入的研究,留待下一篇文章討論。

靜態成員中的封閉類型參數

編譯器完全禁止在靜態方法和靜態內部類中引用封閉類型參數。所以,舉例來說,以下代碼在 Tiger 中就是非法的:

清單 1. 在靜態上下文中非法引用封閉類型參數

class C<T> {
  static void m() {
   T t;
  }
  static class D {
   C<T> t;
  }
}

當編譯這一代碼時,會生成兩個錯誤:

在靜態方法 m 中非法引用 T 的錯誤

在靜態類 D 中非法引用 T 的錯誤

當定義靜態字段時,情況變得更加復雜。在 JSR-14 和 Tiger 中,在泛型類的所有實例中共享該類中的靜態字段。現在,在 JSR-14 編譯器 1.0 和 1.2 中,如果您在靜態字段聲明中引用類型參數,編譯器不會報錯,但它本應該這麼做。字段被共享這一事實很容易在運行時導致奇怪的錯誤,如在不包含數據類型轉換的代碼中出現 ClassCastException 。

例如,以下程序將在這兩個版本的 JSR-14 下通過編譯而沒有任何警告:

清單 2. 在靜態字段中對封閉類型參數的有問題的引用

class C<T> {
  static T member;
  C(T t) { member = t; }
  T getMember() { return member; }
  public static void main(String[] args) {
   C<String> c = new C<String>("test");
   System.out.println(c.getMember().toString());
   new C<Integer>(new Integer(1));
   System.out.println(c.getMember().toString());
  }
}

請注意,每次分配類 C 的實例時,都要重新設置靜態字段 member 。而且,它被設置成的對象類型取決於 C 的實例的類型!在所提供的 main 方法中,第一個實例 c 是 C<String> 類型。而第二個是 C<Integer> 類型。每當從 c 訪問 member 這一共享靜態字段時,總是假定 member 的類型是 String 。但是,在分配了類型為 C<Integer> 的第二個實例之後, member 的類型是 Integer 。

運行 C 的 main 方法的結果可能會讓您吃驚 ― 它將發出一個 ClassCastException !源代碼根本沒有包含任何數據類型轉換,怎麼會這樣呢?事實證明編譯器確實在編譯階段將數據類型轉換插入到代碼中,這樣做是為了解決類型擦除會降低某些表達式的類型的精度這一事實。這些數據類型轉換 被期望能夠成功,但在本例中卻沒有成功。

應該認為 JSR-14 1.0 和 1.2 的這一特殊“功能”是個錯誤。它破壞了類型系統的健全性,或者可以說,它破壞了類型系統應該和程序員達成的“基本契約”。象對靜態方法和類所做的那樣,只要防止程序員在靜態字段中引用泛型類型,情況就會好很多。

請注意允許這種有潛在“爆炸性”的代碼存在所帶來的問題並不是程序員 有意在自己的代碼中覆蓋類型系統。問題是程序員可能會無意中編寫這樣的代碼(比如,由於“復制和粘貼”操作,錯誤地在字段聲明中包括靜態修飾符)。

類型檢查器應該能幫助程序員從這些類型的錯誤中恢復,但對於靜態字段而言,類型系統實際上會使程序員更迷惑。當未使用數據類型轉換的代碼中顯示的唯一錯誤就是 ClassCastException 時,我們應如何診斷這樣的錯誤?對於不清楚 Tiger 中泛型類型所用的實現方案而又恰好假定類型系統合理運行的程序員而言,情況更糟。因為在這樣的情況下,類型系統不是合理地運行。

幸運的是,JSR-14 的最新版本(1.3)宣布在靜態字段中使用類型參數是不合法的。因此,我們有理由期待在 Tiger 的靜態字段中使用類型參數也是不合法的。

泛型類型參數和基本類型

和我們剛才討論的不同,這一限制沒有同樣的潛在缺陷,但它會使您的代碼非常冗長。例如,在 java.util.Hashtable 的泛型版本中,有兩種類型參數:用於 Key 類型的和用於 Value 類型的。因此,如果我們想要一個將 String 映射到 String 的 Hashtable ,我們可以用表達式 new Hashtable<String, String>() 指定新的實例。但是,如果我們想要一個將 String 映射到 int 的 Hashtable ,我們只能創建 Hashtable<String, Integer> 的實例,並將所有的 int 值包裝在 Integer 中。

同樣,Tiger 在這方面當然也是由所用的實現方案得到的。既然類型參數被擦除為它們的界限,而界限不能是基本類型,所以一旦類型被擦除,則對基本類型的實例化會完全沒有意義。

數據類型轉換或 instanceof 操作中的“外露”參數

回想一下,對於“外露”類型參數,我們是指在詞匯上單獨出現的類型參數,而不是更大類型的語法子組件。例如, C<T> 不是“外露”類型參數,但(在 C 主體中) T 是。

如果在代碼中對“外露”類型參數進行數據類型轉換或 instanceof 操作,則編譯器將發出名為“unchecked”的警告。例如,以下代碼將生成警告: Warning: unchecked cast to type T :

清單 3. 帶 unchecked 操作的泛型代碼

import java.util.Hashtable;
interface Registry {
  public void register(Object o);
}
class C<T> implements Registry {
  int counter = 0;
  Hashtable<Integer, T> values;
  public C() {
   values = new Hashtable<Integer, T>();
  }
  public void register(Object o) {
   values.put(new Integer(counter), (T)o);
   counter++;
  }
}

您應該嚴肅地對待這些警告,因為它們說明您的代碼在運行時會表現得非常奇怪。事實上,它們會使得診斷代碼變得極為困難。在以前的代碼中,我們認為如果對實例 C<JFrame> 調用 register("test") ,會發出 ClassCastException 。但並非如此;計算將繼續,就仿佛數據類型轉換成功了一樣,然後在進一步進行計算時發出錯誤,或者更糟:用遭破壞的數據完成計算,但不向外發出任何錯誤信號。同樣,對“外露”類型參數的 instanceof 檢查將在編譯時產生“unchecked”警告,而且檢查將不會如期在運行時進行。

雙刃劍

那麼,這裡到底發生了什麼?因為 Tiger 依靠類型擦除,所以數據類型轉換和 instanceof 測試中的外露類型參數被“擦除”為它們的上界(在前面的例子中,那將是類型 Object )。因此,對類型參數的數據類型轉換將變成對參數上界的轉換。

同樣, instanceof 將檢查操作數是否是參數界限的 instanceof 。那根本不是我們打算做的,如果是的話,我們完全可以顯式地強制轉換為界限。因此,通常應避免對類型參數使用數據類型轉換和 instanceof 檢查。

然而,有時為了編譯代碼,您必須依靠對類型參數的數據類型轉換。如果是這樣的情況,只要記住,在代碼的那一部分中,類型檢查不保險 ― 要靠自己。

盡管泛型類型是制作健壯代碼的強大武器,但我們已經演示了誤用它們會使代碼不再健壯而且極難診斷和修正。下次,我們將介紹 Tiger 中泛型類型的後兩個限制,並討論試圖在泛型 Java 類型系統中包括它們時必定會出現的一些問題。

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