ammayjxf

           

          2009年12月3日

          java 初始化

          解析 Java 類和對(duì)象的初始化過程

          由一個(gè)單態(tài)模式引出的問題談起

          developerWorks
          文檔選項(xiàng)
          將打印機(jī)的版面設(shè)置成橫向打印模式

          打印本頁

          將此頁作為電子郵件發(fā)送

          將此頁作為電子郵件發(fā)送


          級(jí)別: 初級(jí)

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

          2006 年 8 月 31 日

          類的初始化和對(duì)象初始化是 JVM 管理的類型生命周期中非常重要的兩個(gè)環(huán)節(jié),Google 了一遍網(wǎng)絡(luò),有關(guān)類裝載機(jī)制的文章倒是不少,然而類初始化和對(duì)象初始化的文章并不多,特別是從字節(jié)碼和 JVM 層次來分析的文章更是鮮有所見。

          本文主要對(duì)類和對(duì)象初始化全過程進(jìn)行分析,通過一個(gè)實(shí)際問題引入,將源代碼轉(zhuǎn)換成 JVM 字節(jié)碼后,對(duì) JVM 執(zhí)行過程的關(guān)鍵點(diǎn)進(jìn)行全面解析,并在文中穿插入了相關(guān) JVM 規(guī)范和 JVM 的部分內(nèi)部理論知識(shí),以理論與實(shí)際結(jié)合的方式介紹對(duì)象初始化和類初始化之間的協(xié)作以及可能存在的沖突問題。

          問題引入

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

          下面是經(jīng)過我簡(jiǎn)化后的示例代碼:


          [清單一]
          package com.ccb.framework.enums;
                                  import java.util.Collections;
                                  import java.util.HashMap;
                                  import java.util.Map;
                                  public class CachingEnumResolver {
                                  //單態(tài)實(shí)例 一切問題皆由此行引起
                                  private static final CachingEnumResolver SINGLE_ENUM_RESOLVER = new
                                  CachingEnumResolver();
                                  /*MSGCODE->Category內(nèi)存索引*/
                                  private static Map CODE_MAP_CACHE;
                                  static {
                                  CODE_MAP_CACHE = new HashMap();
                                  //為了說明問題,我在這里初始化一條數(shù)據(jù)
                                  CODE_MAP_CACHE.put("0","北京市");
                                  }
                                  //private, for single instance
                                  private CachingEnumResolver() {
                                  //初始化加載數(shù)據(jù)  引起問題,該方法也要負(fù)點(diǎn)責(zé)任
                                  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);
                                  }
                                  /**
                                  * 獲取單態(tài)實(shí)例
                                  *
                                  * @return
                                  */
                                  public static CachingEnumResolver getInstance() {
                                  return SINGLE_ENUM_RESOLVER;
                                  }
                                  public static void main(String[] args) {
                                  System.out.println(CachingEnumResolver.getInstance().getCache());
                                  }
                                  }
                                  

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

          是的,他看起來的確沒有問題,可是如果將他 run 起來時(shí),其結(jié)果是他不會(huì)為你正確 work。運(yùn)行該類,它的執(zhí)行結(jié)果是:


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

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

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

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





          回頁首


          類的生命周期



          上圖展示的是類生命周期流向;在本文里,我只打算談?wù)勵(lì)惖?初始化"以及"對(duì)象實(shí)例化"兩個(gè)階段。





          回頁首


          類初始化

          類"初始化"階段,它是一個(gè)類或接口被首次使用的前階段中的最后一項(xiàng)工作,本階段負(fù)責(zé)為類變量賦予正確的初始值。

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

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

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




          回頁首


          對(duì)象初始化

          在類被裝載、連接和初始化,這個(gè)類就隨時(shí)都可能使用了。對(duì)象實(shí)例化和初始化是就是對(duì)象生命的起始階段的活動(dòng),在這里我們主要討論對(duì)象的初始化工作的相關(guān)特點(diǎn)。

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

          通常來說,<init>() 方法內(nèi)包括的代碼內(nèi)容大概為:調(diào)用另一個(gè) <init>() 方法;對(duì)實(shí)例變量初始化;與其對(duì)應(yīng)的構(gòu)造方法內(nèi)的代碼。

          如果構(gòu)造方法是明確地從調(diào)用同一個(gè)類中的另一個(gè)構(gòu)造方法開始,那它對(duì)應(yīng)的 <init>() 方法體內(nèi)包括的內(nèi)容為:一個(gè)對(duì)本類的 <init>() 方法的調(diào)用;對(duì)應(yīng)用構(gòu)造方法內(nèi)的所有字節(jié)碼。

          如果構(gòu)造方法不是通過調(diào)用自身類的其它構(gòu)造方法開始,并且該對(duì)象不是 Object 對(duì)象,那 <init>() 法內(nèi)則包括的內(nèi)容為:一個(gè)對(duì)父類 <init>() 方法的調(diào)用;對(duì)實(shí)例變量初始化方法的字節(jié)碼;最后是對(duì)應(yīng)構(gòu)造子的方法體字節(jié)碼。

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





          回頁首


          類的初始化時(shí)機(jī)

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

          Java 虛擬機(jī)規(guī)范為類的初始化時(shí)機(jī)做了嚴(yán)格定義:"initialize on first active use"--" 在首次主動(dòng)使用時(shí)初始化"。這個(gè)規(guī)則直接影響著類裝載、連接和初始化類的機(jī)制--因?yàn)樵陬愋捅怀跏蓟八仨氁呀?jīng)被連接,然而在連接之前又必須保證它已經(jīng)被裝載了。

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

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

          首次主動(dòng)使用的情形:

          • 創(chuàng)建某個(gè)類的新實(shí)例時(shí)--new、反射、克隆或反序列化;
          • 調(diào)用某個(gè)類的靜態(tài)方法時(shí);
          • 使用某個(gè)類或接口的靜態(tài)字段或?qū)υ撟侄钨x值時(shí)(final字段除外);
          • 調(diào)用Java的某些反射方法時(shí)
          • 初始化某個(gè)類的子類時(shí)
          • 在虛擬機(jī)啟動(dòng)時(shí)某個(gè)含有main()方法的那個(gè)啟動(dòng)類。

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





          回頁首


          我的問題究竟出在哪里

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

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


          [清單三]
          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
                                  }
                                  

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

          現(xiàn)在,讓我們對(duì)該類從類初始化到對(duì)象實(shí)例初始化全過程分析[清單一]中的代碼執(zhí)行軌跡。

          如前面所述,類初始化是在類真正可用時(shí)的最后一項(xiàng)前階工作,該階段負(fù)責(zé)對(duì)所有類正確的初始化值,此項(xiàng)工作是線程安全的,JVM會(huì)保證多線程同步。

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

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

          第3步:讓我們順著執(zhí)行順序向下看,"④" 行,該行所在方法就是該類的構(gòu)造器,該方法先調(diào)用父類的構(gòu)造器 <init>() 對(duì)父對(duì)象進(jìn)行初始化,然后調(diào)用 CachingEnumResolver.initEnum() 方法加載數(shù)據(jù)。

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

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

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

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

          第8步:類初始化完畢,同時(shí)該單態(tài)類的實(shí)例化工作也完成。

          通過對(duì)上面的字節(jié)碼執(zhí)行過程分析,或許你已經(jīng)清楚了解到導(dǎo)致錯(cuò)誤的深層原因了,也或許你可能早已被上面的分析過程給弄得暈頭轉(zhuǎn)向了,不過也沒折,雖然我也可以從源代碼的角度來闡述問題,但這樣不夠深度,同時(shí)也會(huì)有僅為個(gè)人觀點(diǎn)、不足可信之嫌。





          回頁首


          如何解決

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





          回頁首


          寫在最后

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


          程序一
          	public class CachingEnumResolver {
                                  public  static Map CODE_MAP_CACHE;
                                  static {
                                  CODE_MAP_CACHE = new HashMap();
                                  //為了說明問題,我在這里初始化一條數(shù)據(jù)
                                  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();
                                  //為了說明問題,我在這里初始化一條數(shù)據(jù)
                                  CODE_MAP_CACHE.put("0","北京市");
                                  SINGLE_ENUM_RESOLVER = new CachingEnumResolver();
                                  initEnums();
                                  }
                                  

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

          獻(xiàn)上此文,謹(jǐn)以共勉。



          關(guān)于作者

           

          北京高偉達(dá)西南分軟 Java EE 軟件工程師,三年 Java EE 項(xiàng)目經(jīng)驗(yàn),行業(yè)方向?yàn)殂y行 OCRM 系統(tǒng)。對(duì) JAVA 有著濃厚的興趣,業(yè)余研究 AOP/ESB 方向。


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

          泛型

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

          創(chuàng)建泛型和泛型方法
          創(chuàng)建一個(gè)簡(jiǎn)單的泛型是非常容易的。首先,在一對(duì)尖括號(hào)(< >)中聲明類型變量,以逗號(hào)間隔變量名列表。在類的實(shí)例變量和方法中,可以在任何類型的地方使用那些類型變量。切記,類型變量?jī)H在編譯時(shí)存在,所以不能使用instanceof和new這類運(yùn)行時(shí)操作符來操作類型變量。

          讓我們以一個(gè)簡(jiǎn)單的例子來開始這部分的學(xué)習(xí),而后將精簡(jiǎn)這個(gè)例子。這段代碼定義了一個(gè)樹形數(shù)據(jù)結(jié)構(gòu),使用類型變量V代表存儲(chǔ)在各個(gè)樹結(jié)點(diǎn)中的值。

          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); }
          }


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

          注意到,當(dāng)一個(gè)變量被聲明為泛型時(shí),只能被實(shí)例變量和方法調(diào)用(還有內(nèi)嵌類型)而不能被靜態(tài)變量和方法調(diào)用。原因很簡(jiǎn)單,參數(shù)化的泛型是一些實(shí)例。靜態(tài)成員是被類的實(shí)例和參數(shù)化的類所共享的,所以靜態(tài)成員不應(yīng)該有類型參數(shù)和他們關(guān)聯(lián)。方法,包括靜態(tài)方法,可以聲明和使用他們自己的類型參數(shù),但是,調(diào)用這樣一個(gè)方法,可以被不同地參數(shù)化。這些內(nèi)容將在本章后面談到。

          類型變量綁定
          上面例子中的Tree中的類型變量V是不受約束的:Tree可以被參數(shù)化為任何類型。以前我們常常會(huì)設(shè)置一些約束條件在需要使用的類型上:也許我們需要強(qiáng)制一個(gè)類型參數(shù)實(shí)現(xiàn)一個(gè)或多個(gè)接口,或是一個(gè)特定類的子類。這可以通過指明類型綁定來完成。我們已經(jīng)看到了統(tǒng)配符的上界,而且使用簡(jiǎn)單的語法可以指定一般類型變量的上界。后面的代碼,還是使用Tree這個(gè)例子,并且通過實(shí)現(xiàn)Serializable和Comparable來重寫。為了做到這點(diǎn),例子中使用類型變量綁定來確保值類型的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;
          }


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

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

          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); }
          }


          通配符綁定允許我們?cè)谥?jié)點(diǎn)上增加一個(gè)Tree,比如,一個(gè)樹枝Tree
          Tree t = new Tree(0); // Note autoboxing
          t.addBranch(new Tree(1)); // int 1 autoboxed to Integer


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


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


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


          這個(gè)例子在設(shè)計(jì)時(shí)找到了一個(gè)平衡點(diǎn):使用綁定通配符使得數(shù)據(jù)結(jié)構(gòu)更加靈活,但是減少了安全使用其中方法的可能。這個(gè)設(shè)計(jì)是好是壞就要根據(jù)上下文聯(lián)系了。通常,好的范型設(shè)計(jì)是非常困難的。幸運(yùn)的是,大多我們要使用的已經(jīng)在java.util包中設(shè)計(jì)好了,而不用我們自己再去設(shè)計(jì)。

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

          這里有一個(gè)要添加到Tree中的靜態(tài)方法。他不是一個(gè)范型方法,但是使用了綁定的通配符,就好像先前我們看到的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;
          }


            通過通配符的上界綁定,聲明自己的類型變量來重寫這個(gè)方法:
          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()不比通配符版本的簡(jiǎn)單,而且聲明變量并沒有讓我們獲得什么。這種情況下,通配符方案要比范型方法更有效,當(dāng)一個(gè)類型變量用來表達(dá)兩個(gè)參數(shù)之間或者參數(shù)和返回值之間的關(guān)系時(shí),范型方法才是需要的。請(qǐng)看下面的例子:
          // 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;
          }
            

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

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


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


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

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


          當(dāng)你調(diào)用這個(gè)方法時(shí),不需要指明N,因?yàn)镹是隱含地由t和u指明。在后面的代碼中,編譯器決定N為Integer:

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


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

          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;
          }
          }


          這里有兩個(gè)該方法的調(diào)用:

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


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

          Set empty = Collections.emptySet();


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

          Set empty = Collections.emptySet();


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


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

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

          如果你創(chuàng)建一個(gè)方法使用varargs(參見第二章的2.6.4)和類型變量,記住調(diào)用varargs隱含創(chuàng)建一個(gè)數(shù)組,請(qǐng)看下面的例子:

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


          你可以使用一個(gè)Integer類型來調(diào)用這個(gè)方法,因?yàn)榫幾g器會(huì)在調(diào)用的時(shí)候插入必要的數(shù)組創(chuàng)建代碼。但是你不能將參數(shù)轉(zhuǎn)換為Comparable來調(diào)用這個(gè)方法,因?yàn)閯?chuàng)建一個(gè)Comparable[]是不合法的。

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

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


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

          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); }



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

          java.util中的范形是比較簡(jiǎn)單的:因?yàn)榇蠖喽际羌项悾愋妥兞恳彩谴砑现械脑亍ava.lang中的幾個(gè)重要范型是比較難以理解的,他們不是集合,而且第一眼很不容易理解為什么設(shè)計(jì)成范型。學(xué)習(xí)這些范型可以讓我們更深層次的理解范形的工作機(jī)制,并且介紹一些我們沒有提到的概念。特別的,我們要檢查Comparable接口和Enum類(枚舉類型的超類,后面一張講解)并且學(xué)習(xí)一些重要但是很少使用的范型特性,比如通配符下界。

          在java1.5中,Comparable接口被修改為范型的。大多數(shù)的類都實(shí)現(xiàn)了這個(gè)接口,考慮一下Integer:

          public final class Integer extends Number implements Comparable


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

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

          public class BigInteger extends Number implements Comparable


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

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

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

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

          這些是比較簡(jiǎn)單的,本章我們已經(jīng)看到了通配符上界,我們?cè)賮砜纯磎ax()中的類型變量聲明:

          >


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

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

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


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

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

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

          最后,讓我們把注意力放到j(luò)ava.lang.Enum類。Enum是所有枚舉類型的父類,它實(shí)現(xiàn)了Comparable接口,但是有一個(gè)讓人迷惑的范型聲明方法:

          public class Enum> implements Comparable, Serializable


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


          資源:
          ·Onjava.com:Onjava.com
          ·Matrix-Java開發(fā)者社區(qū):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) | 評(píng)論 (0)編輯 收藏

          泛型類型

          泛型類型,第一部分

          作者: David Flanagan

          翻譯:cat


          版權(quán)聲明:可以任意轉(zhuǎn)載,轉(zhuǎn)載時(shí)請(qǐng)務(wù)必以超鏈接形式標(biāo)明文章原始出處和作者信息及本聲明
          作者:
          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
          關(guān)鍵詞: Generic Types


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

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

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

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

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

          類型安全集合類

          Java.util類包包含了Java集合框架(Java Collections Framework),這是一批包含對(duì)象的set、對(duì)象的list以及基于key-value的map。第五章將談到集合類。這里,我們討論的是在java5.0中集合類使用類型參數(shù)來界定集合中的對(duì)象的類型。這個(gè)討論并不適合java1.4或更早期版本。如果沒有泛型,對(duì)于集合類的使用需要程序員記住每個(gè)集合中元素的類型。當(dāng)您在java1.4種創(chuàng)建了一個(gè)集合,您知道您放入到集合中的對(duì)象的類型,但是編譯器不知道。您必須小心地往其中加入一個(gè)合適類型的元素,當(dāng)需要從集合中獲取元素時(shí),您必須顯式的寫強(qiáng)制類型轉(zhuǎn)換以將他們從Object轉(zhuǎn)換為他們真是的類型。考察下邊的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或是其他集合類已經(jīng)使用泛型重寫過了。就像前面提到的, List被重新定義為一個(gè)list,它中間的元素類型被一個(gè)類型可變的名稱為E的占位符描述。Add()方法被重新定義為期望一個(gè)類型為E的參數(shù),用于替換以前的Object,get()方法被重新定義為返回一個(gè)E,替換了以前的Object。

          在java5.0中,當(dāng)我們申明一個(gè)List或者創(chuàng)建一個(gè)ArrayList的實(shí)例的時(shí)候,我們需要在泛型類型的名字后面緊跟一對(duì)“<>”,尖括號(hào)中寫入我們需要的實(shí)際的類型。比如,一個(gè)保持String的List應(yīng)該寫成“List<String>”。需要注意的是,這非常象給一個(gè)方法傳一個(gè)參數(shù),區(qū)別是我們使用類型而不是值,同時(shí)使用尖括號(hào)而不是圓括號(hào)

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

          在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);
          }


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

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

          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>這個(gè)一個(gè)參數(shù)類型其本身也是也一個(gè)類型,也能夠被用于當(dāng)作其他類型的一個(gè)類型變量值。您可能會(huì)看到這樣的代碼:

          // 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()方法返回一個(gè)類型為E的list元素或者一個(gè)類型為V的map元素。注意,無論如何,泛型類型能夠更精密的使用他們的變量。在本書中的參考章節(jié)查看List<E>,您將會(huì)看到它的iterator( )方法被聲明為返回一個(gè)Iterator<E>。這意味著,這個(gè)方法返回一個(gè)跟list的實(shí)際的參數(shù)類型一樣的一個(gè)參數(shù)類型的實(shí)例。為了具體的說明這點(diǎn),下面的例子提供了不使用get(0)方法來獲取一個(gè)List<String>的第一個(gè)元素的方法。

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


          理解泛型類型

          本段將對(duì)泛型類型的使用細(xì)節(jié)做進(jìn)一步的探討,以嘗試說明下列問題:
          不帶類型參數(shù)的使用泛型的后果
          參數(shù)化類型的體系
          一個(gè)關(guān)于編譯期泛型類型的類型安全的漏洞和一個(gè)用于確保運(yùn)行期類型安全的補(bǔ)丁
          為什么參數(shù)化類型的數(shù)組不是類型安全的

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

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


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

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


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

          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()方法的調(diào)用上給出了警告,因?yàn)樗荒軌虼_信加入到list中的值具有正確的類型。它告訴我們說我們使用了一個(gè)未經(jīng)處理的類型,它不能驗(yàn)證我們的代碼是類型安全的。注意,get()方法的調(diào)用是沒有問題的,因?yàn)槟軌虮猾@得的元素已經(jīng)安全的存在于list中了。

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

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


          參數(shù)化類型的體系

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

          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


          一個(gè)List<Integer>是一個(gè)Collection<Integer>,但不是一個(gè)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>不是一個(gè)List<Object>的原因,雖然List<Integer>中所有的元素事實(shí)上是一個(gè)Object的實(shí)例。如果允許轉(zhuǎn)換成List<Object>,那么轉(zhuǎn)換后,理論上非整型的對(duì)象也將被允許添加到list中。

          運(yùn)行時(shí)類型安全

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

          // 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的編譯器來編譯您的代碼并且沒有得到任何警告,這些編譯器的檢查能夠確保您的代碼在運(yùn)行期也是類型安全的。如果您獲得了警告或者使用了像未經(jīng)處理的類型那樣修改您的集合的代碼,那么您需要增加一些步驟來確保運(yùn)行期的類型安全。您可以通過使用java.util.Collections中的checkedList()和checkedMap( )方法來做到這一步。這些方法將把您的集合打包成一個(gè)wrapper集合,從而在運(yùn)行時(shí)檢查確認(rèn)只有正確類型的值能夠被置入集合眾。下面是一個(gè)能夠補(bǔ)上類型安全漏洞的一個(gè)例子:

          // 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");


          參數(shù)化類型的數(shù)組

          在使用泛型類型的時(shí)候,數(shù)組需要特別的考慮。回憶一下,如果T是S的父類(或者接口),那么類型為S的數(shù)組S[],同時(shí)又是類型為T的數(shù)組T[]。正因?yàn)槿绱耍看文娣乓粋€(gè)對(duì)象到數(shù)組中時(shí),Java解釋器都必須進(jìn)行檢查以確保您放入的對(duì)象類型與要存放的數(shù)組所允許的類型是匹對(duì)的。例如,下列代碼在運(yùn)行期會(huì)檢查失敗,拋出一個(gè)ArrayStoreException異常:

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


          雖然編譯時(shí)obj是一個(gè)Object[],但是在運(yùn)行時(shí)它是一個(gè)String[],它不允許被用于存放一個(gè)Integer.
          當(dāng)我們使用泛型類型的時(shí)候,僅僅依靠運(yùn)行時(shí)的數(shù)組存放異常檢查是不夠的,因?yàn)橐粋€(gè)運(yùn)行時(shí)進(jìn)行的檢查并不能夠獲取編譯時(shí)的類型參數(shù)信息。查看下列代碼:

          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!


          如果上面的代碼被允許,那么運(yùn)行時(shí)的數(shù)組存儲(chǔ)檢查將會(huì)成功:沒有編譯時(shí)的類型參數(shù),代碼簡(jiǎn)單地存儲(chǔ)一個(gè)ArrayList到一個(gè)ArrayList[]數(shù)組,非常正確。既然編譯器不能阻止您通過這個(gè)方法來戰(zhàn)勝類型安全,那么它轉(zhuǎn)而阻止您創(chuàng)建一個(gè)參數(shù)化類型的數(shù)組。所以上述情節(jié)永遠(yuǎn)不會(huì)發(fā)生,編譯器在第一行就開始拒絕編譯了。

          注意這并不是一個(gè)在使用數(shù)組時(shí)使用泛型的全部的約束,這僅僅是一個(gè)創(chuàng)建一個(gè)參數(shù)化類型數(shù)組的約束。我們將在學(xué)習(xí)如何寫泛型方法時(shí)再來討論這個(gè)話題。


          類型參數(shù)通配符
          假設(shè)我們需要寫一個(gè)方法來顯示一個(gè)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是一個(gè)泛型類型,如果我們?cè)噲D編譯這個(gè)方法,我們將會(huì)得到unchecked警告。為了解決這些警告,您可能需要這樣來修改這個(gè)方法:

          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());
              }
          }


          這段代碼能夠編譯通過同時(shí)不會(huì)有警告,但是它并不是非常地有效,因?yàn)橹挥心切┍宦暶鳛長(zhǎng)ist<Object>的list才會(huì)被允許使用這個(gè)方法。還記得么,類似于List<String>和List<Integer>這樣的List并不能被轉(zhuǎn)型為L(zhǎng)ist<Object>。事實(shí)上我們需要一個(gè)類型安全的printList()方法,它能夠接受我們傳入的任何List,而不關(guān)心它被參數(shù)化為什么。解決辦法是使用類型參數(shù)通配符。方法可以被修改成這樣:

          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());
              }
          }


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

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

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


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

          第二,考察List的類似add()的方法,他們被聲明為接受一個(gè)參數(shù),這個(gè)參數(shù)被類型參數(shù)所定義。出人意料的是,當(dāng)類型參數(shù)是未確定的,編譯器不允許您調(diào)用任何有不確定參數(shù)類型的方法——因?yàn)樗荒艽_認(rèn)您傳入了一個(gè)恰當(dāng)?shù)闹怠R粋€(gè)List(?)實(shí)際上是只讀的——既然編譯器不允許我們調(diào)用類似于add(),set(),addAll()這類的方法。

          界定通配符
          讓我們?cè)谖覀冊(cè)瓉淼睦由献餍┬⌒〉纳晕?fù)雜一點(diǎn)的改動(dòng)。假設(shè)我們希望寫一個(gè)sumList()方法來計(jì)算list中Number類型的值的合計(jì)。在以前,我們使用未經(jīng)處理的List,但是我們不想放棄類型安全,同時(shí)不得不處理來自編譯器的unchecked警告。或者我們可以使用List<Number>,那樣的話我們就不能調(diào)用List<Integer>、List<Double>中的方法了,而事實(shí)上我們需要調(diào)用。如果我們使用通配符,那么我們實(shí)際上不能得到我們期望的類型安全,我們不能確定我們的方法被什么樣的List所調(diào)用,Number?還是Number的子類?甚至,String?這樣的一個(gè)方法也許會(huì)被寫成這樣:

          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;
          }


          要修改這個(gè)方法讓它變得真正的類型安全,我們需要使用界定通配符(bounded wildcard),能夠確保List的類型參數(shù)是未知的,但又是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”。理解這點(diǎn)非常重要,在這段文字中,Number被認(rèn)為是其自身的子類。

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

          同樣可行的是使用下界通配符,不同的是用super替換extends。這個(gè)技巧在被調(diào)用的方法上有一點(diǎn)不同的作用。在實(shí)際應(yīng)用中,下界通配符要比上界通配符用得少。我們將在后面的章節(jié)里討論這個(gè)問題。


          腳注

          [1] 在本章中,我會(huì)堅(jiān)持用術(shù)語”泛型類型”來指一個(gè)聲明一個(gè)或多個(gè)類型變量的類型,用”參數(shù)化的類型”來指由實(shí)際類型參數(shù)來替換其類型變量的泛型類型。然而,在一般情況下,這種區(qū)別并不明顯,并且這些術(shù)語有時(shí)通用。
          [2] 在撰寫本文時(shí)候,javac并不支持@SuppressWarnings 的注解。期望在Java 5.1中得到支持。
          [3] 本節(jié)所示的3個(gè)printList()方法忽略了這樣一個(gè)事實(shí),即java.util 中List的所有實(shí)現(xiàn)類都有一個(gè)可用的toString()方法。還要注意這些方法假定List實(shí)現(xiàn)RandomAccess并在LinkedList實(shí)例中只提供了很差的運(yùn)行效率。
          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) | 評(píng)論 (0)編輯 收藏

          泛型

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

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

          引言

          很多 Java 程序員都使用過集合(Collection),集合中元素的類型是多種多樣的,例如,有些集合中的元素是 Byte 類型的,而有些則可能是 String 類型的,等等。Java 語言之所以支持這么多種類的集合,是因?yàn)樗试S程序員構(gòu)建一個(gè)元素類型為 Object 的 Collection,所以其中的元素可以是任何類型。

          當(dāng)使用 Collection 時(shí),我們經(jīng)常要做的一件事情就是要進(jìn)行類型轉(zhuǎn)換,當(dāng)轉(zhuǎn)換成所需的類型以后,再對(duì)它們進(jìn)行處理。很明顯,這種設(shè)計(jì)給編程人員帶來了極大的不便,同時(shí)也容易引入錯(cuò)誤。

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

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





          回頁首


          泛型概覽

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

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

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

          轉(zhuǎn)化后的程序和沒有引入泛型時(shí)程序員不得不手工完成轉(zhuǎn)換的程序是非常一致的,具體的轉(zhuǎn)化過程會(huì)在后面介紹。GJ 保持了和 Java 語言以及 Java 虛擬機(jī)很好的兼容性,下面對(duì) GJ 的特點(diǎn)做一個(gè)簡(jiǎn)要的總結(jié)。

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

          以上是泛型的一些主要特點(diǎn),下面通過幾個(gè)相關(guān)的例子來對(duì) Java 語言中的泛型進(jìn)行說明。





          回頁首


          帶有泛型的類

          為了幫助大家更好地理解 Java 語言中的泛型,我們?cè)谶@里先來對(duì)比兩段實(shí)現(xiàn)相同功能的 GJ 代碼和 Java 代碼。通過觀察它們的不同點(diǎn)來對(duì) Java 中的泛型有個(gè)總體的把握,首先來分析一下不帶泛型的 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 提供了兩個(gè)方法,即添加元素的方法 add(Object x),見第 2 行,以及返回該 CollectionIterator 實(shí)例的方法 iterator(),見第 3 行。Iterator 接口也提供了兩個(gè)方法,其一就是判斷是否有下一個(gè)元素的方法 hasNext(),見第 8 行,另外就是返回下一個(gè)元素的方法 next(),見第 7 行。LinkedList 類是對(duì)接口 Collection 的實(shí)現(xiàn),它是一個(gè)含有一系列節(jié)點(diǎn)的鏈表,節(jié)點(diǎn)中的數(shù)據(jù)類型是 Object,這樣就可以創(chuàng)建任意類型的節(jié)點(diǎn)了,比如 Byte, String 等等。上面這段程序就是用沒有泛型的傳統(tǒng)的 Java 語言編寫的代碼。接下來我們分析一下傳統(tǒng)的 Java 語言是如何使用這個(gè)類的。

          代碼如下:


          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	}
                      

          從上面的程序我們可以看出,當(dāng)從一個(gè)鏈表中提取元素時(shí)需要進(jìn)行類型轉(zhuǎn)換,這些都要由程序員顯式地完成。如果我們不小心從 String 類型的鏈表中試圖提取一個(gè) Byte 型的元素,見第 15 到第 16 行的代碼,那么這將會(huì)拋出一個(gè)運(yùn)行時(shí)的異常。請(qǐng)注意,上面這段程序可以順利地經(jīng)過編譯,不會(huì)產(chǎn)生任何編譯時(shí)的錯(cuò)誤,因?yàn)榫幾g器并不做類型檢查,這種檢查是在運(yùn)行時(shí)進(jìn)行的。不難發(fā)現(xiàn),傳統(tǒng) Java 語言的這一缺陷推遲了發(fā)現(xiàn)程序中錯(cuò)誤的時(shí)間,從軟件工程的角度來看,這對(duì)軟件的開發(fā)是非常不利的。接下來,我們討論一下如何用 GJ 來實(shí)現(xiàn)同樣功能的程序。源程序如下:


          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	}
                      

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

          下面再來分析一下在 GJ 中是如何對(duì)這個(gè)類進(jìn)行操作的,程序如下:


          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	}
                      

          在這里我們可以看到,有了泛型以后,程序員并不需要進(jìn)行顯式的類型轉(zhuǎn)換,只要賦予一個(gè)參數(shù)化的類型即可,見第 4,8 和 12 行,這是非常方便的,同時(shí)也不會(huì)因?yàn)橥涍M(jìn)行類型轉(zhuǎn)換而產(chǎn)生錯(cuò)誤。另外需要注意的就是當(dāng)試圖從一個(gè)字符串類型的鏈表里提取出一個(gè)元素,然后將它賦值給一個(gè) Byte 型的變量時(shí),見第 16 行,編譯器將會(huì)在編譯時(shí)報(bào)出錯(cuò)誤,而不是由虛擬機(jī)在運(yùn)行時(shí)報(bào)錯(cuò),這是因?yàn)榫幾g器會(huì)在編譯時(shí)刻對(duì) GJ 代碼進(jìn)行類型檢查,此種機(jī)制有利于盡早地發(fā)現(xiàn)并改正錯(cuò)誤。

          類型參數(shù)的作用域是定義這個(gè)類型參數(shù)的整個(gè)類,但是不包括靜態(tài)成員函數(shù)。這是因?yàn)楫?dāng)訪問同一個(gè)靜態(tài)成員函數(shù)時(shí),同一個(gè)類的不同實(shí)例可能有不同的類型參數(shù),所以上述提到的那個(gè)作用域不應(yīng)該包括這些靜態(tài)函數(shù),否則就會(huì)引起混亂。





          回頁首


          泛型中的子類型

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

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


          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>,按照以往的經(jīng)驗(yàn),這種賦值好像是正確的,因?yàn)?List<String> 應(yīng)該是 List<Object> 的子類型。這里需要特別注意的是,這種賦值在泛型當(dāng)中是不允許的!List<String> 也不是 List<Object> 的子類型。

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

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





          回頁首


          泛化方法和受限類型參數(shù)

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


          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	}
                      

          這里定義了一個(gè)接口 Comparable<A>,用來和 A 類型的對(duì)象進(jìn)行比較。類 Byte 實(shí)現(xiàn)了這個(gè)接口,并以它自己作為類型參數(shù),因此,它們自己就可以和自己進(jìn)行比較了。

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

          之所以說它是泛化了的方法,是因?yàn)檫@個(gè)方法可以應(yīng)用到很多種類型上。當(dāng)要將一個(gè)方法聲明為泛化方法時(shí),我們只需要在這個(gè)方法的返回類型(A)之前加上一個(gè)類型參數(shù)(A),并用尖括號(hào)(< >)將它括起來。這里的類型參數(shù)(A)是在方法被調(diào)用時(shí)自動(dòng)實(shí)例化的。例如,假設(shè)對(duì)象 m 的類型是 Collection<Byte>,那么當(dāng)使用下面的語句:


          Byte x = Collections.max(m);
                      

          調(diào)用方法 max 時(shí),該方法的參數(shù) A 將被推測(cè)為 Byte。

          根據(jù)上面討論的內(nèi)容,泛化方法 max 的完整聲明應(yīng)該是下面的形式:


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

          但是,我們見到的 max 在 < A > 中還多了 "implements Comparable<A>" 一項(xiàng),這是什么呢?這就是我們下面將要談到的"受限的類型參數(shù)"。在上面的例子中,類型參數(shù) A 就是一個(gè)受限的的類型參數(shù),因?yàn)樗皇欠褐溉魏晤愋停侵改切┳约汉妥约鹤鞅容^的類型。例如參數(shù)可以被實(shí)例化為 Byte,因?yàn)槌绦蛑杏?Byte implements Comparable<Byte> 的語句,參見第 5 行。這種限制(或者說是范圍)通過如下的方式表示,"類型參數(shù) implements 接口",或是 "類型參數(shù) extend 類",上面程序中的"Byte implements Comparable<Byte>"就是一例。





          回頁首


          泛型的轉(zhuǎn)化

          在前面的幾部分內(nèi)容當(dāng)中,我們介紹了有關(guān)泛型的基礎(chǔ)知識(shí),到此讀者對(duì) Java 中的泛型技術(shù)應(yīng)該有了一定的了解,接下來的這部分內(nèi)容將討論有關(guān)泛型的轉(zhuǎn)化,即如何將帶有泛型的 Java 代碼轉(zhuǎn)化成一般的沒有泛型 Java 代碼。其實(shí)在前面的部分里,我們或多或少地也提到了一些相關(guān)的內(nèi)容,下面再來詳細(xì)地介紹一下。

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

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

          對(duì)于不同的情況,擦除技術(shù)所執(zhí)行的"擦除"動(dòng)作是不同的,主要分為以下幾種情況:

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

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

          情況 1. 方法的返回類型是類型參數(shù);

          情況 2. 在訪問數(shù)據(jù)域時(shí),域的類型是一個(gè)類型參數(shù)。

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


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

          第 10 行的代碼進(jìn)行了類型轉(zhuǎn)換,這是因?yàn)樵谡{(diào)用 next() 方法時(shí),編譯器發(fā)現(xiàn)該方法的返回值類型是類型參數(shù) A(請(qǐng)參見對(duì)方法 next() 的定義),因此根據(jù)上面提到的情況 1,需要進(jìn)行類型轉(zhuǎn)換。

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

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


              9		public int compareTo(Byte that) {
                      10			return this.value - that.value;
                      11		}
                      這部分代碼經(jīng)過轉(zhuǎn)化,就變成了下面的樣子:
                      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) 就是一個(gè)橋方法,在這里引入這個(gè)方法是為了保證覆蓋能夠正常的發(fā)生。我們?cè)谇懊嫣岬竭^,覆蓋必須保證方法名和參數(shù)的類型及數(shù)目完全匹配,在這里通過引入這個(gè)"橋"即可達(dá)到這一目的,由這個(gè)"橋"進(jìn)行類型轉(zhuǎn)換,并調(diào)用第 9 行參數(shù)類型為 Byte 的方法 compareTo(Byte that),需要注意的一點(diǎn)是這里的 "Object" 也并不一定是完全匹配的類型,但由于它是 Java 語言中類層次結(jié)構(gòu)的根,所以這里用 "Object" 可以接受其他任何類型的參數(shù)。

          根據(jù)面向?qū)ο蟮幕靖拍睿覀冎溃剌d(overloading)允許橋方法和原來的方法共享同一個(gè)方法名,正如上面例子所顯示的那樣,因此橋方法的引入是完全合法的。一般情況下,當(dāng)一個(gè)類實(shí)現(xiàn)了一個(gè)參數(shù)化的接口或是繼承了一個(gè)參數(shù)化的類時(shí),需要引入橋方法。

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

          首先來分析一下本文提到的那個(gè) Collection 的例子。這里先是定義了兩個(gè)接口 CollectionIterator,然后又定義了一個(gè)對(duì)接口 Collection 的一個(gè)實(shí)現(xiàn) LinkedList。根據(jù)上面所介紹的對(duì)泛型的轉(zhuǎn)化過程,這段代碼轉(zhuǎn)化后的 Java 程序?yàn)椋?/p>
          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	}
                      

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

          下面我們分析一下在介紹有關(guān)泛化方法(generic method)和受限類型參數(shù)(bounded type parameter)時(shí)舉的那個(gè)例子,該段 GJ 代碼經(jīng)過轉(zhuǎn)換后的等價(jià) 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	}
                      

          同樣請(qǐng)讀者注意黑體字標(biāo)出的部分,這些關(guān)鍵點(diǎn)我們?cè)谇懊嬉呀?jīng)介紹過了,故不贅述。唯一需要注意的一點(diǎn)就是第 18,20,22 行出現(xiàn)的Comparable。在泛型轉(zhuǎn)化中,類型變量應(yīng)該用其上限來替換,一般情況下這些上限是 "Object",但是當(dāng)遇到受限的類型參數(shù)時(shí),這個(gè)上限就不再是 "Object" 了,編譯器會(huì)用限制這些類型參數(shù)的類型來替換它,上述代碼就用了對(duì) A 進(jìn)行限制的類型 "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	}
                      

          根據(jù)以上所講的內(nèi)容,這部分代碼轉(zhuǎn)換后的 Java 程序應(yīng)該是如下這個(gè)樣子:


          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	}
                      

          相信有些讀者已經(jīng)發(fā)現(xiàn)了這里的問題,這不是一段合法的 Java 源程序,因?yàn)榈?14 行和第 16 行的兩個(gè) next() 有相同的參數(shù),無法加以區(qū)分。代碼中的 %1% 和 %2% 是為了區(qū)分而人為加入的,并非 GJ 轉(zhuǎn)化的結(jié)果。

          不過,這并不是什么太大的問題,因?yàn)?Java 虛擬機(jī)可以區(qū)分這兩個(gè) next() 方法,也就是說,從 Java 源程序的角度來看,上述程序是不正確的,但是當(dāng)編譯成字節(jié)碼時(shí),JVM 可以對(duì)兩個(gè) next() 方法進(jìn)行識(shí)別。這是因?yàn)椋?JVM 中,方法定義時(shí)所使用的方法簽名包括方法的返回類型,這樣一來,只要 GJ 編譯出的字節(jié)碼符合Java字節(jié)碼的規(guī)范即可,這也正好說明了 GJ 和 JVM 中字節(jié)碼規(guī)范要求的一致性!

          最后,值得一提的是,JDK 5.0 除了在編譯器層面對(duì) Java 中的泛型進(jìn)行了支持,Java 的類庫為支持泛型也做了相應(yīng)地調(diào)整,例如,集合框架中所有的標(biāo)準(zhǔn)集合接口都進(jìn)行了泛型化,同時(shí),集合接口的實(shí)現(xiàn)也都進(jìn)行了相應(yīng)地泛型化。





          回頁首


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

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

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

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





          回頁首


          總結(jié)

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



          參考資料

          • 診斷 Java 代碼: 輕松掌握 Java 泛型(developerWorks,2003 年 5 月):在本文當(dāng)中,Eric Allen 通過描述諸如基本類型限制和多態(tài)方法等特性來說明泛型的優(yōu)缺點(diǎn)。

          • Java 理論和實(shí)踐: 了解泛型(developerWorks,2005 年 1 月):在本文當(dāng)中,Brian Goetz 通過示例分析了剛剛接觸泛型的新用戶常犯的錯(cuò)誤,從反面給用戶以警示。

          • 介紹 JDK 5.0 中的泛型(developerWorks,2004 年 12 月):一篇關(guān)于 JDK 5.0 中的泛型的教程,該教程解釋了在 Java 語言中引入泛型的動(dòng)機(jī),詳細(xì)介紹了泛型的語法和語義,并講述了如何在自己的類中使用泛型。

          • 馴服 Tiger系列專欄:詳細(xì)地討論有關(guān) Java SE 5 平臺(tái)的改變,并提供一些例子作為快速參考。

          • Java 理論與實(shí)踐系列專欄:探索設(shè)計(jì)模式,可靠軟件設(shè)計(jì)原則,以及應(yīng)用于實(shí)際問題的“最佳實(shí)踐”。

          • GJ: Extending the Java programming language with type parameters:Gilad Bracha, Martin Odersky 等人寫的一篇介紹 GJ 基本概念的文章。

          • Generics in the Java Programming Language:Gilad Bracha 寫的一篇詳細(xì)介紹 JDK 5.0 中的泛型的文章。


          關(guān)于作者

           

          周晶,2006年4月畢業(yè)于北京航空航天大學(xué)計(jì)算機(jī)學(xué)院,獲計(jì)算機(jī)碩士學(xué)位。主要研究領(lǐng)域?yàn)楦呒?jí)編譯技術(shù),Java虛擬機(jī)技術(shù)。 beyond.zhou@gmail.com



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

          泛型

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

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

          一,Java中的泛型:
          在Java中能使用到泛型的多是容器類,如各種list map set,因?yàn)镴ava是單根繼承,所以容器里邊可以放的
          內(nèi)容是任何Object,所以從意義上講原本的設(shè)計(jì)才是泛型。但用過Java的人是否感覺每次轉(zhuǎn)型很麻煩呢?
          而且會(huì)有些錯(cuò)誤,比如一個(gè)容器內(nèi)放入了異質(zhì)對(duì)象,強(qiáng)制轉(zhuǎn)型的時(shí)候會(huì)出現(xiàn)cast異常。而這中錯(cuò)誤在編譯器是
          無從發(fā)現(xiàn)的。所以jdk1.5中提供了泛型,這個(gè)泛型其實(shí)是向c++靠攏了.好,我們先看幾個(gè)實(shí)例再細(xì)說原理。

          二,泛型的用法:(多個(gè)實(shí)例)

          1 實(shí)例A
          2 ArrayList < String >  strList  =   new  ArrayList < String > ();
          3 strList.add( " 1 " );
          4 strList.add( " 2 " );
          5 strList.add( " 3 " );
          6 // 關(guān)鍵點(diǎn)(1) 注意下邊這行,沒有強(qiáng)制轉(zhuǎn)型
          7 String str  =  strList.get( 1 );
          8 // 關(guān)鍵點(diǎn)(2)然後我們加入,這個(gè)時(shí)候你會(huì)發(fā)現(xiàn)編譯器報(bào)錯(cuò),錯(cuò)誤在編譯器被發(fā)現(xiàn),錯(cuò)誤當(dāng)然是發(fā)現(xiàn)的越早越好
          9 strList.add( new  Object());


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


          1 實(shí)例C
          2 // 關(guān)鍵點(diǎn)(5)展示一下key-value的時(shí)候要怎么寫,同時(shí)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


          三,看完了實(shí)例了,詳細(xì)來說說為什么吧
          首先jdk1.5中的泛型,第一個(gè)解決的問題,就是Java中很多不必要的強(qiáng)制轉(zhuǎn)型了,具體的實(shí)現(xiàn),我們以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方法,參數(shù)不再是Object 而是E
          11public boolean add(E o) {
          12    ensureCapacity(size + 1);  // Increments modCount!!
          13    elementData[size++= o;
          14    return true;
          15}

          16


          四,Boxing 和UnBoxing
          看到上邊的關(guān)鍵點(diǎn)(3)和(4)是否感覺驚奇呢,因?yàn)镴ava中煩人的除了強(qiáng)制轉(zhuǎn)型,另一個(gè)就是基礎(chǔ)類型了
          放入容器的時(shí)候要包裝,取出了還要轉(zhuǎn)回。Jdk1.5中解決了這個(gè)問題.如上邊的使用方法

          五,泛型的生命周期(使用注意事項(xiàng))
          如果我們?cè)囍袮rrayList<String> list的內(nèi)容序列化,然後再讀取出來,在使用的過程中會(huì)發(fā)現(xiàn)出錯(cuò),
          為什么呢?用Stream讀取一下回來的數(shù)據(jù),你會(huì)發(fā)現(xiàn)<String>不見了,list變成了普通的ArrayList,而不是
          參數(shù)化型別的ArrayList了,為什么會(huì)這樣呢 ?見下邊的比較

          六,C++的泛型和Java的泛型
          在泛型的實(shí)現(xiàn)上,C++和Java有著很大的不同,
          Java是擦拭法實(shí)現(xiàn)的
          C++是膨脹法實(shí)現(xiàn)的
          因?yàn)镴ava原本實(shí)現(xiàn)就是泛型的,現(xiàn)在加入型別,其實(shí)是"窄化",所以采用擦拭法,在實(shí)現(xiàn)上,其實(shí)是封裝了原本的
          ArrayList,這樣的話,對(duì)于下邊這些情況,Java的實(shí)現(xiàn)類只有一個(gè)。

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

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

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

          八,Jdk 1.5加入了不少新東西,有些能很大的提高開發(fā)質(zhì)量,例如Jdk1.4 ,Jdk.15中StringBuffer的不同
          因?yàn)閺?。4轉(zhuǎn)入1。5不久,所以慢慢會(huì)發(fā)一些在1。5的使用過程中發(fā)現(xiàn)的東西。

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

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

          xquery

          http://download.csdn.net/sort/tag/XQuery

          posted @ 2009-12-03 22:43 ammay 閱讀(191) | 評(píng)論 (0)編輯 收藏

          導(dǎo)航

          統(tǒng)計(jì)

          常用鏈接

          留言簿

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評(píng)論

          閱讀排行榜

          評(píng)論排行榜

          主站蜘蛛池模板: 宜章县| 莎车县| 丰顺县| 东辽县| 上栗县| 陇西县| 家居| 无极县| 荔波县| 太康县| 秦皇岛市| 浠水县| 安庆市| 沈丘县| 三明市| 泰兴市| 玉环县| 四平市| 德令哈市| 河间市| 石嘴山市| 中宁县| 美姑县| 井冈山市| 垦利县| 房产| 渑池县| 淮安市| 军事| 民权县| 绥德县| 弋阳县| 工布江达县| 营口市| 毕节市| 乐东| 同江市| 齐河县| 昭通市| 乌兰县| 榕江县|