ammayjxf

           

          2009年12月14日

          java 初始化

          解析 Java 類和對象的初始化過程

          由一個單態模式引出的問題談起

          developerWorks
          文檔選項
          將打印機的版面設置成橫向打印模式

          打印本頁

          將此頁作為電子郵件發送

          將此頁作為電子郵件發送


          級別: 初級

          張 國建 (guojian.zhang@gmail.com), 軟件工程師, 北京高偉達西南分軟

          2006 年 8 月 31 日

          類的初始化和對象初始化是 JVM 管理的類型生命周期中非常重要的兩個環節,Google 了一遍網絡,有關類裝載機制的文章倒是不少,然而類初始化和對象初始化的文章并不多,特別是從字節碼和 JVM 層次來分析的文章更是鮮有所見。

          本文主要對類和對象初始化全過程進行分析,通過一個實際問題引入,將源代碼轉換成 JVM 字節碼后,對 JVM 執行過程的關鍵點進行全面解析,并在文中穿插入了相關 JVM 規范和 JVM 的部分內部理論知識,以理論與實際結合的方式介紹對象初始化和類初始化之間的協作以及可能存在的沖突問題。

          問題引入

          近日我在調試一個枚舉類型的解析器程序,該解析器是將數據庫內一萬多條枚舉代碼裝載到緩存中,為了實現快速定位枚舉代碼和具體枚舉類別的所有枚舉元素,該類在裝載枚舉代碼的同時對其采取兩種策略建立內存索引。由于該類是一個公共服務類,在程序各個層面都會使用到它,因此我將它實現為一個單例類。這個類在我調整類實例化語句位置之前運行正常,但當我把該類實例化語句調整到靜態初始化語句之前時,我的程序不再為我工作了。

          下面是經過我簡化后的示例代碼:


          [清單一]
          package com.ccb.framework.enums;
                                  import java.util.Collections;
                                  import java.util.HashMap;
                                  import java.util.Map;
                                  public class CachingEnumResolver {
                                  //單態實例 一切問題皆由此行引起
                                  private static final CachingEnumResolver SINGLE_ENUM_RESOLVER = new
                                  CachingEnumResolver();
                                  /*MSGCODE->Category內存索引*/
                                  private static Map CODE_MAP_CACHE;
                                  static {
                                  CODE_MAP_CACHE = new HashMap();
                                  //為了說明問題,我在這里初始化一條數據
                                  CODE_MAP_CACHE.put("0","北京市");
                                  }
                                  //private, for single instance
                                  private CachingEnumResolver() {
                                  //初始化加載數據  引起問題,該方法也要負點責任
                                  initEnums();
                                  }
                                  /**
                                  * 初始化所有的枚舉類型
                                  */
                                  public static void initEnums() {
                                  // ~~~~~~~~~問題從這里開始暴露 ~~~~~~~~~~~//
                                  if (null == CODE_MAP_CACHE) {
                                  System.out.println("CODE_MAP_CACHE為空,問題在這里開始暴露.");
                                  CODE_MAP_CACHE = new HashMap();
                                  }
                                  CODE_MAP_CACHE.put("1", "北京市");
                                  CODE_MAP_CACHE.put("2", "云南省");
                                  //..... other code...
                                  }
                                  public Map getCache() {
                                  return Collections.unmodifiableMap(CODE_MAP_CACHE);
                                  }
                                  /**
                                  * 獲取單態實例
                                  *
                                  * @return
                                  */
                                  public static CachingEnumResolver getInstance() {
                                  return SINGLE_ENUM_RESOLVER;
                                  }
                                  public static void main(String[] args) {
                                  System.out.println(CachingEnumResolver.getInstance().getCache());
                                  }
                                  }
                                  

          想必大家看了上面的代碼后會感覺有些茫然,這個類看起來沒有問題啊,這的確屬于典型的餓漢式單態模式啊,怎么會有問題呢?

          是的,他看起來的確沒有問題,可是如果將他 run 起來時,其結果是他不會為你正確 work。運行該類,它的執行結果是:


          [清單二]
          CODE_MAP_CACHE為空,問題在這里開始暴露.
                                  {0=北京市}
                                  

          我的程序怎么會這樣?為什么在 initEnum() 方法里 CODE_MAP_CACHE 為空?為什么我輸出的 CODE_MAP_CACHE 內容只有一個元素,其它兩個元素呢????!!

          看到這里,如果是你在調試該程序,你此刻一定覺得很奇怪,難道是我的 Jvm 有問題嗎?非也!如果不是,那我的程序是怎么了?這絕對不是我想要的結果。可事實上無論怎么修改 initEnum() 方法都無濟于事,起碼我最初是一定不會懷疑到問題可能出在創建 CachingEnumResolver 實例這一環節上。正是因為我太相信我創建 CachingEnumResolver 實例的方法,加之對 Java 類初始化與對象實例化底層原理理解有所偏差,使我為此付出了三、四個小時--約半個工作日的大好青春。

          那么問題究竟出在哪里呢?為什么會出現這樣的怪事呢?在解決這個問題之前,先讓我們來了解一下JVM的類和對象初始化的底層機制。





          回頁首


          類的生命周期



          上圖展示的是類生命周期流向;在本文里,我只打算談談類的"初始化"以及"對象實例化"兩個階段。





          回頁首


          類初始化

          類"初始化"階段,它是一個類或接口被首次使用的前階段中的最后一項工作,本階段負責為類變量賦予正確的初始值。

          Java 編譯器把所有的類變量初始化語句和類型的靜態初始化器通通收集到 <clinit> 方法內,該方法只能被 Jvm 調用,專門承擔初始化工作。

          除接口以外,初始化一個類之前必須保證其直接超類已被初始化,并且該初始化過程是由 Jvm 保證線程安全的。另外,并非所有的類都會擁有一個 <clinit>() 方法,在以下條件中該類不會擁有 <clinit>() 方法:

          • 該類既沒有聲明任何類變量,也沒有靜態初始化語句;
          • 該類聲明了類變量,但沒有明確使用類變量初始化語句或靜態初始化語句初始化;
          • 該類僅包含靜態 final 變量的類變量初始化語句,并且類變量初始化語句是編譯時常量表達式。




          回頁首


          對象初始化

          在類被裝載、連接和初始化,這個類就隨時都可能使用了。對象實例化和初始化是就是對象生命的起始階段的活動,在這里我們主要討論對象的初始化工作的相關特點。

          Java 編譯器在編譯每個類時都會為該類至少生成一個實例初始化方法--即 "<init>()" 方法。此方法與源代碼中的每個構造方法相對應,如果類沒有明確地聲明任何構造方法,編譯器則為該類生成一個默認的無參構造方法,這個默認的構造器僅僅調用父類的無參構造器,與此同時也會生成一個與默認構造方法對應的 "<init>()" 方法.

          通常來說,<init>() 方法內包括的代碼內容大概為:調用另一個 <init>() 方法;對實例變量初始化;與其對應的構造方法內的代碼。

          如果構造方法是明確地從調用同一個類中的另一個構造方法開始,那它對應的 <init>() 方法體內包括的內容為:一個對本類的 <init>() 方法的調用;對應用構造方法內的所有字節碼。

          如果構造方法不是通過調用自身類的其它構造方法開始,并且該對象不是 Object 對象,那 <init>() 法內則包括的內容為:一個對父類 <init>() 方法的調用;對實例變量初始化方法的字節碼;最后是對應構造子的方法體字節碼。

          如果這個類是 Object,那么它的 <init>() 方法則不包括對父類 <init>() 方法的調用。





          回頁首


          類的初始化時機

          本文到目前為止,我們已經大概有了解到了類生命周期中都經歷了哪些階段,但這個類的生命周期的開始階段--類裝載又是在什么時候被觸發呢?類又是何時被初始化的呢?讓我們帶著這三個疑問繼續去尋找答案。

          Java 虛擬機規范為類的初始化時機做了嚴格定義:"initialize on first active use"--" 在首次主動使用時初始化"。這個規則直接影響著類裝載、連接和初始化類的機制--因為在類型被初始化之前它必須已經被連接,然而在連接之前又必須保證它已經被裝載了。

          在與初始化時機相關的類裝載時機問題上,Java 虛擬機規范并沒有對其做嚴格的定義,這就使得 JVM 在實現上可以根據自己的特點提供采用不同的裝載策略。我們可以思考一下 Jboss AOP 框架的實現原理,它就是在對你的 class 文件裝載環節做了手腳--插入了 AOP 的相關攔截字節碼,這使得它可以對程序員做到完全透明化,哪怕你用 new 操作符創建出的對象實例也一樣能被 AOP 框架攔截--與之相對應的 Spring AOP,你必須通過他的 BeanFactory 獲得被 AOP 代理過的受管對象,當然 Jboss AOP 的缺點也很明顯--他是和 JBOSS 服務器綁定很緊密的,你不能很輕松的移植到其它服務器上。嗯~……,說到這里有些跑題了,要知道 AOP 實現策略足可以寫一本厚厚的書了,嘿嘿,就此打住。

          說了這么多,類的初始化時機就是在"在首次主動使用時",那么,哪些情形下才符合首次主動使用的要求呢?

          首次主動使用的情形:

          • 創建某個類的新實例時--new、反射、克隆或反序列化;
          • 調用某個類的靜態方法時;
          • 使用某個類或接口的靜態字段或對該字段賦值時(final字段除外);
          • 調用Java的某些反射方法時
          • 初始化某個類的子類時
          • 在虛擬機啟動時某個含有main()方法的那個啟動類。

          除了以上幾種情形以外,所有其它使用JAVA類型的方式都是被動使用的,他們不會導致類的初始化。





          回頁首


          我的問題究竟出在哪里

          好了,了解了JVM的類初始化與對象初始化機制后,我們就有了理論基礎,也就可以理性的去分析問題了。

          下面讓我們來看看前面[清單一]的JAVA源代碼反組譯出的字節碼:


          [清單三]
          public class com.ccb.framework.enums.CachingEnumResolver extends
                                  java.lang.Object{
                                  static {};
                                  Code:
                                  0:	new	#2; //class CachingEnumResolver
                                  3:	dup
                                  4:	invokespecial	#14; //Method "<init>":()V  ①
                                  7:	putstatic	#16; //Field
                                  SINGLE_ENUM_RESOLVER:Lcom/ccb/framework/enums/CachingEnumResolver;
                                  10:	new	#18; //class HashMap              ②
                                  13:	dup
                                  14:	invokespecial	#19; //Method java/util/HashMap."<init>":()V
                                  17:	putstatic	#21; //Field CODE_MAP_CACHE:Ljava/util/Map;
                                  20:	getstatic	#21; //Field CODE_MAP_CACHE:Ljava/util/Map;
                                  23:	ldc	#23; //String 0
                                  25:	ldc	#25; //String 北京市
                                  27:	invokeinterface	#31,  3; //InterfaceMethod
                                  java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;   ③
                                  32:	pop
                                  33:	return
                                  private com.ccb.framework.enums.CachingEnumResolver();
                                  Code:
                                  0:	aload_0
                                  1:	invokespecial	#34; //Method java/lang/Object."<init>":()V
                                  4:	invokestatic	#37; //Method initEnums:()V                  ④
                                  7:	return
                                  public static void initEnums();
                                  Code:
                                  0:	getstatic	#21; //Field CODE_MAP_CACHE:Ljava/util/Map;    ⑤
                                  3:	ifnonnull	24
                                  6:	getstatic	#44; //Field java/lang/System.out:Ljava/io/PrintStream;
                                  9:	ldc	#46; //String CODE_MAP_CACHE為空,問題在這里開始暴露.
                                  11:	invokevirtual	#52; //Method
                                  java/io/PrintStream.println:(Ljava/lang/String;)V
                                  14:	new	#18; //class HashMap
                                  17:	dup
                                  18:	invokespecial	#19; //Method java/util/HashMap."<init>":()V      ⑥
                                  21:	putstatic	#21; //Field CODE_MAP_CACHE:Ljava/util/Map;
                                  24:	getstatic	#21; //Field CODE_MAP_CACHE:Ljava/util/Map;
                                  27:	ldc	#54; //String 1
                                  29:	ldc	#25; //String 北京市
                                  31:	invokeinterface	#31,  3; //InterfaceMethod
                                  java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;   ⑦
                                  36:	pop
                                  37:	getstatic	#21; //Field CODE_MAP_CACHE:Ljava/util/Map;
                                  40:	ldc	#56; //String 2
                                  42:	ldc	#58; //String 云南省
                                  44:	invokeinterface	#31,  3; //InterfaceMethod
                                  java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;    ⑧
                                  49:	pop
                                  50:	return
                                  public java.util.Map getCache();
                                  Code:
                                  0:	getstatic	#21; //Field CODE_MAP_CACHE:Ljava/util/Map;
                                  3:	invokestatic	#66; //Method
                                  java/util/Collections.unmodifiableMap:(Ljava/util/Map;)Ljava/util/Map;
                                  6:	areturn
                                  public static com.ccb.framework.enums.CachingEnumResolver getInstance();
                                  Code:
                                  0:	getstatic	#16;
                                  //Field SINGLE_ENUM_RESOLVER:Lcom/ccb/framework/enums/CachingEnumResolver;   ⑨
                                  3:	areturn
                                  }
                                  

          如果上面[清單一]顯示,清單內容是在 JDK1.4 環境下的字節碼內容,可能這份清單對于很大部分兄弟來說確實沒有多少吸引力,因為這些 JVM 指令確實不像源代碼那樣漂亮易懂。但它的的確確是查找和定位問題最直接的辦法,我們想要的答案就在這份 JVM 指令清單里。

          現在,讓我們對該類從類初始化到對象實例初始化全過程分析[清單一]中的代碼執行軌跡。

          如前面所述,類初始化是在類真正可用時的最后一項前階工作,該階段負責對所有類正確的初始化值,此項工作是線程安全的,JVM會保證多線程同步。

          第1步:調用類初始化方法 CachingEnumResolver.<clinit>(),該方法對外界是不可見的,換句話說是 JVM 內部專用方法,<clinit>() 內包括了 CachingEnumResolver 內所有的具有指定初始值的類變量的初始化語句。要注意的是并非每個類都具有該方法,具體的內容在前面已有敘述。

          第2步:進入 <clinit>() 方法內,讓我們看字節碼中的 "①" 行,該行與其上面兩行組合起來代表 new 一個 CachingEnumResolver 對象實例,而該代碼行本身是指調用 CachingEnumResolver 類的 <init>()方法。每一個 Java 類都具有一個 <init>() 方法,該方法是 Java 編譯器在編譯時生成的,對外界不可見,<init>() 方法內包括了所有具有指定初始化值的實例變量初始化語句和java類的構造方法內的所有語句。對象在實例化時,均通過該方法進行初始化。然而到此步,一個潛在的問題已經在此埋伏好,就等著你來犯了。

          第3步:讓我們順著執行順序向下看,"④" 行,該行所在方法就是該類的構造器,該方法先調用父類的構造器 <init>() 對父對象進行初始化,然后調用 CachingEnumResolver.initEnum() 方法加載數據。

          第4步:"⑤" 行,該行獲取 "CODE_MAP_CACHE" 字段值,其運行時該字段值為 null。注意,問題已經開始顯現了。(作為程序員的你一定是希望該字段已經被初始化過了,而事實上它還沒有被初始化)。通過判斷,由于該字段為 NULL,因此程序將繼續執行到 "⑥" 行,將該字段實例化為 HashMap()。

          第5步:在 "⑦"、"⑧" 行,其功能就是為 "CODE_MAP_CACHE" 字段填入兩條數據。

          第6步:退出對象初始化方法 <init>(),將生成的對象實例初始化給類字段 "SINGLE_ENUM_RESOLVER"。(注意,此刻該對象實例內的類變量還未初始化完全,剛才由 <init>() 調用 initEnum() 方法賦值的類變量 "CODE_MAP_CACHE" 是 <clinit>() 方法還未初始化字段,它還將在后面的類初始化過程再次被覆蓋)。

          第7步:繼續執行 <clinit>()方法內的后繼代碼,"②" 行,該行對 "CODE_MAP_CACHE" 字段實例化為 HashMap 實例(注意:在對象實例化時已經對該字段賦值過了,現在又重新賦值為另一個實例,此刻,"CODE_MAP_CACHE"變量所引用的實例的類變量值被覆蓋,到此我們的疑問已經有了答案)。

          第8步:類初始化完畢,同時該單態類的實例化工作也完成。

          通過對上面的字節碼執行過程分析,或許你已經清楚了解到導致錯誤的深層原因了,也或許你可能早已被上面的分析過程給弄得暈頭轉向了,不過也沒折,雖然我也可以從源代碼的角度來闡述問題,但這樣不夠深度,同時也會有僅為個人觀點、不足可信之嫌。





          回頁首


          如何解決

          要解決上面代碼所存在的問題很簡單,那就是將 "SINGLE_ENUM_RESOLVER" 變量的初始化賦值語句轉移到 getInstance() 方法中去即可。換句話說就是要避免在類還未初始化完成時從內部實例化該類或在初始化過程中引用還未初始化的字段。





          回頁首


          寫在最后

          靜下浮燥之心,仔細思量自己是否真的掌握了本文主題所引出的知識,如果您覺得您已經完全或基本掌握了,那么很好,在最后,我將前面的代碼稍做下修改,請思考下面兩組程序是否同樣會存在問題呢?


          程序一
          	public class CachingEnumResolver {
                                  public  static Map CODE_MAP_CACHE;
                                  static {
                                  CODE_MAP_CACHE = new HashMap();
                                  //為了說明問題,我在這里初始化一條數據
                                  CODE_MAP_CACHE.put("0","北京市");
                                  initEnums();
                                  }
                                  


          程序二
          	public class CachingEnumResolver {
                                  private static final CachingEnumResolver SINGLE_ENUM_RESOLVER;
                                  public  static Map CODE_MAP_CACHE;
                                  static {
                                  CODE_MAP_CACHE = new HashMap();
                                  //為了說明問題,我在這里初始化一條數據
                                  CODE_MAP_CACHE.put("0","北京市");
                                  SINGLE_ENUM_RESOLVER = new CachingEnumResolver();
                                  initEnums();
                                  }
                                  

          最后,一點關于 JAVA 群體的感言:時下正是各種開源框架盛行時期,Spring 更是大行其道,吸引著一大批 JEE 開發者的眼球(我也是 fans 中的一員)。然而,讓我們仔細觀察一下--以 Spring 群體為例,在那么多的 Spring fans 當中,有多少人去研究過 Spring 源代碼?又有多少人對 Spring 設計思想有真正深入了解呢?當然,我是沒有資格以這樣的口吻來說事的,我只是想表明一個觀點--學東西一定要"正本清源"。

          獻上此文,謹以共勉。



          關于作者

           

          北京高偉達西南分軟 Java EE 軟件工程師,三年 Java EE 項目經驗,行業方向為銀行 OCRM 系統。對 JAVA 有著濃厚的興趣,業余研究 AOP/ESB 方向。


          posted @ 2009-12-16 22:48 ammay 閱讀(700) | 評論 (0)編輯 收藏

          泛型

          編者注:在從Java in a Nutshell,5th edith中摘錄的兩部部分中的第一部分,David Flanagan描述了如何使用泛型。這部分,David Flanagan將具體告訴你如何創建自己的泛型和泛型方法,并且以Java核心API很多重要的泛型作為結束總結。

          創建泛型和泛型方法
          創建一個簡單的泛型是非常容易的。首先,在一對尖括號(< >)中聲明類型變量,以逗號間隔變量名列表。在類的實例變量和方法中,可以在任何類型的地方使用那些類型變量。切記,類型變量僅在編譯時存在,所以不能使用instanceof和new這類運行時操作符來操作類型變量。

          讓我們以一個簡單的例子來開始這部分的學習,而后將精簡這個例子。這段代碼定義了一個樹形數據結構,使用類型變量V代表存儲在各個樹結點中的值。

          import java.util.*;
          /**
          * A tree is a data structure that holds values of type V.
          * Each tree has a single value of type V and can have any number of
          * branches, each of which is itself a Tree.
          */
          public class Tree {
          // The value of the tree is of type V.
          V value;

          // A Tree can have branches, each of which is also a Tree

          List<tree branches = new ArrayList<tree();

          // Here's the constructor. Note the use of the type variable V.
          public Tree(V value) { this.value = value; }

          // These are instance methods for manipulating the node value and branches.
          // Note the use of the type variable V in the arguments or return types.
          V getValue() { return value; }
          void setValue(V value) { this.value = value; }
          int getNumBranches() { return branches.size(); }
          Tree getBranch(int n) { return branches.get(n); }
          void addBranch(Tree branch) { branches.add(branch); }
          }


          正如你所看到的,命名一個類型變量習慣于一個大寫字母。使用一個字母可以同現實中那些具有描述性的,長的實際變量名有所區別。使用大寫字母要同變量命名規則一致,并且要區別于局部變量,方法參數,成員變量,而這些變量常常使用一個小寫字母。集合類中,比如java.util中常常使用類型變量E代表“Element type”。T和S常常用來表示范型變量名(好像使用i和j作為循環變量一樣)。

          注意到,當一個變量被聲明為泛型時,只能被實例變量和方法調用(還有內嵌類型)而不能被靜態變量和方法調用。原因很簡單,參數化的泛型是一些實例。靜態成員是被類的實例和參數化的類所共享的,所以靜態成員不應該有類型參數和他們關聯。方法,包括靜態方法,可以聲明和使用他們自己的類型參數,但是,調用這樣一個方法,可以被不同地參數化。這些內容將在本章后面談到。

          類型變量綁定
          上面例子中的Tree中的類型變量V是不受約束的:Tree可以被參數化為任何類型。以前我們常常會設置一些約束條件在需要使用的類型上:也許我們需要強制一個類型參數實現一個或多個接口,或是一個特定類的子類。這可以通過指明類型綁定來完成。我們已經看到了統配符的上界,而且使用簡單的語法可以指定一般類型變量的上界。后面的代碼,還是使用Tree這個例子,并且通過實現Serializable和Comparable來重寫。為了做到這點,例子中使用類型變量綁定來確保值類型的Serializable和Comparable。

          import java.io.Serializable;
          import java.util.*;

          public class Tree>
          implements Serializable, Comparable<tree

          {
          V value;
          List<tree branches = new ArrayList<tree();

          public Tree(V value) { this.value = value; }

          // Instance methods
          V getValue() { return value; }
          void setValue(V value) { this.value = value; }
          int getNumBranches() { return branches.size(); }
          Tree getBranch(int n) { return branches.get(n); }
          void addBranch(Tree branch) { branches.add(branch); }

          // This method is a nonrecursive implementation of Comparable<tree

          // It only compares the value of this node and ignores branches.
          public int compareTo(Tree that) {
          if (this.value == null && that.value == null) return 0;
          if (this.value == null) return -1;
          if (that.value == null) return 1;
          return this.value.compareTo(that.value);
          }

          // javac -Xlint warns us if we omit this field in a Serializable class
          private static final long serialVersionUID = 833546143621133467L;
          }


          一個類型變量的綁定是通過extends后的名字和一個類型列表(這可以是參數化的,就像Comparable一樣)表達的。注意當有不止一個綁定時,就像上面例子中的,綁定的類型要用&作為分隔符,而不是使用逗號。都后用來分隔類型變量,如果用來分隔類型變量綁定,就會模棱兩可。一個類型變量可以有任何數量的綁定,包括任何數量的借口和至多一個類。

          范型中的通配符
          上一章的例子中我們看到了通配符和控制參數化類型的通配符綁定。這些在范型中同樣非常有用。當前設計的Tree要求每個節點有相同類型的值,V。也許這樣太嚴格了,也許我們應該讓Tree的branches能夠存放V的子類而不全是V。這個版本的Tree(刪除了Comparable和Serializable接口的實現)這樣做會更靈活。

          public class Tree {
          // These fields hold the value and the branches
          V value;
          List<tree<? extends V>> branches = new ArrayList<tree<? extends V>>();

          // Here's a constructor
          public Tree(V value) { this.value = value; }

          // These are instance methods for manipulating value and branches
          V getValue() { return value; }
          void setValue(V value) { this.value = value; }
          int getNumBranches() { return branches.size(); }
          Tree<? extends V> getBranch(int n) { return branches.get(n); }
          void addBranch(Tree<? extends V> branch) { branches.add(branch); }
          }


          通配符綁定允許我們在枝節點上增加一個Tree,比如,一個樹枝Tree
          Tree t = new Tree(0); // Note autoboxing
          t.addBranch(new Tree(1)); // int 1 autoboxed to Integer


          通過getBranch()查詢樹枝,而樹枝的返回類型不知道,所以必須使用統配符來表達。接下來的兩個是合法的,但第三個不是:
          Tree<? extends Number> b = t.getBranch(0);
          Tree<?> b2 = t.getBranch(0);
          Tree b3 = t.getBranch(0); // compilation error


          當我們這樣來查詢一個樹枝時,不能精確確定它的返回類型,但是存在類型的上限,所以,我們可以這樣做:
          Tree<? extends Number> b = t.getBranch(0);
          Number value = b.getValue();


          那我們不能做什么呢?設定樹枝的值,或者在原有的樹枝上添加新的樹枝。早前章節解釋的,上界的存在不會改變返回值的類型不可知,編譯器沒有足夠的信息讓我們安全的給setValue()或者一個樹枝(包括值類型)的addBranch()傳遞一個值。下面的兩行代碼都是非法的:
          b.setValue(3.0); // Illegal, value type is unknown
          b.addBranch(new Tree(Math.PI));


          這個例子在設計時找到了一個平衡點:使用綁定通配符使得數據結構更加靈活,但是減少了安全使用其中方法的可能。這個設計是好是壞就要根據上下文聯系了。通常,好的范型設計是非常困難的。幸運的是,大多我們要使用的已經在java.util包中設計好了,而不用我們自己再去設計。

          范型方法
            正如前面說的,范型只能被實例成員調用,而不是靜態成員。同實例方法一樣,靜態方法也可以使用通配符。盡管靜態方法不能使用包含他們的類中的類型變量,但是他們可以聲明自己的類型變量。當一個方法聲明了自己的類型變量,就叫做范型方法。

          這里有一個要添加到Tree中的靜態方法。他不是一個范型方法,但是使用了綁定的通配符,就好像先前我們看到的sumList()一樣:
          /** Recursively compute the sum of the values of all nodes on the tree */
          public static double sum(Tree<? extends Number> t) {
          double total = t.value.doubleValue();
          for(Tree<? extends Number> b : t.branches) total += sum(b);
          return total;
          }


            通過通配符的上界綁定,聲明自己的類型變量來重寫這個方法:
          public static  double sum(Tree t) {
          N value = t.value;
          double total = value.doubleValue();
          for(Tree<? extends N> b : t.branches) total += sum(b);
          return total;
          }


            范型的sum()不比通配符版本的簡單,而且聲明變量并沒有讓我們獲得什么。這種情況下,通配符方案要比范型方法更有效,當一個類型變量用來表達兩個參數之間或者參數和返回值之間的關系時,范型方法才是需要的。請看下面的例子:
          // This method returns the largest of two trees, where tree size
          // is computed by the sum() method. The type variable ensures that
          // both trees have the same value type and that both can be passed to sum().
          public static Tree max(Tree t, Tree u) {
          double ts = sum(t);
          double us = sum(u);
          if (ts > us) return t;
          else return u;
          }
            

          這個方法使用類型變量N來約束參數和返回值有相同類型,并且參數是Number或者他的子類。

            使得參數具有相同類型也許是有爭議的,應該讓我們能調用max()不論是Tree或者Tree。一種方法是使用兩個不相干的類型變量來表示兩個不相干的值類型。注意,我們不能在方法的返回時使用變量而必須使用通配符:
          public static 
          Tree<? extends Number> max(Tree t, Tree u) {...}


            既然兩個類型變量N和M沒有任何聯系,而且每個僅在簽名的時候使用,他們沒有提供比通配符更多的好處,這種方法最好這樣寫:
          public static Tree<? extends Number> max(Tree<? extends Number> t,
          Tree<? extends Number> u) {...}


          所有在這里的范型方法都是靜態的,這并不是必須的,實例方法也可以聲明自己的類型變量。

          調用范型方法
          當你使用范型時,必須指定實際類型參數來代替相應的類型變量。但這些對范型方法有些不同:編譯器總是能計算出基于你所傳遞的參數的相應范型方法參數。考慮一下上面定義的max(),作為例子:
          public static  Tree max(Tree t, Tree u) {...}


          當你調用這個方法時,不需要指明N,因為N是隱含地由t和u指明。在后面的代碼中,編譯器決定N為Integer:

          Tree x = new Tree(1);
          Tree y = new Tree(2);
          Tree z = Tree.max(x, y);


          編譯器判斷范型方法的參數類型稱為類型推斷。類型推斷是相對于知覺推斷的。而實際編譯器的實現方法是一種非常復雜的過程,超過了這本書的討論范圍。更多的細節在The Java Language Specification, Third Edition的第十五章。
          讓我們看一個更加復雜的類型推斷,考慮一下這個方法:

          public class Util {
          /** Set all elements of a to the value v; return a. */
          public static T[] fill(T[] a, T v) {
          for(int i = 0; i < a.length; i++) a[i] = v;
          return a;
          }
          }


          這里有兩個該方法的調用:

          Boolean[] booleans = Util.fill(new Boolean[100], Boolean.TRUE);
          Object o = Util.fill(new Number[5], new Integer(42));


          在第一個例子中,編譯器可以輕松的推斷出T是Boolean類型,第二個例子中,編譯器判斷T是Number。
          在非常罕見的情況下,你可能會顯示的指明范型方法的參數類型。有時候這是必要的,比如,當范型方法不需要參數時。考慮一下java.util.Collections.emptySet():返回一個空集合,但是不同于Collections.singleton()(可以在參考部分察看),他不帶任何參數,但需要指明返回類型。通過在方法名前的<>中,可以顯示的指明參數類型:

          Set empty = Collections.emptySet();


            類型參數不能同沒有限制的方法名結合使用:他們必須跟隨在一個.后或者在關鍵字new后,或者在關鍵字this前,或者構造函數的super前。
          可以證明,如果如果你將Collections.emptySet()的返回值賦給一個變量,就像我們上邊通過類型推斷機制推斷基于變量類型的參數類型。盡管顯示的類型說明可以更加清楚,但這不是必要的,可以像下面一樣重寫:

          Set empty = Collections.emptySet();


            在方法調用表達式中,顯示的說明emptySet()的返回值類型是必要的。比如,假設你要調用一個名為printWords()的方法,該方法僅需一個Set的參數,如果你想傳遞一個空的集合給該方法,就要像下面一樣寫:
          printWords(Collections.emptySet());


          這種情況下,顯示的類型說明是必要的。

          范型方法和數組
          早先我們看到,編譯器不允許創建一個類型參數化的數組。但是對于范型的使用會是不同的。考慮一下前面定義的Util.fill(),它得以第一個參數和返回值類型都是T[]。而方法體內不必創建任何參數為T的數組,所以這個方法是合法的。

          如果你創建一個方法使用varargs(參見第二章的2.6.4)和類型變量,記住調用varargs隱含創建一個數組,請看下面的例子:

          /** Return the largest of the specified values or null if there are none */
          public static > T max(T... values) { ... }


          你可以使用一個Integer類型來調用這個方法,因為編譯器會在調用的時候插入必要的數組創建代碼。但是你不能將參數轉換為Comparable來調用這個方法,因為創建一個Comparable[]是不合法的。

          參數化異常
          異常是在運行時拋出和捕獲的。沒有辦法讓編譯器完成類型檢查,來保證在catch塊中拋出的未知的類型匹配異常。由于這個原因,catch塊很可能不包含類型變量和通配符。既然不可能保證在運行時捕獲一個編譯器時類型參數完整性異常,所以不允許創建任何Throwable類型的子類。參數化異常是不允許的。
          但是你可以使用類型變量在throw塊里的方法簽名中。看看下面的例子:

          public interface Command {
          public void doit(String arg) throws X;
          }


          這個接口描述了一個“command”:一塊代碼只有一個String類型的參數,沒有返回值。代碼可能拋出一個類型為X的異常。這里有一個例子使用這個接口:

          Command save = new Command() {
          public void doit(String filename) throws IOException {
          PrintWriter out = new PrintWriter(new FileWriter(filename));
          out.println("hello world");
          out.close();
          }
          };

          try { save.doit("/tmp/foo"); }
          catch(IOException e) { System.out.println(e); }



          范型個案研究:比較和枚舉
          Java1.5引入的范型新特性,在1.5的API中有使用,特別多的是在java.util包中,但是在java.lang,java.lang.reflect和java.util.concurrent中也有。這些API都是經過仔細的斟酌創建的,通過學習這些API我們可以學到很多好的設計方法。

          java.util中的范形是比較簡單的:因為大多都是集合類,類型變量也是代表集合中的元素。java.lang中的幾個重要范型是比較難以理解的,他們不是集合,而且第一眼很不容易理解為什么設計成范型。學習這些范型可以讓我們更深層次的理解范形的工作機制,并且介紹一些我們沒有提到的概念。特別的,我們要檢查Comparable接口和Enum類(枚舉類型的超類,后面一張講解)并且學習一些重要但是很少使用的范型特性,比如通配符下界。

          在java1.5中,Comparable接口被修改為范型的。大多數的類都實現了這個接口,考慮一下Integer:

          public final class Integer extends Number implements Comparable


          原先的Comparable接口在類型安全方面是有問題的。兩個繼承了Comparable接口的對象可能不能相互比較。JDK5.0前,非范形的Comparable接口是非常有用但是不安全的,而現在的接口,捕獲了我們需要的信息:他告訴我們一個對象是可比較的,并且可以同什么比較。

          現在,考慮一下comparable類的子類。Integer是final的,所以不能有子類,那么讓我們看看java.math.BigInteger:

          public class BigInteger extends Number implements Comparable


          如果我們實現一個BiggerInteger類是BigInteger的子類,他從父類那里繼承了Comparable接口,但是注意繼承的是Comparable而不是Comparable。這意味著BigInteger和BiggerInteger是可以相互比較的,這是非常好的。BiggerInteger可以重載compareTo(),但是不允許實現一個不同的參數化的Comparable。這就是說BiggerInteger不能同時繼承BigInteger和實現Comparable

          當你使用可比較的對象時(當寫排序算法的時候)記住兩點。首先,使用原生類型是不夠充分的:考慮到類型安全,必須指明同什么比較。接下來,類型是不允許同自己比較的:有時候他會同他的祖先比較。為了具體說明,考慮java.util.Collections.max():

          這是一個冗長而且復雜的方法標簽,我們來一步步考慮:
          方法中包含一個類型變量T,并且有復雜的綁定,稍后我們返回來討論。
          方法的返回值類型是T。
          方法名是max()。

          方法的參數是一個集合。元素的類型指定為綁定的通配符。我們并不知道元素的確切類型,但直到有一個上限T。所以我們知道元素的類型要么為T,要么是T的子類。集合的任何元素都可以作為返回值使用。

          這些是比較簡單的,本章我們已經看到了通配符上界,我們再來看看max()中的類型變量聲明:

          >


          要說明的第一點,T必須實現了Comparable接口。(范型的語法使用關鍵字extends來代表類型綁定,不論是類或接口)這是期望的,因為這個方法是找到集合中最大的元素。但是觀察這個參數化的Comparable接口,這是一個通配符,但是這個通過關鍵字super來綁定,而不是extends。這是下界綁定。? extends T是我們熟悉的上界綁定:這意味著T或者其子類。? super T比較少用:這意味著T或者他的超類。

          總結一下,類型變量聲明表明:“T是一個實現了Comparable接口或者他的父類實現了該接口的類型。”Collections.min()和Collections.binarySearch()有著相同的聲明。
          對其他的下界通配符(對于Comparable接口沒有作用)的例子,Collections中的addAll(),copy(),和fill()。觀察addAll()的聲明:

          public static  boolean addAll(Collection<? super T> c, T... a)


          這是一個varargs方法,接受任意數量的參數,并且傳遞給他們一個T[],命名為a。他將a中的所有元素都賦給集合c。集合的元素類型雖然不知道,但是有一個下界:元素均為T或者T的超類。不論類型是什么,我們可以確定數組的元素都是類型的實例,所以將數組的元素添加到集合中是合法的。

          返回到我們先前討論的上界通配符,如果有一個集合的元素是上界通配符,那么都是只讀的。考慮List<? extends Serializable>。我們知道,所有的元素都是Serializable,所以像get()這樣的方法返回一個Serializable類型的返回值。編譯器不允許我們調用add()這樣的方法,因為實際的元素類型是不可知的。不能夠添加絕對的Serializable對象到list中,因為實現他們的類可能不是正確的類型。

          既然上界統配符的結果是只讀的,所以你可能會期望下界通配符來實現只寫的集合。實際并不是這樣,假設這里有一個List<? extends Integer>。元素的實際類型是不知道的,但是可能性是Integer或者他的祖先類Number和Object。無論實際類型是什么,將Integer類型(而不是Number和Object對象)的元素添加到list中是安全的。無論實際類型是什么,list中所有元素都是Object對象的實例,所以list中像get()一樣的方法返回Object。

          最后,讓我們把注意力放到java.lang.Enum類。Enum是所有枚舉類型的父類,它實現了Comparable接口,但是有一個讓人迷惑的范型聲明方法:

          public class Enum> implements Comparable, Serializable


          第一眼,類型變量E的聲明在一個循環中。再仔細的看一看:聲明真正說明了,Enum必須是一個本身就是Enum類型的類型。這種表面上的循環是很顯然的,如果我們看到了implements子句。正如我們看到的,Comparable類通常被定義為可以同自己比較的。而且他們的子類也可以同他們的父類比較。從另一個方面將,Enum實現了Comparable接口不是為了他本身,而是為了他的子類E。


          資源:
          ·Onjava.com:Onjava.com
          ·Matrix-Java開發者社區:http://www.matrix.org.cn/


          Java, java, J2SE, j2se, J2EE, j2ee, J2ME, j2me, ejb, ejb3, JBOSS, jboss, spring, hibernate, jdo, struts, webwork, ajax, AJAX, mysql, MySQL, Oracle, Weblogic, Websphere, scjp, scjd
          Java中的泛型 第二部分

          posted @ 2009-12-14 22:55 ammay 閱讀(249) | 評論 (0)編輯 收藏

          泛型類型

          泛型類型,第一部分

          作者: David Flanagan

          翻譯:cat


          版權聲明:可以任意轉載,轉載時請務必以超鏈接形式標明文章原始出處和作者信息及本聲明
          作者:
          David Flanagan;cat
          原文地址:
          http://www.onjava.com/pub/a/onjava/excerpt/javaian5_chap04/index.html
          中文地址:
          http://www.matrix.org.cn/resource/article/43/43864_Generic_Types.html
          關鍵詞: Generic Types


          編輯按:《Java in a Nutshell, 5th Edition》覆蓋了jdk5.0中很多變化和新特征,其中最重要的就是泛型。在本文的第一部分,作者David Flanagan介紹了如何使用泛型;而在第二部分,作者描述了如何寫你自己的泛型和泛型方法。

          Java5.0的新特性之一是引入了泛型類型和泛型方法。一個泛型類型通過使用一個或多個類型變量來定義,并擁有一個或多個使用一個類型變量作為一個參數或者返回值的占位符。例如,類型java.util.List<E>是一個泛型類型:一個list,其元素的類型被占位符E描述。這個類型有一個名為add()的方法,被聲明為有一個類型為E的參數,同時,有一個get()方法,返回值被聲明為E類型。

          為了使用泛型類型,你應該為類型變量詳細指明實際的類型,形成一個就像List<String>類似的參數化類型。[1]指明這些額外的類型信息的原因是編譯器據此能夠在編譯期為您提供很強的類型檢查,增強您的程序的類型安全性。舉個例子來說,您有一個只能保持String對象的List,那么這種類型檢查就能夠阻止您往里面加入String[]對象。同樣的,增加的類型信息使編譯器能夠為您做一些類型轉換的事情。比如,編譯器知道了一個List<String>有個get()方法,其返回值是一個String對象,因此您不再需要去將返回值由一個Object強制轉換為String。

          Java.util包中的集合類在java5.0中已經被做成了泛型,也許您將會在您的程序中頻繁的使用到他們。類型安全的集合類就是一個泛型類型的典型案例。即便您從沒有定義過您自己的泛型類型甚至從未用過除了java.util中的集合類以外的泛型類型,類型安全的集合類的好處也是極有意義的一個標志——他們證明了這個主要的新語言特性的復雜性。

          我們從探索類型安全的集合類中的基本的泛型用法開始,進而研究更多使用泛型類型的復雜細節。然后我們討論類型參數通配符和有界通配符。描繪了如何使用泛型以后,我們闡明如何編寫自己的泛型類型和泛型方法。我們對于泛型的討論將結束于一趟對于JavaAPI的核心中重要的泛型類型的旅行。這趟旅程將探索這些類型以及他們的用法,旅程的目的是為了讓您對泛型如何工作這個問題有個深入的理解。

          類型安全集合類

          Java.util類包包含了Java集合框架(Java Collections Framework),這是一批包含對象的set、對象的list以及基于key-value的map。第五章將談到集合類。這里,我們討論的是在java5.0中集合類使用類型參數來界定集合中的對象的類型。這個討論并不適合java1.4或更早期版本。如果沒有泛型,對于集合類的使用需要程序員記住每個集合中元素的類型。當您在java1.4種創建了一個集合,您知道您放入到集合中的對象的類型,但是編譯器不知道。您必須小心地往其中加入一個合適類型的元素,當需要從集合中獲取元素時,您必須顯式的寫強制類型轉換以將他們從Object轉換為他們真是的類型。考察下邊的java1.4的代碼。

          public static void main(String[] args) {
              // This list is intended to hold only strings.
              // The compiler doesn't know that so we have to remember ourselves.
              List wordlist = new ArrayList();  

              // Oops! We added a String[] instead of a String.
              // The compiler doesn't know that this is an error.
              wordlist.add(args);

              // Since List can hold arbitrary objects, the get() method returns
              // Object.  Since the list is intended to hold strings, we cast the
              // return value to String but get a ClassCastException because of
              // the error above.
              String word = (String)wordlist.get(0);
          }


          泛型類型解決了這段代碼中的顯示的類型安全問題。Java.util中的List或是其他集合類已經使用泛型重寫過了。就像前面提到的, List被重新定義為一個list,它中間的元素類型被一個類型可變的名稱為E的占位符描述。Add()方法被重新定義為期望一個類型為E的參數,用于替換以前的Object,get()方法被重新定義為返回一個E,替換了以前的Object。

          在java5.0中,當我們申明一個List或者創建一個ArrayList的實例的時候,我們需要在泛型類型的名字后面緊跟一對“<>”,尖括號中寫入我們需要的實際的類型。比如,一個保持String的List應該寫成“List<String>”。需要注意的是,這非常象給一個方法傳一個參數,區別是我們使用類型而不是值,同時使用尖括號而不是圓括號

          Java.util的集合類中的元素必須是對象化的,他們不能是基本類型。泛型的引入并沒有改變這點。泛型不能使用基本類型:我們不能這樣來申明——Set<char>或者List<int>。記住,無論如何,java5.0中的自動打包和自動解包特性使得使用Set<Character>或者List<Integer>和直接使用char和int值一樣方便。(查看第二章以了解更多關于自動打包和自動解包的細節)。

          在Java5.0中,上面的例子將被重寫為如下方式:

          public static void main(String[] args) {
              // This list can only hold String objects
              List<String> wordlist = new ArrayList<String>();

              // args is a String[], not String, so the compiler won't let us do this
              wordlist.add(args);  // Compilation error!

              // We can do this, though.  
              // Notice the use of the new for/in looping statement
              for(String arg : args) wordlist.add(arg);

              // No cast is required.  List<String>.get() returns a String.
              String word = wordlist.get(0);
          }


          值得注意的是代碼量其實并沒有比原來那個沒有泛型的例子少多少。使用“(String)”這樣的類型轉換被替換成了類型參數“<String>”。 不同的是類型參數需要且僅需要聲明一次,而list能夠被使用任何多次,不需要類型轉換。在更長點的例子代碼中,這一點將更加明顯。即使在那些看上去泛型語法比非泛型語法要冗長的例子里,使用泛型依然是非常有價值的——額外的類型信息允許編譯器在您的代碼里執行更強的錯誤檢查。以前只能在運行起才能發現的錯誤現在能夠在編譯時就被發現。此外,以前為了處理類型轉換的異常,我們需要添加額外的代碼行。如果沒有泛型,那么當發生類型轉換異常的時候,一個ClassCastException異常就會被從實際代碼中拋出。

          就像一個方法可以使用任意數量的參數一樣,類允許使用多個類型變量。接口Java.util.Map就是一個例子。一個Map體現了從一個key的對象到一個value的對象的映射關系。接口Map申明了一個類型變量來描述key的類型而另一個類型變量來描述value的類型。舉個例子來說,假設您希望做一個String對象到Integer對象的映射關系:

          public static void main(String[] args) {
              // A map from strings to their position in the args[] array
              Map<String,Integer> map = new HashMap<String,Integer>();

              // Note that we use autoboxing to wrap i in an Integer object.
              for(int i=0; i < args.length; i++) map.put(args[i], i);  

              // Find the array index of a word.  Note no cast is required!
              Integer position = map.get("hello");

              // We can also rely on autounboxing to convert directly to an int,
              // but this throws a NullPointerException if the key does not exist
              // in the map
              int pos = map.get("world");
          }


          象List<String>這個一個參數類型其本身也是也一個類型,也能夠被用于當作其他類型的一個類型變量值。您可能會看到這樣的代碼:

          // Look at all those nested angle brackets!
          Map<String, List<List<int[]>>> map = getWeirdMap();

          // The compiler knows all the types and we can write expressions
          // like this without casting.  We might still get NullPointerException
          // or ArrayIndexOutOfBounds at runtime, of course.
          int value = map.get(key).get(0).get(0)[0];

          // Here's how we break that expression down step by step.
          List<List<int[]>> listOfLists = map.get(key);
          List<int[]> listOfIntArrays = listOfLists.get(0);
          int[] array = listOfIntArrays.get(0);
          int element = array[0];


          在上面的代碼里,java.util.List<E>和java.util.Map<K,V>的get()方法返回一個類型為E的list元素或者一個類型為V的map元素。注意,無論如何,泛型類型能夠更精密的使用他們的變量。在本書中的參考章節查看List<E>,您將會看到它的iterator( )方法被聲明為返回一個Iterator<E>。這意味著,這個方法返回一個跟list的實際的參數類型一樣的一個參數類型的實例。為了具體的說明這點,下面的例子提供了不使用get(0)方法來獲取一個List<String>的第一個元素的方法。

          List<String> words = // ...initialized elsewhere...
          Iterator<String> iterator = words.iterator();
          String firstword = iterator.next();


          理解泛型類型

          本段將對泛型類型的使用細節做進一步的探討,以嘗試說明下列問題:
          不帶類型參數的使用泛型的后果
          參數化類型的體系
          一個關于編譯期泛型類型的類型安全的漏洞和一個用于確保運行期類型安全的補丁
          為什么參數化類型的數組不是類型安全的

          未經處理的類型和不被檢查的警告
          即使被重寫的Java集合類帶來了泛型的好處,在使用他們的時候您也不被要求說明類型變量。一個不帶類型變量的泛型類型被認為是一個未經處理的類型(raw type)。這樣,5.0版本以前的java代碼仍然能夠運行:您顯式的編寫所有類型轉換就像您已經這樣寫的一樣,您可能會被一些來自編譯器的麻煩所困擾。查看下列存儲不同類型的對象到一個未經處理的List:

          List l = new ArrayList();
          l.add("hello");  
          l.add(new Integer(123));
          Object o = l.get(0);


          這段代碼在java1.4下運行得很好。如果您用java5.0來編譯它,javac編譯了,但是會打印出這樣的“抱怨”:

          Note: Test.java uses unchecked or unsafe operations.
          Note: Recompile with -Xlint:unchecked for details.


          如果我們加入-Xlint參數后重新編譯,我們會看到這些警告:

          Test.java:6: warning: [unchecked]
              unchecked call to add(E) as a member of the raw type java.util.List
                  l.add("hello");  
                   ^
          Test.java:7: warning: [unchecked]
              unchecked call to add(E) as a member of the raw type java.util.List
                  l.add(new Integer(123));

                   ^
          編譯在add()方法的調用上給出了警告,因為它不能夠確信加入到list中的值具有正確的類型。它告訴我們說我們使用了一個未經處理的類型,它不能驗證我們的代碼是類型安全的。注意,get()方法的調用是沒有問題的,因為能夠被獲得的元素已經安全的存在于list中了。

          如果您不想使用任何的java5.0的新特性,您可以簡單的通過帶-source1.4標記來編譯他們,這樣編譯器就不會再“抱怨”了。如果您不能這樣做,您可以忽略這些警告,通過使用一個“@SuppressWarnings("unchecked")”注解(查看本章的4.3節)隱瞞這些警告信息或者升級您的代碼,加入類型變量描述。[2]下列示例代碼,編譯的時候不再會有警告但仍然允許您往list中放入不同的類型的對象。

          List<Object> l = new ArrayList<Object>();
          l.add("hello");  
          l.add(123);              // autoboxing
          Object o = l.get(0);


          參數化類型的體系

          參數化類型有類型體系,就像一般的類型一樣。這個體系基于對象的類型,而不是變量的類型。這里有些例子您可以嘗試:

          ArrayList<Integer> l = new ArrayList<Integer>();
          List<Integer> m = l;                            // okay
          Collection<Integer> n = l;                      // okay
          ArrayList<Number> o = l;                        // error
          Collection<Object> p = (Collection<Object>)l;   // error, even with cast


          一個List<Integer>是一個Collection<Integer>,但不是一個List<Object>。這句話不容易理解,如果您想理解為什么泛型這樣做,這段值得看一下。考察這段代碼:

          List<Integer> li = new ArrayList<Integer>();
          li.add(123);

          // The line below will not compile.  But for the purposes of this
          // thought-experiment, assume that it does compile and see how much
          // trouble we get ourselves into.
          List<Object> lo = li;  

          // Now we can retrieve elements of the list as Object instead of Integer
          Object number = lo.get(0);

          // But what about this?
          lo.add("hello world");

          // If the line above is allowed then the line below throws ClassCastException
          Integer i = li.get(1);  // Can't cast a String to Integer!


          這就是為什么List<Integer>不是一個List<Object>的原因,雖然List<Integer>中所有的元素事實上是一個Object的實例。如果允許轉換成List<Object>,那么轉換后,理論上非整型的對象也將被允許添加到list中。

          運行時類型安全

          就像我們所見到的,一個List<X>不允許被轉換為一個List<Y>,即使這個X能夠被轉換為Y。然而,一個List<X>能夠被轉換為一個List,這樣您就可以通過繼承的方法來做這樣的事情。
          這種將參數化類型轉換為非參數化類型的能力對于向下兼容是必要的,但是它會在泛型所帶來的類型安全體系上鑿個漏洞:

          // Here's a basic parameterized list.
          List<Integer> li = new ArrayList<Integer>();

          // It is legal to assign a parameterized type to a nonparameterized variable
          List l = li;  

          // This line is a bug, but it compiles and runs.
          // The Java 5.0 compiler will issue an unchecked warning about it.
          // If it appeared as part of a legacy class compiled with Java 1.4, however,
          // then we'd never even get the warning.  
          l.add("hello");

          // This line compiles without warning but throws ClassCastException at runtime.
          // Note that the failure can occur far away from the actual bug.
          Integer i = li.get(0);


          泛型僅提供了編譯期的類型安全。如果您使用java5.0的編譯器來編譯您的代碼并且沒有得到任何警告,這些編譯器的檢查能夠確保您的代碼在運行期也是類型安全的。如果您獲得了警告或者使用了像未經處理的類型那樣修改您的集合的代碼,那么您需要增加一些步驟來確保運行期的類型安全。您可以通過使用java.util.Collections中的checkedList()和checkedMap( )方法來做到這一步。這些方法將把您的集合打包成一個wrapper集合,從而在運行時檢查確認只有正確類型的值能夠被置入集合眾。下面是一個能夠補上類型安全漏洞的一個例子:

          // Here's a basic parameterized list.
          List<Integer> li = new ArrayList<Integer>();

          // Wrap it for runtime type safety
          List<Integer> cli = Collections.checkedList(li, Integer.class);

          // Now widen the checked list to the raw type
          List l = cli;  

          // This line compiles but fails at runtime with a ClassCastException.
          // The exception occurs exactly where the bug is, rather than far away
          l.add("hello");


          參數化類型的數組

          在使用泛型類型的時候,數組需要特別的考慮。回憶一下,如果T是S的父類(或者接口),那么類型為S的數組S[],同時又是類型為T的數組T[]。正因為如此,每次您存放一個對象到數組中時,Java解釋器都必須進行檢查以確保您放入的對象類型與要存放的數組所允許的類型是匹對的。例如,下列代碼在運行期會檢查失敗,拋出一個ArrayStoreException異常:

          String[] words = new String[10];
          Object[] objs = words;
          objs[0] = 1;  // 1 autoboxed to an Integer, throws ArrayStoreException


          雖然編譯時obj是一個Object[],但是在運行時它是一個String[],它不允許被用于存放一個Integer.
          當我們使用泛型類型的時候,僅僅依靠運行時的數組存放異常檢查是不夠的,因為一個運行時進行的檢查并不能夠獲取編譯時的類型參數信息。查看下列代碼:

          List<String>[] wordlists = new ArrayList<String>[10];
          ArrayList<Integer> ali = new ArrayList<Integer>();
          ali.add(123);
          Object[] objs = wordlists;
          objs[0] = ali;                       // No ArrayStoreException
          String s = wordlists[0].get(0);      // ClassCastException!


          如果上面的代碼被允許,那么運行時的數組存儲檢查將會成功:沒有編譯時的類型參數,代碼簡單地存儲一個ArrayList到一個ArrayList[]數組,非常正確。既然編譯器不能阻止您通過這個方法來戰勝類型安全,那么它轉而阻止您創建一個參數化類型的數組。所以上述情節永遠不會發生,編譯器在第一行就開始拒絕編譯了。

          注意這并不是一個在使用數組時使用泛型的全部的約束,這僅僅是一個創建一個參數化類型數組的約束。我們將在學習如何寫泛型方法時再來討論這個話題。


          類型參數通配符
          假設我們需要寫一個方法來顯示一個List中的元素。[3]在以前,我們只需要象這樣寫段代碼:

          public static void printList(PrintWriter out, List list) {
              for(int i=0, n=list.size(); i < n; i++) {
                  if (i > 0) out.print(", ");
                  out.print(list.get(i).toString());
              }
          }


          在Java5.0中,List是一個泛型類型,如果我們試圖編譯這個方法,我們將會得到unchecked警告。為了解決這些警告,您可能需要這樣來修改這個方法:

          public static void printList(PrintWriter out, List<Object> list) {
              for(int i=0, n=list.size(); i < n; i++) {
                  if (i > 0) out.print(", ");
                  out.print(list.get(i).toString());
              }
          }


          這段代碼能夠編譯通過同時不會有警告,但是它并不是非常地有效,因為只有那些被聲明為List<Object>的list才會被允許使用這個方法。還記得么,類似于List<String>和List<Integer>這樣的List并不能被轉型為List<Object>。事實上我們需要一個類型安全的printList()方法,它能夠接受我們傳入的任何List,而不關心它被參數化為什么。解決辦法是使用類型參數通配符。方法可以被修改成這樣:

          public static void printList(PrintWriter out, List<?> list) {
              for(int i=0, n=list.size(); i < n; i++) {
                  if (i > 0) out.print(", ");
                  Object o = list.get(i);
                  out.print(o.toString());
              }
          }


          這個版本的方法能夠被編譯過,沒有警告,而且能夠在任何我們希望使用的地方使用。通配符“?”表示一個未知類型,類型List<?>被讀作“List of unknown”
          作為一般原則,如果類型是泛型的,同時您并不知道或者并不關心值的類型,您應該使用“?”通配符來代替一個未經處理的類型。未經處理的類型被允許僅是為了向下兼容,而且應該只能夠被允許出現在老的代碼中。注意,無論如何,您不能在調用構造器時使用通配符。下面的代碼是非法的:
          List<?> l = new ArrayList<?>();

          創建一個不知道類型的List是毫無道理的。如果您創建了它,那么您必須知道它將保持的元素是什么類型的。您可以在隨后的方法中不關心元素類型而去遍歷這里list,但是您需要在您創建它的時候描述元素的類型。如果你確實需要一個List來保持任何類型,那么您只能這么寫:

          List<Object> l = new ArrayList<Object>();


          從上面的printList()例子中,必須要搞清楚List<?>既不是List<Object>也不是一個未經處理的List。一個使用通配符的List<?>有兩個重要的特性。第一,考察類似于get()的方法,他們被聲明返回一個值,這個值的類型是類型參數中指定的。在這個例子中,類型是“unknown”,所以這些方法返回一個Object。既然我們期望的是調用這個object的toString()方法,程序能夠很好的滿足我們的意愿。

          第二,考察List的類似add()的方法,他們被聲明為接受一個參數,這個參數被類型參數所定義。出人意料的是,當類型參數是未確定的,編譯器不允許您調用任何有不確定參數類型的方法——因為它不能確認您傳入了一個恰當的值。一個List(?)實際上是只讀的——既然編譯器不允許我們調用類似于add(),set(),addAll()這類的方法。

          界定通配符
          讓我們在我們原來的例子上作些小小的稍微復雜一點的改動。假設我們希望寫一個sumList()方法來計算list中Number類型的值的合計。在以前,我們使用未經處理的List,但是我們不想放棄類型安全,同時不得不處理來自編譯器的unchecked警告。或者我們可以使用List<Number>,那樣的話我們就不能調用List<Integer>、List<Double>中的方法了,而事實上我們需要調用。如果我們使用通配符,那么我們實際上不能得到我們期望的類型安全,我們不能確定我們的方法被什么樣的List所調用,Number?還是Number的子類?甚至,String?這樣的一個方法也許會被寫成這樣:

          public static double sumList(List<?> list) {
              double total = 0.0;
              for(Object o : list) {
                  Number n = (Number) o;  // A cast is required and may fail
                  total += n.doubleValue();
              }
              return total;
          }


          要修改這個方法讓它變得真正的類型安全,我們需要使用界定通配符(bounded wildcard),能夠確保List的類型參數是未知的,但又是Number或者Number的子類。下面的代碼才是我們想要的:

          public static double sumList(List<? extends Number> list) {
              double total = 0.0;
              for(Number n : list) total += n.doubleValue();
              return total;
          }


          類型List<? extends Number>可以被理解為“Number未知子類的List”。理解這點非常重要,在這段文字中,Number被認為是其自身的子類。

          注意,這樣的話,那些類型轉換已經不再需要了。我們并不知道list中元素的具體類型,但是我們知道他們能夠向上轉型為Number,因此我們可以把他們從list中把他們當作一個Number對象取出。使用一個for/in循環能夠稍微封裝一下從list中取出元素的過程。普遍性的原則是當您使用一個界定通配符時,類似于List中的get()方法的那些方法將返回一個類型為上界的值。因此如果我們在for/in循環中調用list.get(),我們將得到一個Number。在前一節說到使用通配符時類似于list.add()這種方法中的限制依然有效:舉個例子來說,如果編譯器允許我們調用這類方法,我們就可以將一個Integer放到一個聲明為僅保持Short值的list中去。

          同樣可行的是使用下界通配符,不同的是用super替換extends。這個技巧在被調用的方法上有一點不同的作用。在實際應用中,下界通配符要比上界通配符用得少。我們將在后面的章節里討論這個問題。


          腳注

          [1] 在本章中,我會堅持用術語”泛型類型”來指一個聲明一個或多個類型變量的類型,用”參數化的類型”來指由實際類型參數來替換其類型變量的泛型類型。然而,在一般情況下,這種區別并不明顯,并且這些術語有時通用。
          [2] 在撰寫本文時候,javac并不支持@SuppressWarnings 的注解。期望在Java 5.1中得到支持。
          [3] 本節所示的3個printList()方法忽略了這樣一個事實,即java.util 中List的所有實現類都有一個可用的toString()方法。還要注意這些方法假定List實現RandomAccess并在LinkedList實例中只提供了很差的運行效率。
          David Flanagan是眾多O'Reilly書籍的作者。這些書包括《Java in a Nutshell》,《Java Examples in a Nutshell》,《Java Foundation Classes in a Nutshell》,《JavaScript: The Definitive Guide》,《JavaScript Pocket Reference》。

          在Java in a Nutshell, 5th Edition中查看目錄信息。

          posted @ 2009-12-14 22:53 ammay 閱讀(396) | 評論 (0)編輯 收藏

          泛型

          泛型是 Sun 公司發布的 JDK 5.0 中的一個重要特性,它的最大優點是提供了程序的類型安全同可以向后兼容。為了幫助讀者更好地理解和使用泛型,本文通過一些示例從基本原理,重要概念,關鍵技術,以及相似技術比較等多個角度對 Java 語言中的泛型技術進行了介紹,重點強調了泛型中的一些基本但又不是很好理解的概念。

          為了避免和 C++ 中的模板混淆,本文簡要介紹了 Java 中的泛型和 C++ 中的模板的主要區別,希望這種比較能夠幫助讀者加深對泛型的理解。

          引言

          很多 Java 程序員都使用過集合(Collection),集合中元素的類型是多種多樣的,例如,有些集合中的元素是 Byte 類型的,而有些則可能是 String 類型的,等等。Java 語言之所以支持這么多種類的集合,是因為它允許程序員構建一個元素類型為 Object 的 Collection,所以其中的元素可以是任何類型。

          當使用 Collection 時,我們經常要做的一件事情就是要進行類型轉換,當轉換成所需的類型以后,再對它們進行處理。很明顯,這種設計給編程人員帶來了極大的不便,同時也容易引入錯誤。

          在很多 Java 應用中,上述情況非常普遍,為了解決這個問題,使 Java 語言變得更加安全好用,近些年的一些編譯器對 Java 語言進行了擴充,使 Java 語言支持了"泛型",特別是 Sun 公司發布的 JDK 5.0 更是將泛型作為其中一個重要的特性加以推廣。

          本文首先對泛型的基本概念和特點進行簡單介紹,然后通過引入幾個實例來討論帶有泛型的類,泛型中的子類型,以及范化方法和受限類型參數等重要概念。為了幫助讀者更加深刻的理解并使用泛型,本文還介紹了泛型的轉化,即,如何將帶有泛型的 Java 程序轉化成一般的沒有泛型的 Java 程序。這樣,讀者對泛型的理解就不會僅僅局限在表面上了。考慮到多數讀者僅僅是使用泛型,因此本文并未介紹泛型在編譯器中的具體實現。Java 中的泛型和 C++ 中的模板表面上非常相似,但實際上二者還是有很大區別的,本文最后簡單介紹了 Java 中的泛型與 C++ 模板的主要區別。





          回頁首


          泛型概覽

          泛型本質上是提供類型的"類型參數",它們也被稱為參數化類型(parameterized type)或參量多態(parametric polymorphism)。其實泛型思想并不是 Java 最先引入的,C++ 中的模板就是一個運用泛型的例子。

          GJ(Generic Java)是對 Java 語言的一種擴展,是一種帶有參數化類型的 Java 語言。用 GJ 編寫的程序看起來和普通的 Java 程序基本相同,只不過多了一些參數化的類型同時少了一些類型轉換。實際上,這些 GJ 程序也是首先被轉化成一般的不帶泛型的 Java 程序后再進行處理的,編譯器自動完成了從 Generic Java 到普通 Java 的翻譯。具體的轉化過程大致分為以下幾個部分:

          • 將參數化類型中的類型參數"擦除"(erasure)掉;
          • 將類型變量用"上限(upper bound)"取代,通常情況下這些上限是 Object。這里的類型變量是指實例域,本地方法域,方法參數以及方法返回值中用來標記類型信息的"變量",例如:實例域中的變量聲明 A elem;,方法聲明 Node (A elem){};,其中,A 用來標記 elem 的類型,它就是類型變量。
          • 添加類型轉換并插入"橋方法"(bridge method),以便覆蓋(overridden)可以正常的工作。

          轉化后的程序和沒有引入泛型時程序員不得不手工完成轉換的程序是非常一致的,具體的轉化過程會在后面介紹。GJ 保持了和 Java 語言以及 Java 虛擬機很好的兼容性,下面對 GJ 的特點做一個簡要的總結。

          • 類型安全。 泛型的一個主要目標就是提高 Java 程序的類型安全。使用泛型可以使編譯器知道變量的類型限制,進而可以在更高程度上驗證類型假設。如果沒有泛型,那么類型的安全性主要由程序員來把握,這顯然不如帶有泛型的程序安全性高。
          • 消除強制類型轉換。泛型可以消除源代碼中的許多強制類型轉換,這樣可以使代碼更加可讀,并減少出錯的機會。
          • 向后兼容。支持泛型的 Java 編譯器(例如 JDK5.0 中的 Javac)可以用來編譯經過泛型擴充的 Java 程序(GJ 程序),但是現有的沒有使用泛型擴充的 Java 程序仍然可以用這些編譯器來編譯。
          • 層次清晰,恪守規范。無論被編譯的源程序是否使用泛型擴充,編譯生成的字節碼均可被虛擬機接受并執行。也就是說不管編譯器的輸入是 GJ 程序,還是一般的 Java 程序,經過編譯后的字節碼都嚴格遵循《Java 虛擬機規范》中對字節碼的要求。可見,泛型主要是在編譯器層面實現的,它對于 Java 虛擬機是透明的。
          • 性能收益。目前來講,用 GJ 編寫的代碼和一般的 Java 代碼在效率上是非常接近的。 但是由于泛型會給 Java 編譯器和虛擬機帶來更多的類型信息,因此利用這些信息對 Java 程序做進一步優化將成為可能。

          以上是泛型的一些主要特點,下面通過幾個相關的例子來對 Java 語言中的泛型進行說明。





          回頁首


          帶有泛型的類

          為了幫助大家更好地理解 Java 語言中的泛型,我們在這里先來對比兩段實現相同功能的 GJ 代碼和 Java 代碼。通過觀察它們的不同點來對 Java 中的泛型有個總體的把握,首先來分析一下不帶泛型的 Java 代碼,程序如下:


          1	interface Collection {
                      2	   	public void add (Object x);
                      3		public Iterator iterator ();
                      4	}
                      5
                      6	interface Iterator {
                      7	   	public Object next ();
                      8		public boolean hasNext ();
                      9	}
                      10
                      11	class NoSuchElementException extends RuntimeException {}
                      12
                      13	class LinkedList implements Collection {
                      14
                      15		protected class Node {
                      16		   	Object elt;
                      17			Node next = null;
                      18			Node (Object elt) { this.elt = elt; }
                      19		}
                      20
                      21		protected Node head = null, tail = null;
                      22
                      23		public LinkedList () {}
                      24
                      25		public void add (Object elt) {
                      26			if (head == null) { head = new Node(elt); tail = head; }
                      27			else { tail.next = new Node(elt); tail = tail.next; }
                      28		}
                      29
                      30		public Iterator iterator () {
                      31
                      32			return new Iterator () {
                      33				protected Node ptr = head;
                      34				public boolean hasNext () { return ptr != null; }
                      35				public Object next () {
                      36					if (ptr != null) {
                      37						Object elt = ptr.elt; ptr = ptr.next; return elt;
                      |-------10--------20--------30--------40--------50--------60--------70--------80--------9|
                      |-------- XML error:  The previous line is longer than the max of 90 characters ---------|
                      38					} else throw new NoSuchElementException ();
                      39				}
                      40			};
                      41		}
                      42	}
                      

          接口 Collection 提供了兩個方法,即添加元素的方法 add(Object x),見第 2 行,以及返回該 CollectionIterator 實例的方法 iterator(),見第 3 行。Iterator 接口也提供了兩個方法,其一就是判斷是否有下一個元素的方法 hasNext(),見第 8 行,另外就是返回下一個元素的方法 next(),見第 7 行。LinkedList 類是對接口 Collection 的實現,它是一個含有一系列節點的鏈表,節點中的數據類型是 Object,這樣就可以創建任意類型的節點了,比如 Byte, String 等等。上面這段程序就是用沒有泛型的傳統的 Java 語言編寫的代碼。接下來我們分析一下傳統的 Java 語言是如何使用這個類的。

          代碼如下:


          1	class Test {
                      2		    public static void main (String[] args) {
                      3			// byte list
                      4			LinkedList xs = new LinkedList();
                      5			xs.add(new Byte(0)); xs.add(new Byte(1));
                      6			Byte x = (Byte)xs.iterator().next();
                      7			// string list
                      8			LinkedList ys = new LinkedList();
                      9			ys.add("zero"); ys.add("one");
                      10			String y = (String)ys.iterator().next();
                      11			// string list list
                      12			LinkedList zss = new LinkedList();
                      13			zss.add(ys);
                      14			String z = (String)((LinkedList)zss.iterator().next()).iterator().next();
                      |-------10--------20--------30--------40--------50--------60--------70--------80--------9|
                      |-------- XML error:  The previous line is longer than the max of 90 characters ---------|
                      15			// string list treated as byte list
                      16			Byte w = (Byte)ys.iterator().next(); // run-time exception
                      17			}
                      18	}
                      

          從上面的程序我們可以看出,當從一個鏈表中提取元素時需要進行類型轉換,這些都要由程序員顯式地完成。如果我們不小心從 String 類型的鏈表中試圖提取一個 Byte 型的元素,見第 15 到第 16 行的代碼,那么這將會拋出一個運行時的異常。請注意,上面這段程序可以順利地經過編譯,不會產生任何編譯時的錯誤,因為編譯器并不做類型檢查,這種檢查是在運行時進行的。不難發現,傳統 Java 語言的這一缺陷推遲了發現程序中錯誤的時間,從軟件工程的角度來看,這對軟件的開發是非常不利的。接下來,我們討論一下如何用 GJ 來實現同樣功能的程序。源程序如下:


          1	interface Collection<A> {
                      2		public void add(A x);
                      3		public Iterator<A> iterator();
                      4	}
                      5
                      6	interface Iterator<A> {
                      7		public A next();
                      8		public boolean hasNext();
                      9	}
                      10
                      11	class NoSuchElementException extends RuntimeException {}
                      12
                      13	class LinkedList<A> implements Collection<A> {
                      14		protected class Node {
                      15			A elt;
                      16			Node next = null;
                      17			Node (A elt) { this.elt = elt; }
                      18		}
                      19
                      20		protected Node head = null, tail = null;
                      21
                      22		public LinkedList () {}
                      23
                      24		public void add (A elt) {
                      25			if (head == null) { head = new Node(elt); tail = head; }
                      26			else { tail.next = new Node(elt); tail = tail.next; }
                      27		}
                      28
                      29		public Iterator<A> iterator () {
                      30			return new Iterator<A> () {
                      31				protected Node ptr = head;
                      32				public boolean hasNext () { return ptr != null; }
                      33				public A next () {
                      34					if (ptr != null) {
                      35						A elt = ptr.elt; ptr = ptr.next; return elt;
                      |-------10--------20--------30--------40--------50--------60--------70--------80--------9|
                      |-------- XML error:  The previous line is longer than the max of 90 characters ---------|
                      36					} else throw new NoSuchElementException ();
                      37				}
                      38			};
                      39	 	}
                      40	}
                      

          程序的功能并沒有任何改變,只是在實現方式上使用了泛型技術。我們注意到上面程序的接口和類均帶有一個類型參數 A,它被包含在一對尖括號(< >)中,見第 1,6 和 13 行,這種表示法遵循了 C++ 中模板的表示習慣。這部分程序和上面程序的主要區別就是在 Collection, Iterator, 或 LinkedList 出現的地方均用 Collection<A>, Iterator<A>, 或 LinkedList<A> 來代替,當然,第 22 行對構造函數的聲明除外。

          下面再來分析一下在 GJ 中是如何對這個類進行操作的,程序如下:


          1	class Test {
                      2		public static void main (String [] args) {
                      3			// byte list
                      4			LinkedList<Byte> xs = new LinkedList<Byte>();
                      5			xs.add(new Byte(0)); xs.add(new Byte(1));
                      6			Byte x = xs.iterator().next();
                      7			// string list
                      8			LinkedList<String> ys = new LinkedList<String>();
                      9			ys.add("zero"); ys.add("one");
                      10			String y = ys.iterator().next();
                      11			// string list list
                      12			LinkedList<LinkedList<String>>zss=
                      newLinkedList<LinkedList<String>>();
                      13			zss.add(ys);
                      14			String z = zss.iterator().next().iterator().next();
                      15			// string list treated as byte list
                      16			Byte w = ys.iterator().next(); // compile-time error
                      17		}
                      18	}
                      

          在這里我們可以看到,有了泛型以后,程序員并不需要進行顯式的類型轉換,只要賦予一個參數化的類型即可,見第 4,8 和 12 行,這是非常方便的,同時也不會因為忘記進行類型轉換而產生錯誤。另外需要注意的就是當試圖從一個字符串類型的鏈表里提取出一個元素,然后將它賦值給一個 Byte 型的變量時,見第 16 行,編譯器將會在編譯時報出錯誤,而不是由虛擬機在運行時報錯,這是因為編譯器會在編譯時刻對 GJ 代碼進行類型檢查,此種機制有利于盡早地發現并改正錯誤。

          類型參數的作用域是定義這個類型參數的整個類,但是不包括靜態成員函數。這是因為當訪問同一個靜態成員函數時,同一個類的不同實例可能有不同的類型參數,所以上述提到的那個作用域不應該包括這些靜態函數,否則就會引起混亂。





          回頁首


          泛型中的子類型

          在 Java 語言中,我們可以將某種類型的變量賦值給其父類型所對應的變量,例如,String 是 Object 的子類型,因此,我們可以將 String 類型的變量賦值給 Object 類型的變量,甚至可以將 String [ ] 類型的變量(數組)賦值給 Object [ ] 類型的變量,即 String [ ] 是 Object [ ] 的子類型。

          上述情形恐怕已經深深地印在了廣大讀者的腦中,對于泛型來講,上述情形有所變化,因此請廣大讀者務必引起注意。為了說明這種不同,我們還是先來分析一個小例子,代碼如下所示:


          1			List<String> ls = new ArrayList<String>();
                      2			List<Object> lo = ls;
                      3	lo.add(new Integer());
                      4	String s = ls.get(0);
                      

          上述代碼的第二行將 List<String> 賦值給了 List<Object>,按照以往的經驗,這種賦值好像是正確的,因為 List<String> 應該是 List<Object> 的子類型。這里需要特別注意的是,這種賦值在泛型當中是不允許的!List<String> 也不是 List<Object> 的子類型。

          如果上述賦值是合理的,那么上面代碼的第三行的操作將是可行的,因為 loList<Object>,所以向其添加 Integer 類型的元素應該是完全合法的。讀到此處,我們已經看到了第二行的這種賦值所潛在的危險,它破壞了泛型所帶來的類型安全性。

          一般情況下,如果 A 是 B 的子類型,C 是某個泛型的聲明,那么 C<A> 并不是 C<B> 的子類型,我們也不能將 C<A> 類型的變量賦值給 C<B> 類型的變量。這一點和我們以前接觸的父子類型關系有很大的出入,因此請讀者務必引起注意。





          回頁首


          泛化方法和受限類型參數

          在這一部分我們將討論有關泛化方法(generic method )和受限類型參數(bounded type parameter)的內容,這是泛型中的兩個重要概念,還是先來分析一下與此相關的代碼。


          1	interface Comparable<A> {
                      2		public int compareTo(A that);
                      3	}
                      4
                      5	class Byte implements Comparable<Byte> {
                      6		private byte value;
                      7		public Byte(byte value) {this.value = value;}
                      8		public byte byteValue() {return value;}
                      9		public int compareTo(Byte that) {
                      10			return this.value - that.value;
                      11		}
                      12	}
                      13
                      14	class Collections {
                      15		public static <A implements Comparable<A>>
                      16	            A max (Collection<A> xs) {
                      17			    	Iterator<A> xi = xs.iterator();
                      18			    	A w = xi.next();
                      19			    	while (xi.hasNext()) {
                      20						A x = xi.next();
                      21						if (w.compareTo(x) < 0) w = x;
                      22					}
                      23					return w;
                      24		}
                      25	}
                      

          這里定義了一個接口 Comparable<A>,用來和 A 類型的對象進行比較。類 Byte 實現了這個接口,并以它自己作為類型參數,因此,它們自己就可以和自己進行比較了。

          第 14 行到第 25 行的代碼定義了一個類 Collections,這個類包含一個靜態方法 max(Collection<A> xs),它用來在一個非空的 Collection 中尋找最大的元素并返回這個元素。這個方法的兩個特點就是它是一個泛化方法并且有一個受限類型參數。

          之所以說它是泛化了的方法,是因為這個方法可以應用到很多種類型上。當要將一個方法聲明為泛化方法時,我們只需要在這個方法的返回類型(A)之前加上一個類型參數(A),并用尖括號(< >)將它括起來。這里的類型參數(A)是在方法被調用時自動實例化的。例如,假設對象 m 的類型是 Collection<Byte>,那么當使用下面的語句:


          Byte x = Collections.max(m);
                      

          調用方法 max 時,該方法的參數 A 將被推測為 Byte。

          根據上面討論的內容,泛化方法 max 的完整聲明應該是下面的形式:


                      < A >  A max (Collection<A> xs) {
                      max 的方法體
                      }
                      

          但是,我們見到的 max 在 < A > 中還多了 "implements Comparable<A>" 一項,這是什么呢?這就是我們下面將要談到的"受限的類型參數"。在上面的例子中,類型參數 A 就是一個受限的的類型參數,因為它不是泛指任何類型,而是指那些自己和自己作比較的類型。例如參數可以被實例化為 Byte,因為程序中有 Byte implements Comparable<Byte> 的語句,參見第 5 行。這種限制(或者說是范圍)通過如下的方式表示,"類型參數 implements 接口",或是 "類型參數 extend 類",上面程序中的"Byte implements Comparable<Byte>"就是一例。





          回頁首


          泛型的轉化

          在前面的幾部分內容當中,我們介紹了有關泛型的基礎知識,到此讀者對 Java 中的泛型技術應該有了一定的了解,接下來的這部分內容將討論有關泛型的轉化,即如何將帶有泛型的 Java 代碼轉化成一般的沒有泛型 Java 代碼。其實在前面的部分里,我們或多或少地也提到了一些相關的內容,下面再來詳細地介紹一下。

          首先需要明確的一點是上面所講的這種轉化過程是由編譯器(例如:Javac)完成的,虛擬機并不負責完成這一任務。當編譯器對帶有泛型的 Java 代碼進行編譯時,它會去執行類型檢查和類型推斷,然后生成普通的不帶泛型的字節碼,這種字節碼可以被一般的Java虛擬機接收并執行,這種技術被稱為擦除(erasure)。

          可見,編譯器可以在對源程序(帶有泛型的 Java 代碼)進行編譯時使用泛型類型信息保證類型安全,對大量如果沒有泛型就不會去驗證的類型安全約束進行驗證,同時在生成的字節碼當中,將這些類型信息清除掉。

          對于不同的情況,擦除技術所執行的"擦除"動作是不同的,主要分為以下幾種情況:

          • 對于參數化類型,需要刪除其中的類型參數,例如,LinkedList<A> 將被"擦除"為 LinkedList;
          • 對于非參數化類型,不作擦除,或者說用它自己來擦除自己,例如 String 將被"擦除"為 String;
          • 對于類型變量(有關類型變量的說明請參考"泛型概覽"相關內容),要用它們的上限來對它們進行替換。多數情況下這些上限是 Object,但是也有例外,后面的部分將會對此進行介紹。

          除此之外,還需要注意的一點是,在某些情況下,擦除技術需要引入類型轉換(cast),這些情況主要包括:

          情況 1. 方法的返回類型是類型參數;

          情況 2. 在訪問數據域時,域的類型是一個類型參數。

          例如在本文"帶有泛型的類"一小節的最后,我們給出了一段測試程序,一個 Test 類。這個類包含以下幾行代碼:


          8			LinkedList<String> ys = new LinkedList<String>();
                      9			ys.add("zero"); ys.add("one");
                      10			String y = ys.iterator().next();
                      這部分代碼轉換后就變成了如下的代碼:
                      8	       	LinkedList ys = new LinkedList();
                      9           ys.add("zero"); ys.add("one");
                      10	String y = (String)ys.iterator().next();
                      

          第 10 行的代碼進行了類型轉換,這是因為在調用 next() 方法時,編譯器發現該方法的返回值類型是類型參數 A(請參見對方法 next() 的定義),因此根據上面提到的情況 1,需要進行類型轉換。

          上面介紹了泛型轉化中的擦除技術,接下來,我們討論一下泛型轉化中的另外一個重要問題--橋方法(bridge method)。

          Java 是一種面向對象的語言,因此覆蓋(overridden)是其中的一項重要技術。覆蓋能夠正常"工作"的前提是方法名和方法的參數類型及個數完全匹配(參數的順序也應一致),為了滿足這項要求,編譯器在泛型轉化中引入了橋方法(bridge method)。接下來,我們通過一個例子來分析一下橋方法在泛型轉化中所起的作用。在本文"泛化方法和受限類型參數"一小節所給出的代碼中,第 9 行到第 11 行的程序如下所示:


              9		public int compareTo(Byte that) {
                      10			return this.value - that.value;
                      11		}
                      這部分代碼經過轉化,就變成了下面的樣子:
                      9		public int compareTo(Byte that) {
                      10			return this.value - that.value;
                      11		}
                      12  public int compareTo(Object that){
                      13			return this.compareTo((Byte)that);
                      14		}
                      

          第 12 行的方法 compareTo(Object that) 就是一個橋方法,在這里引入這個方法是為了保證覆蓋能夠正常的發生。我們在前面提到過,覆蓋必須保證方法名和參數的類型及數目完全匹配,在這里通過引入這個"橋"即可達到這一目的,由這個"橋"進行類型轉換,并調用第 9 行參數類型為 Byte 的方法 compareTo(Byte that),需要注意的一點是這里的 "Object" 也并不一定是完全匹配的類型,但由于它是 Java 語言中類層次結構的根,所以這里用 "Object" 可以接受其他任何類型的參數。

          根據面向對象的基本概念,我們知道,重載(overloading)允許橋方法和原來的方法共享同一個方法名,正如上面例子所顯示的那樣,因此橋方法的引入是完全合法的。一般情況下,當一個類實現了一個參數化的接口或是繼承了一個參數化的類時,需要引入橋方法。

          到此,我們對泛型中的子類型,帶有泛型的類,泛化方法,受限類型參數以及泛型的轉化進行了簡要的介紹,下面部分將結合這些技術對前面提到的例子進行一下總結,以便能夠幫助讀者更深刻更全面地理解泛型。

          首先來分析一下本文提到的那個 Collection 的例子。這里先是定義了兩個接口 CollectionIterator,然后又定義了一個對接口 Collection 的一個實現 LinkedList。根據上面所介紹的對泛型的轉化過程,這段代碼轉化后的 Java 程序為:


          1	interface Collection {
                      2	   	public void add (Object x);
                      3	        public Iterator iterator ();
                      4	}
                      5
                      6	interface Iterator {
                      7	       	public Object next ();
                      8	        public boolean hasNext ();
                      9	}
                      10
                      11	class NoSuchElementException extends RuntimeException {}
                      12
                      13	class LinkedList implements Collection {
                      14
                      15		protected class Node {
                      16	       	Object elt;
                      17	        Node next = null;
                      18	        Node (Object elt) { this.elt = elt; }
                      19		}
                      20
                      21		protected Node head = null, tail = null;
                      22
                      23		public LinkedList () {}
                      24
                      25		public void add (Object elt) {
                      26	       	if (head == null) {
                      27		       	head = new Node(elt); tail = head;
                      28			} else {
                      29		       	tail.next = new Node(elt); tail = tail.next;
                      30			}
                      31		}
                      32
                      33		public Iterator iterator () {
                      34	       	return new Iterator () {
                      35		       	protected Node ptr = head;
                      36	            public boolean hasNext () { return ptr != null; }
                      37		       	public Object next () {
                      38			       	if (ptr != null) {
                      39				       	Object elt = ptr.elt; ptr = ptr.next; return elt;
                      40					} else {
                      41				       		throw new NoSuchElementException ();
                      42						}
                      43				}
                      44			};
                      45		}
                      46	}
                      

          通過分析上述代碼,我們不難發現,所有參數化類型 Collection, Iterator 和 LinkedList 中的類型參數 "A" 全都被擦除了。另外,剩下的類型變量 "A" 都用其上限進行了替換,這里的上限是 Object,見黑體字標出的部分,這是轉化的關鍵部分。

          下面我們分析一下在介紹有關泛化方法(generic method)和受限類型參數(bounded type parameter)時舉的那個例子,該段 GJ 代碼經過轉換后的等價 Java 程序如下所示:


          1	interface Comparable {
                      2		public int compareTo(Object that);
                      3	}
                      4
                      5	class Byte implements Comparable {
                      6		private byte value;
                      7		public Byte(byte value) {this.value = value;}
                      8		public byte byteValue(){return value;}
                      9		public int compareTo(Byte that) {
                      10			return this.value - that.value;
                      11		}
                      12      public int compareTo(Object that){
                      13			return this.compareTo((Byte)that);
                      14		}
                      15	}
                      16
                      17	class Collections {
                      18		public static Comparable max(Collection xs){
                      19			Iterator xi = xs.iterator();
                      20			Comparable w = (Comparable)xi.next();
                      21			while (xi.hasNext()) {
                      22				Comparable x = (Comparable)xi.next();
                      23				if (w.compareTo(x) < 0) w = x;
                      23			}
                      24			return w;
                      25		}
                      26	}
                      

          同樣請讀者注意黑體字標出的部分,這些關鍵點我們在前面已經介紹過了,故不贅述。唯一需要注意的一點就是第 18,20,22 行出現的Comparable。在泛型轉化中,類型變量應該用其上限來替換,一般情況下這些上限是 "Object",但是當遇到受限的類型參數時,這個上限就不再是 "Object" 了,編譯器會用限制這些類型參數的類型來替換它,上述代碼就用了對 A 進行限制的類型 "Comparable" 來替換 A。

          橋方法的引入,為解決覆蓋問題帶來了方便,但是這種方法還存在一些問題,例如下面這段代碼:


          1	interface Iterator<A> {
                      2		public boolean hasNext ();
                      3		public A next ();
                      4	}
                      5	class Interval implements Iterator<Integer> {
                      6		private int i;
                      7		private int n;
                      8		public Interval (int l, int u) { i = l; n = u; }
                      9		public boolean hasNext () { return (i <= n); }
                      10		public Integer next () { return new Integer(i++); }
                      11	}
                      

          根據以上所講的內容,這部分代碼轉換后的 Java 程序應該是如下這個樣子:


          1	interface Iterator {
                      2
                      3		public boolean hasNext ();
                      4		public Object next ();
                      5
                      6	}
                      7
                      8	class Interval implements Iterator {
                      9
                      10		private int i;
                      11		private int n;
                      12		public Interval (int l, int u) { i = l; n = u; }
                      13		public boolean hasNext () { return (i <= n); }
                      14		public Integer next%1% () { return new Integer(i++); }
                      15		// bridge
                      16		public Object next%2%() { return next%1%(); }
                      17
                      18	}
                      

          相信有些讀者已經發現了這里的問題,這不是一段合法的 Java 源程序,因為第 14 行和第 16 行的兩個 next() 有相同的參數,無法加以區分。代碼中的 %1% 和 %2% 是為了區分而人為加入的,并非 GJ 轉化的結果。

          不過,這并不是什么太大的問題,因為 Java 虛擬機可以區分這兩個 next() 方法,也就是說,從 Java 源程序的角度來看,上述程序是不正確的,但是當編譯成字節碼時,JVM 可以對兩個 next() 方法進行識別。這是因為,在 JVM 中,方法定義時所使用的方法簽名包括方法的返回類型,這樣一來,只要 GJ 編譯出的字節碼符合Java字節碼的規范即可,這也正好說明了 GJ 和 JVM 中字節碼規范要求的一致性!

          最后,值得一提的是,JDK 5.0 除了在編譯器層面對 Java 中的泛型進行了支持,Java 的類庫為支持泛型也做了相應地調整,例如,集合框架中所有的標準集合接口都進行了泛型化,同時,集合接口的實現也都進行了相應地泛型化。





          回頁首


          Java 中的泛型與 C++ 模板的比較

          GJ 程序的語法在表面上與 C++ 中的模板非常類似,但是二者之間有著本質的區別。

          首先,Java 語言中的泛型不能接受基本類型作為類型參數――它只能接受引用類型。這意味著可以定義 List<Integer>,但是不可以定義 List<int>。

          其次,在 C++ 模板中,編譯器使用提供的類型參數來擴充模板,因此,為 List<A> 生成的 C++ 代碼不同于為 List<B> 生成的代碼,List<A> 和 List<B> 實際上是兩個不同的類。而 Java 中的泛型則以不同的方式實現,編譯器僅僅對這些類型參數進行擦除和替換。類型 ArrayList<Integer> 和 ArrayList<String> 的對象共享相同的類,并且只存在一個 ArrayList 類。





          回頁首


          總結

          本文通過一些示例從基本原理,重要概念,關鍵技術,以及相似技術比較等多個角度對 Java 語言中的泛型技術進行了介紹,希望這種介紹方法能夠幫助讀者更好地理解和使用泛型。本文主要針對廣大的 Java 語言使用者,在介紹了泛型的基本概念后,重點介紹了比較底層的泛型轉化技術,旨在幫助讀者更加深刻地掌握泛型,筆者相信這部分內容可以使讀者避免對泛型理解的表面化,也所謂知其然更知其所以然。



          參考資料



          關于作者

           

          周晶,2006年4月畢業于北京航空航天大學計算機學院,獲計算機碩士學位。主要研究領域為高級編譯技術,Java虛擬機技術。 beyond.zhou@gmail.com



          posted @ 2009-12-14 22:52 ammay 閱讀(203) | 評論 (0)編輯 收藏

          泛型

          Jdk1.5中的新特性 --泛型 (詳細版)

          本來只轉載了個鏈接,和一個簡單的使用程序,但昨天不小心看到有人批判jdk1.5,先說java要強制轉型不好的問題沒解決,
          容器不能放基類型不好,接著說泛型沒用。而恰恰Jdk1.5中解決了這些問題,所以感嘆之余,把這篇文章改一下,詳細的說說泛型。

          一,Java中的泛型:
          在Java中能使用到泛型的多是容器類,如各種list map set,因為Java是單根繼承,所以容器里邊可以放的
          內容是任何Object,所以從意義上講原本的設計才是泛型。但用過Java的人是否感覺每次轉型很麻煩呢?
          而且會有些錯誤,比如一個容器內放入了異質對象,強制轉型的時候會出現cast異常。而這中錯誤在編譯器是
          無從發現的。所以jdk1.5中提供了泛型,這個泛型其實是向c++靠攏了.好,我們先看幾個實例再細說原理。

          二,泛型的用法:(多個實例)

          1 實例A
          2 ArrayList < String >  strList  =   new  ArrayList < String > ();
          3 strList.add( " 1 " );
          4 strList.add( " 2 " );
          5 strList.add( " 3 " );
          6 // 關鍵點(1) 注意下邊這行,沒有強制轉型
          7 String str  =  strList.get( 1 );
          8 // 關鍵點(2)然後我們加入,這個時候你會發現編譯器報錯,錯誤在編譯器被發現,錯誤當然是發現的越早越好
          9 strList.add( new  Object());


          1 實例B
          2 ArrayList < Integer >  iList  =   new  ArrayList < Integer > ();
          3 // 關鍵點(3) 注意直接把整數放入了集合中,而沒有用Integer包裹
          4 iList.add( 1 );
          5 iList.add( 2 );
          6 iList.add( 3 );
          7 // 關鍵點(4)同樣直接取出就是int
          8 int  num  =  iList.get( 1 );


          1 實例C
          2 // 關鍵點(5)展示一下key-value的時候要怎么寫,同時key和value也可以是基本類型了。
          3 HashMap < Integer,Integer >  map  =   new  HashMap < Integer,Integer > ();
          4 map.put( 1 11 );
          5 map.put( 2 22 );
          6 map.put( 3 33 );
          7 int  inum  =  map.get( 1 );
          8


          三,看完了實例了,詳細來說說為什么吧
          首先jdk1.5中的泛型,第一個解決的問題,就是Java中很多不必要的強制轉型了,具體的實現,我們以ArrayList
          為例,下邊是ArrayList中的片斷代碼:

           1ArrayList類的定義,這里加入了<E>
           2public class ArrayList<E> extends AbstractList<E>
           3        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
           4
           5//get方法,返回不再是Object 而是E
           6public E get(int index) {
           7    RangeCheck(index);
           8    return elementData[index];
           9}

          10//add方法,參數不再是Object 而是E
          11public boolean add(E o) {
          12    ensureCapacity(size + 1);  // Increments modCount!!
          13    elementData[size++= o;
          14    return true;
          15}

          16


          四,Boxing 和UnBoxing
          看到上邊的關鍵點(3)和(4)是否感覺驚奇呢,因為Java中煩人的除了強制轉型,另一個就是基礎類型了
          放入容器的時候要包裝,取出了還要轉回。Jdk1.5中解決了這個問題.如上邊的使用方法

          五,泛型的生命周期(使用注意事項)
          如果我們試著把ArrayList<String> list的內容序列化,然後再讀取出來,在使用的過程中會發現出錯,
          為什么呢?用Stream讀取一下回來的數據,你會發現<String>不見了,list變成了普通的ArrayList,而不是
          參數化型別的ArrayList了,為什么會這樣呢 ?見下邊的比較

          六,C++的泛型和Java的泛型
          在泛型的實現上,C++和Java有著很大的不同,
          Java是擦拭法實現的
          C++是膨脹法實現的
          因為Java原本實現就是泛型的,現在加入型別,其實是"窄化",所以采用擦拭法,在實現上,其實是封裝了原本的
          ArrayList,這樣的話,對于下邊這些情況,Java的實現類只有一個。

          1ArrayList<Integer>  .;   public class ArrayList
          2ArrayList<String>  ..;   --同上--
          3ArrayList<Double>  ..;   --同上--
          4而C++采用的是膨脹法,對于上邊的三種情況實際是每一種型別都對應一個實現,實現類有多個
          5list<int> li;                class list; //int 版本
          6list<string> ls;             class list; //string 版本
          7list<double> ld;             class list; //double 版本    

          這就造成了,在序列化后,Java不能分清楚原來的ArrayList是
          ArrayList<Integer>還是ArrayList

          七,題外話,在很多東西的實現上C++和Java有很多不同
          例如運算符的問題i=i++問題,詳細看這里
          例如在C++中能很好實現的double-checked locking單態模式,在Java中幾乎很難實現 詳細看這里
          還有就是上邊提到的泛型實現上。

          八,Jdk 1.5加入了不少新東西,有些能很大的提高開發質量,例如Jdk1.4 ,Jdk.15中StringBuffer的不同
          因為從1。4轉入1。5不久,所以慢慢會發一些在1。5的使用過程中發現的東西。

          最后,我們還可以自己寫類似ArrayList這樣的泛型類,至于如何自定義泛型類,泛型方法請參見候捷先生的文章

          posted @ 2009-12-14 22:51 ammay 閱讀(185) | 評論 (0)編輯 收藏

          導航

          統計

          常用鏈接

          留言簿

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 长治县| 广东省| 长宁区| 永年县| 呼玛县| 新泰市| 金平| 团风县| 吉隆县| 建宁县| 嵊泗县| 元阳县| 曲阳县| 信阳市| 景德镇市| 大余县| 滁州市| 遂溪县| 富民县| 静宁县| 新竹县| 长汀县| 宁陵县| 枝江市| 壶关县| 含山县| 来安县| 襄汾县| 张家口市| 班戈县| 石台县| 威信县| 沿河| 临澧县| 泗洪县| 紫阳县| 新晃| 剑河县| 揭西县| 资兴市| 海丰县|