深入Java 1.5列舉型別的內部 (分析得相當透徹)
阿新 • • 發佈:2019-02-19
Java是一種面向物件的高階程式語言。它的出眾之處就在於它的簡潔。一個程式設計師所要做的就是建立類(Create Class)以及定義介面(Define Interface),如此而已。當然,這種簡潔和優美是有代價的,比如失去了Enum這種廣泛使用的資料型別就是一個不小的損失。在Java 1.5以前,程式設計師們不得不通過一些變通的方法來間接的解決這一問題。比如說,被普遍使用的整數列舉替代法和型別安全類替代法(Type
safe Enum)。在正式討論Java 1.5的列舉型別之前,讓我們先簡單回顧一下這兩種列舉替代方法。
一.整數列舉替代法
比如說我們要定義一個春夏秋冬四季的列舉型別,如果使用整數來模擬,其樣子大概為:
對於上面這段程式大家可能不會陌生,因為你可能在你的程式中已經多次使用了這樣的整數型別列舉。儘管這是非常普遍的一種列舉替代品,但這並不說明它是一種好的替代品。這種簡單的方法有很多嚴重的問題。
問題1:型別安全問題
首先,使用整數我們無法保證型別安全問題。比如我我們設計一個函式,我們的意圖是讓呼叫者傳入春夏秋冬之中的某一個值,但是,使用 “整數列舉”我們無法保證使用者不傳入其它意想不到的值。如下所示:
程式seasonTest(Season.SUMMER)是我們期望的使用方式,而seasonTest(5)是一個明顯的錯誤,但是在編譯的時候,編譯器會認為這是合法的函式呼叫而給於通過。這顯然是不符合Java型別安全的宗旨的。
問題2:字串的表達問題
使用列舉的大多數場合,我們需要很方便的得到列舉型別的字元表達形式,比如Spring, Summer,Fall,Winter,甚至是漢語的春,夏,秋,冬。但這種整數型別的列舉和字元沒有任何聯絡,我們要使用一些其他輔助函式來達到這樣的效果,顯得不夠方便,也就是外國人講的不是一個“Generic solution”。
二.型別安全類替代法
比較好的Enum替代品是一種被叫做型別安全的列舉方法。雖然不同的人的具體實現可能會有些不同,但它們的核心思想是一致的。讓我們先看一個簡單的例子。
它的特點是:
1. 定義一個類,用這個類的例項來表達列舉值
2. 不提供公開建構函式以杜絕客戶自己生成該類的例項
3. 所有的類的例項都是final的,不允許有任何改動
4. 所有的類的例項都是public static的,這樣客戶可以直接使用它
5. 所有的列舉值都是唯一的,所以程式中可以使用==運算子,而不必使用費時的equals()方法
以上這些特點保證了型別安全。如果有這樣的呼叫程式
那麼我們可以放心的是,myFunction方法傳入的引數一定是Season型別,絕對不可能是其他型別。而具體的值只能是我們給出的春夏秋冬的某一個(唯一的例外就是傳入一個null。那是一個其它性質的問題,是所有Java程式共有的,不是我們今天討論的話題)。這不就是使用列舉的最根本初衷嗎!
它的缺點是:
1. 不夠直觀,不夠簡潔
2. 有些情況下不如整數方便,比如不能使用switch語句
3. 記憶體開銷比整數型的要大,雖然對於大部分Java程式這不是一個問題,但對於Java移動裝置卻可能會是一個潛在的問題
比較完整的實現
上面的源程式是一個最基本的框架。在現實的程式開發中,我們會給它增加一些東西,使它更完善,更便於使用。最常見的是增加一個整數變數,來表示列舉值的先後順序或是大小級別,英文裡叫做Ordinal。這樣我們就可以在各個列舉值之間可以進行比較了。另外我們可能會需要得到這個列舉的所有值來進行遍歷或是迴圈等操作。有時候我們可能還希望給出一個字串(比如Summer)而得到相應的列舉類。如果將這些常見的要求加到我們的具體實現中,那麼我們上面的那個程式將會擴充套件為:
上面給出的這個例子雖然比較好的解決了我們對列舉型別的基本要求,但它顯然不夠簡潔。如果我們要寫很多這樣的列舉類,那將會是一個不小的任務。並且重複的寫類似的程式是非常枯燥和容易犯錯誤的。為了將程式設計師從這些繁瑣的工作中解放出來,人們開發了一些工具軟體來完成這些重複的工作。比如比較流行的JEnum,請參看《再談在Java中使用列舉》(http://tech.ccidnet.com/pub/article/c1078_a95621_p1.html )
這些工具軟體其實是一些“程式生成器”。你按照它的語法規則定義你的列舉(語法相對簡單直觀),然後執行這些工具軟體,它會將你的定義轉化為一個完整的Java類,就像我們上面所寫的那個程式一樣。看到這裡,讀者也許會想:“為什麼不能將這種工具軟體的功能放到Java編譯器中呢?那樣我們不是就可以簡單方便的定義枚舉了嗎?而具體的Java類程式由編譯器來生成,我們不必再手工完成那麼冗長的程式行,也不必使用什麼第三方工具來生成這樣的程式行了嗎?”如果你有這樣的想法,那麼就要恭喜你了。因為你和Java的設計開發人員想到一塊兒去了。好,現在就讓我們來看看Java 1.5中新增的列舉的功能。
Java 1.5的列舉型別
在新的Java 1.5中,如果定義我們上面提到的春夏秋冬四季的列舉,那麼語句非常簡單,如下所示:
怎麼樣,夠簡單了吧。在我們全面展開討論之前,讓我們先從這個小例子中我們看一下在Java 1.5中定義一個列舉型別的基本要求。
1. 使用關鍵字enum
2. 型別名稱,比如這裡的Season
3. 一串允許的值,比如上面定義的春夏秋冬四季
4. 列舉可以單獨定義在一個檔案中,也可以嵌在其它Java類中
除了這樣的基本要求外,使用者還有一些其他選擇
1.列舉可以實現一個或多個介面(Interface)
2.可以定義新的變數
3.可以定義新的方法
4.可以定義根據具體列舉值而相異的類
這些選項的具體使用和特點我們會在後面的例子中逐步提到。那麼這樣的小程式和我們前面提到的“型別安全列舉替代”方案有什麼內在聯絡呢?從表面上看,好像大相徑庭,但如果我們深入一步就會發現這兩者的本質幾乎是一模一樣的。怎麼樣,有些吃驚嗎?把上面的列舉編譯後,我們得到了Season.class。我們把這個Season.class反編譯後就會發現Java 1.5列舉的真實面目,其反編譯後的源程式為:
對比一下這個例子和我們前面給出的“型別安全列舉”,你會發現他們幾乎是同出一轍。比較顯著的一個區別是Java 1.5的所有列舉都是Enum型別的衍生子類。但如果你看看Enum類的源程式,你就會發現它只不過是提供了一些基本服務的基類,就本質而言,Java 1.5的列舉和我們所說的“型別安全列舉”是一致的。
帶有引數的建構函式
現在讓我們來看一個比較複雜一點的例子,來進一步闡述Java 1.5列舉的本質。 下面這段程式是Sun公司提供的列舉示範程式,是用太陽系中九大行星來說明列舉的一種能力-- 即我們可以建立新的建構函式,而不是僅僅侷限於Enum的預設的那一個。我們還可以自己定義新的方法,常數等等。
將這段程式編譯後然後再反編譯,我們得到了這樣的程式,也就是Java虛擬機器實際使用的源程式。
從反編譯的程式來看,這個Java 1.5的列舉的其實沒有任何神祕之處。它只不過是稍微改變了一下建構函式,增加了幾個變數和函式。我們前面提到的“型別安全列舉”一樣可以完成同樣的工作。
依附於具體變數之上的方法
下面這段小程式也是一個比較有代表性的例子。即在列出的加,減,乘,除四個列舉值中,每一個值都有其相應的類定義段(就是所謂的Value-Specific Class Bodies)。這樣,加減乘除四個列舉值就有了各自版本的eval函式實現方法。使用Java 1.5的Enum服務,其源程式為:
這段程式看起來比較複雜,如果我們反編譯一下Java編譯器生成的Operation.class檔案,我們就發現其原理並不複雜。
那麼在Java 1.5以前我們是怎樣解決這樣的問題的呢?如果使用面向物件的原則來解決這一問題,其源程式為:
這種方法的本質和Java 1.5中的Enum是一致的。就是定義一個抽象函式(abstract function),然後每一個Enum值提供一個具體的實現方法。在Java 1.5以前,有的人可能會用一種看起來似乎簡單的方法來完成類似的任務。比如:
這種實現的方法和前面提到的方法的區別之處在於它消除了抽象類和抽象函式,使用了條件判斷語句來給加減乘除四個列舉值以不同的方法。從效果上看是完全可以的,但這不是面向物件程式設計所提倡的。所以這種思想沒有被引入到Java 1.5中來。
方便的Switch功能
在Java 1.5以前,Switch語句只能和int, short, char以及byte這些資料型別聯合使用。現在,在Java 1.5的列舉中,你也可以方便的使用switch 語句了。比如:
(注:A, B, C, D, E, F, Incomplete是美國學校裡普遍採用的學習成績等級)
看了上面這個例子,大家肯定不禁要問:“是Java 1.5的Switch功能增強了嗎?”其實事情並不是這樣,我們看到的只是一個假象。在編譯器編譯完程式後,一切又回到從前了。Switch還是在整數上進行轉跳。下面就是反編譯後的程式片斷:
Java 1.5中列舉的本質
從上面我們列舉的例子一路看過來,到了這裡,讀者一定會問,為什麼Java 1.5的列舉和Java 1.5以前的“型別安全列舉”是那麼的相似呢(拋開Java 1.5中的Generics不說)?其實這非常好理解。大家也許注意到了這樣一個細節,Java 1.5中Enum的源程式的第一作者是Josh Bloch。這位Java大師在2001年出版的那本Java經典程式設計手冊《Effective Java Programming Language Guide》中的第五章裡,就已經全面清晰地闡述了“型別安全列舉”的核心思想和實現方法。Java 1.5中的列舉不是一種嶄新的思想,而是原有思想的一個實現和完善。其進步之處就在於將這些思想體現在了Java的編譯器中,程式設計師看到的是一個簡單,直觀的列舉服務,將原先需要手工完成或是藉助於第三方工具完成的任務直接的放在了編譯器中。
當然,Java 1.5中的列舉實現也不是“完美”的。它不是在Java虛擬機器層次實現的列舉,而是在編譯器層次實現的,本質上是一種“Java程式自動生成器”。這樣的實現的好處是不言而喻的。因為它對Java的虛擬機器沒有任何新的要求,這樣極大的減輕了Java虛擬機器的壓力,Java 1.5的虛擬機器不必要做大的改動,在以有的基礎上改進和完善就可以了。同時這種做法還使得程式有比較好的向前相容性。這一指導思想和Java Generics是完全一致的,在Java 編譯器層增加功能,而不是大的更動以有的Java虛擬機器。所以我們可以將Java 1.5中新增的列舉看作是一種的“語法糖衣(Syntax Sugar)” 當然,不在虛擬機器層次實現列舉在有些情況下會暴露一些問題,有時候不免會有一些不倫不類的地方,並且我們多多少少還是犧牲了一些功能。情形和我以前提到的Java Generics一樣 (請參看http://tech.ccidnet.com/pub/article/c1078_a170543_p1.html )。下面就讓我們討論一下幾個比較顯著的問題。
特殊的Enum類
從前面的例子中大家可以看出,所有的列舉型別都是隱式的衍生於基類java.lang.Enum。從表面上看,這個Enum類和其他的Java類沒有什麼區別。如果看一下我們前面給出的Enum源程式,那麼這個問題是非常顯而易見的。既然是一個普通的Java類,那麼我們可以不可以像其它類那樣使用呢?比若說,我們擴充套件一下這個Enum類,生成一個子類:
從語法上講,這樣的定義是完全合法的。但是如果你試圖編譯這段程式,Java的編譯器就會給出你錯誤資訊。也就是說Enum類可以內部隱式的被擴充套件,去不允許你直接顯式的去擴充套件。所以說這個Enum類似比較“特殊”的一個類,編譯器對它有“特殊政策”。從理論上講,這是一種不值得推薦的做法。明明是一個非Final的類,你卻不能去Extends它,這有悖於類最基本原則的。這是Java 1.5列舉實現中的一個瑕紕,不免叫人感到有些遺憾。
無法擴充套件的Enum型別
在很多的時候,我們希望我們定義的列舉有擴充套件能力,就像我們定義的其它類那樣。不管從邏輯的角度,還是從面向物件的程式設計原則來看,這個要求都是非常合理的。比如我們前面定義的Operation列舉,在那裡我們定義了加減乘除四個基本的運算。如果有一天我們想擴充套件一下這個列舉,加入取對數和乘方的能力,我們可能會很自然的想到這樣的方法:
很遺憾,當你試圖編譯這樣的程式的時候,編譯器會給出錯誤資訊,編譯不會通過。也就是說,我們失去了擴充套件一個列舉型別的能力。而在Java 1.5以前,我們是可以手工來完成這樣的工作的,比如說我們將Operation基類的建構函式定義為protected的,那麼我們就可以方便的擴充套件Operation類,其源程式為:
一.整數列舉替代法
比如說我們要定義一個春夏秋冬四季的列舉型別,如果使用整數來模擬,其樣子大概為:
public class Season { public static final int SPRING = 0; public static final int SUMMER = 1; public static final int FALL = 2; public static final int WINTER = 3; } |
對於上面這段程式大家可能不會陌生,因為你可能在你的程式中已經多次使用了這樣的整數型別列舉。儘管這是非常普遍的一種列舉替代品,但這並不說明它是一種好的替代品。這種簡單的方法有很多嚴重的問題。
問題1:型別安全問題
首先,使用整數我們無法保證型別安全問題。比如我我們設計一個函式,我們的意圖是讓呼叫者傳入春夏秋冬之中的某一個值,但是,使用 “整數列舉”我們無法保證使用者不傳入其它意想不到的值。如下所示:
…… public void seasonTest(int season) { //season 應該是 Season.SPRING,Season.SUMMER, Season.FALL, Season.WINTER //但是我們無法保證這一點 …… } public void foo() { seasonTest(Season.SUMMER); //理想中的使用方法 seasonTest(5); //錯誤呼叫 } …… |
程式seasonTest(Season.SUMMER)是我們期望的使用方式,而seasonTest(5)是一個明顯的錯誤,但是在編譯的時候,編譯器會認為這是合法的函式呼叫而給於通過。這顯然是不符合Java型別安全的宗旨的。
問題2:字串的表達問題
使用列舉的大多數場合,我們需要很方便的得到列舉型別的字元表達形式,比如Spring, Summer,Fall,Winter,甚至是漢語的春,夏,秋,冬。但這種整數型別的列舉和字元沒有任何聯絡,我們要使用一些其他輔助函式來達到這樣的效果,顯得不夠方便,也就是外國人講的不是一個“Generic solution”。
…… public String getSeasonString(int season) { If(season == Season.SPRING) return “Spring; else If(season == Season.SUMMRT) return “Summer; …… } |
二.型別安全類替代法
比較好的Enum替代品是一種被叫做型別安全的列舉方法。雖然不同的人的具體實現可能會有些不同,但它們的核心思想是一致的。讓我們先看一個簡單的例子。
public class Season { private Season(String s) { m_name = s; } public String toString() { return m_name; } private final String m_name; public static final Season SPRING = new Season("Spring"); public static final Season SUMMER = new Season("Summer"); public static final Season FALL = new Season("Fall"); public static final Season WINTER = new Season("Winter"); } |
它的特點是:
1. 定義一個類,用這個類的例項來表達列舉值
2. 不提供公開建構函式以杜絕客戶自己生成該類的例項
3. 所有的類的例項都是final的,不允許有任何改動
4. 所有的類的例項都是public static的,這樣客戶可以直接使用它
5. 所有的列舉值都是唯一的,所以程式中可以使用==運算子,而不必使用費時的equals()方法
以上這些特點保證了型別安全。如果有這樣的呼叫程式
public class ClientProgam { …. public String myFunction(Season season) { …… } } |
那麼我們可以放心的是,myFunction方法傳入的引數一定是Season型別,絕對不可能是其他型別。而具體的值只能是我們給出的春夏秋冬的某一個(唯一的例外就是傳入一個null。那是一個其它性質的問題,是所有Java程式共有的,不是我們今天討論的話題)。這不就是使用列舉的最根本初衷嗎!
它的缺點是:
1. 不夠直觀,不夠簡潔
2. 有些情況下不如整數方便,比如不能使用switch語句
3. 記憶體開銷比整數型的要大,雖然對於大部分Java程式這不是一個問題,但對於Java移動裝置卻可能會是一個潛在的問題
比較完整的實現
上面的源程式是一個最基本的框架。在現實的程式開發中,我們會給它增加一些東西,使它更完善,更便於使用。最常見的是增加一個整數變數,來表示列舉值的先後順序或是大小級別,英文裡叫做Ordinal。這樣我們就可以在各個列舉值之間可以進行比較了。另外我們可能會需要得到這個列舉的所有值來進行遍歷或是迴圈等操作。有時候我們可能還希望給出一個字串(比如Summer)而得到相應的列舉類。如果將這些常見的要求加到我們的具體實現中,那麼我們上面的那個程式將會擴充套件為:
public class Season implements Comparable { private Season(String s) { m_ordinal = m_nextOrdinal++; m_name = s; } public String toString() { return m_name; } public String Name() { return m_name; } public int compareTo(Object obj) { return m_ordinal - ((Season)obj).m_ordinal; } public static final Season[] values() { return m_seasons; } public static Season valueOf(String s) { for(int i = 0; i < m_seasons.length; i++) if(m_seasons[i].Name().equals(s)) return m_seasons[i]; throw new IllegalArgumentException(s); } private final String m_name; private static int m_nextOrdinal = 0; private final int m_ordinal; public static final Season SPRING; public static final Season SUMMER; public static final Season FALL; public static final Season WINTER; private static final Season m_seasons[]; static { SPRING = new Season("Spring"); SUMMER = new Season("Summer"); FALL = new Season("Fall"); WINTER = new Season("Winter"); m_seasons = (new Season[] { SPRING, SUMMER, FALL, WINTER }); } } |
上面給出的這個例子雖然比較好的解決了我們對列舉型別的基本要求,但它顯然不夠簡潔。如果我們要寫很多這樣的列舉類,那將會是一個不小的任務。並且重複的寫類似的程式是非常枯燥和容易犯錯誤的。為了將程式設計師從這些繁瑣的工作中解放出來,人們開發了一些工具軟體來完成這些重複的工作。比如比較流行的JEnum,請參看《再談在Java中使用列舉》(http://tech.ccidnet.com/pub/article/c1078_a95621_p1.html )
這些工具軟體其實是一些“程式生成器”。你按照它的語法規則定義你的列舉(語法相對簡單直觀),然後執行這些工具軟體,它會將你的定義轉化為一個完整的Java類,就像我們上面所寫的那個程式一樣。看到這裡,讀者也許會想:“為什麼不能將這種工具軟體的功能放到Java編譯器中呢?那樣我們不是就可以簡單方便的定義枚舉了嗎?而具體的Java類程式由編譯器來生成,我們不必再手工完成那麼冗長的程式行,也不必使用什麼第三方工具來生成這樣的程式行了嗎?”如果你有這樣的想法,那麼就要恭喜你了。因為你和Java的設計開發人員想到一塊兒去了。好,現在就讓我們來看看Java 1.5中新增的列舉的功能。
Java 1.5的列舉型別
在新的Java 1.5中,如果定義我們上面提到的春夏秋冬四季的列舉,那麼語句非常簡單,如下所示:
public enum Season { SPRING, SUMMER, FALL, WINTER } |
怎麼樣,夠簡單了吧。在我們全面展開討論之前,讓我們先從這個小例子中我們看一下在Java 1.5中定義一個列舉型別的基本要求。
1. 使用關鍵字enum
2. 型別名稱,比如這裡的Season
3. 一串允許的值,比如上面定義的春夏秋冬四季
4. 列舉可以單獨定義在一個檔案中,也可以嵌在其它Java類中
除了這樣的基本要求外,使用者還有一些其他選擇
1.列舉可以實現一個或多個介面(Interface)
2.可以定義新的變數
3.可以定義新的方法
4.可以定義根據具體列舉值而相異的類
這些選項的具體使用和特點我們會在後面的例子中逐步提到。那麼這樣的小程式和我們前面提到的“型別安全列舉替代”方案有什麼內在聯絡呢?從表面上看,好像大相徑庭,但如果我們深入一步就會發現這兩者的本質幾乎是一模一樣的。怎麼樣,有些吃驚嗎?把上面的列舉編譯後,我們得到了Season.class。我們把這個Season.class反編譯後就會發現Java 1.5列舉的真實面目,其反編譯後的源程式為:
public final class Season extends Enum { public static final Season[] values() { return (Season[])$VALUES.clone(); } public static Season valueOf(String s) { Season aseason[] = $VALUES; int i = aseason.length; for(int j = 0; j < i; j++) { Season season = aseason[j]; if(season.name().equals(s)) return season; } throw new IllegalArgumentException(s); } private Season(String s, int i) { super(s, i); } public static final Season SPRING; public static final Season SUMMER; public static final Season FALL; public static final Season WINTER; private static final Season $VALUES[]; static { SPRING = new Season("SPRING", 0); SUMMER = new Season("SUMMER", 1); FALL = new Season("FALL", 2); WINTER = new Season("WINTER", 3); $VALUES = (new Season[] { SPRING, SUMMER, FALL, WINTER }); } } |
對比一下這個例子和我們前面給出的“型別安全列舉”,你會發現他們幾乎是同出一轍。比較顯著的一個區別是Java 1.5的所有列舉都是Enum型別的衍生子類。但如果你看看Enum類的源程式,你就會發現它只不過是提供了一些基本服務的基類,就本質而言,Java 1.5的列舉和我們所說的“型別安全列舉”是一致的。
/* * @(#)Enum.java 1.12 04/06/08 * * Copyright 2004 Sun Microsystems, Inc. All rights reserved. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */ package java.lang; import java.io.Serializable; /** * This is the common base class of all Java language enumeration types. * * @author Josh Bloch * @author Neal Gafter * @version 1.12, 06/08/04 * @since 1.5 */ public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { private final String name; public final String name() { return name; } private final int ordinal; public final int ordinal() { return ordinal; } protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; } public String toString() { return name; } public final boolean equals(Object other) { return this==other; } public final int hashCode() { return System.identityHashCode(this); } protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } public final int compareTo(E o) { Enum other = (Enum)o; Enum self = this; if (self.getClass() != other.getClass() && // optimization self.getDeclaringClass() != other.getDeclaringClass()) throw new ClassCastException(); return self.ordinal - other.ordinal; } public final Class<E> getDeclaringClass() { Class clazz = getClass(); Class zuper = clazz.getSuperclass(); return (zuper == Enum.class) ? clazz : zuper; } public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { T result = enumType.enumConstantDirectory().get(name); if (result != null) return result; if (name == null) throw new NullPointerException("Name is null"); throw new IllegalArgumentException( "No enum const " + enumType +"." + name); } } |
帶有引數的建構函式
現在讓我們來看一個比較複雜一點的例子,來進一步闡述Java 1.5列舉的本質。 下面這段程式是Sun公司提供的列舉示範程式,是用太陽系中九大行星來說明列舉的一種能力-- 即我們可以建立新的建構函式,而不是僅僅侷限於Enum的預設的那一個。我們還可以自己定義新的方法,常數等等。
public enum Planet { MERCURY (3.303e+23, 2.4397e6), VENUS (4.869e+24, 6.0518e6), EARTH (5.976e+24, 6.37814e6), MARS (6.421e+23, 3.3972e6), JUPITER (1.9e+27, 7.1492e7), SATURN (5.688e+26, 6.0268e7), URANUS (8.686e+25, 2.5559e7), NEPTUNE (1.024e+26, 2.4746e7), PLUTO (1.27e+22, 1.137e6); private final double mass; // in kilograms private final double radius; // in meters Planet(double mass, double radius) { this.mass = mass; this.radius = radius; } private double mass() { return mass; } private double radius() { return radius; } // universal gravitational constant (m3 kg-1 s-2) public static final double G = 6.67300E-11; double surfaceGravity() { return G * mass / (radius * radius); } double surfaceWeight(double otherMass) { return otherMass * surfaceGravity(); } } |
將這段程式編譯後然後再反編譯,我們得到了這樣的程式,也就是Java虛擬機器實際使用的源程式。
public final class Planet extends Enum { public static final Planet[] values() { return (Planet[])$VALUES.clone(); } public static Planet valueOf(String s) { Planet aplanet[] = $VALUES; int i = aplanet.length; for(int j = 0; j < i; j++) { Planet planet = aplanet[j]; if(planet.name().equals(s)) return planet; } throw new IllegalArgumentException(s); } private Planet(String s, int i, double d, double d1) { super(s, i); mass = d; radius = d1; } private double mass() { return mass; } private double radius() { return radius; } double surfaceGravity() { return (6.6729999999999999E-011D * mass) / (radius * radius); } double surfaceWeight(double d) { return d * surfaceGravity(); } public static final Planet MERCURY; public static final Planet VENUS; public static final Planet EARTH; public static final Planet MARS; public static final Planet JUPITER; public static final Planet SATURN; public static final Planet URANUS; public static final Planet NEPTUNE; public static final Planet PLUTO; private final double mass; private final double radius; public static final double G = 6.6729999999999999E-011D; private static final Planet $VALUES[]; static { MERCURY = new Planet("MERCURY", 0, 3.3030000000000001E+023D, 2439700D); VENUS = new Planet("VENUS", 1, 4.8690000000000001E+024D, 6051800D); EARTH = new Planet("EARTH", 2, 5.9760000000000004E+024D, 6378140D); MARS = new Planet("MARS", 3, 6.4209999999999999E+023D, 3397200D); JUPITER = new Planet("JUPITER", 4, 1.9000000000000001E+027D, 71492000D); SATURN = new Planet("SATURN", 5, 5.6879999999999998E+026D, 60268000D); URANUS = new Planet("URANUS", 6, 8.686E+025D, 25559000D); NEPTUNE = new Planet("NEPTUNE", 7, 1.0239999999999999E+026D, 24746000D); PLUTO = new Planet("PLUTO", 8, 1.2700000000000001E+022D, 1137000D); $VALUES = (new Planet[] { MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN, URANUS, NEPTUNE, PLUTO }); } } |
從反編譯的程式來看,這個Java 1.5的列舉的其實沒有任何神祕之處。它只不過是稍微改變了一下建構函式,增加了幾個變數和函式。我們前面提到的“型別安全列舉”一樣可以完成同樣的工作。
依附於具體變數之上的方法
下面這段小程式也是一個比較有代表性的例子。即在列出的加,減,乘,除四個列舉值中,每一個值都有其相應的類定義段(就是所謂的Value-Specific Class Bodies)。這樣,加減乘除四個列舉值就有了各自版本的eval函式實現方法。使用Java 1.5的Enum服務,其源程式為:
public enum Operation { PLUS { double eval(double x, double y) { return x + y; } }, MINUS { double eval(double x, double y) { return x - y; } }, TIMES { double eval(double x, double y) { return x * y; } }, DIVIDE { double eval(double x, double y) { return x / y; } }; abstract double eval(double x, double y); } |
這段程式看起來比較複雜,如果我們反編譯一下Java編譯器生成的Operation.class檔案,我們就發現其原理並不複雜。
public abstract class Operation extends Enum { public static final Operation[] values() { return (Operation[])$VALUES.clone(); } public static Operation valueOf(String s) { Operation aoperation[] = $VALUES; int i = aoperation.length; for(int j = 0; j < i; j++) { Operation operation = aoperation[j]; if(operation.name().equals(s)) return operation; } throw new IllegalArgumentException(s); } private Operation(String s, int i) { super(s, i); } abstract double eval(double d, double d1); public static final Operation PLUS; public static final Operation MINUS; public static final Operation TIMES; public static final Operation DIVIDE; private static final Operation $VALUES[]; static { PLUS = new Operation("PLUS", 0) { double eval(double d, double d1) { return d + d1; } }; MINUS = new Operation("MINUS", 1) { double eval(double d, double d1) { return d - d1; } }; TIMES = new Operation("TIMES", 2) { double eval(double d, double d1) { return d * d1; } }; DIVIDE = new Operation("DIVIDE", 3) { double eval(double d, double d1) { return d / d1; } }; $VALUES = (new Operation[] { PLUS, MINUS, TIMES, DIVIDE }); } } |
那麼在Java 1.5以前我們是怎樣解決這樣的問題的呢?如果使用面向物件的原則來解決這一問題,其源程式為:
public abstract class Operation { private final String m_name; Operation(String name) {m_name = name;} public static final Operation PLUS = new Operation("Plus"){ protected double eval(double x, double y){ return x + y; } } public static final Operation MINUS = new Operation("Minus"){ protected double eval(double x, double y){ return x - y; } } public static final Operation TIMES = new Operation("Times"){ protected double eval(double x, double y){ return x * y; } } public static final Operation DEVIDE = new Operation("Devide"){ protected double eval(double x, double y){ return x / y; } } abstract double eval (double x, double y); public String toString() {return m_name; } private static int m_nextOridnal = 0; private final int m_ordinal = m_nextOridnal++; private static final Operation[] VALUES = { PLUS, MINUS, TIMES, DEVIDE }; } |
這種方法的本質和Java 1.5中的Enum是一致的。就是定義一個抽象函式(abstract function),然後每一個Enum值提供一個具體的實現方法。在Java 1.5以前,有的人可能會用一種看起來似乎簡單的方法來完成類似的任務。比如:
public class Operation { private final String m_name; Operation(String name) {m_name = name;} public static final Operation PLUS = new Operation("Plus"); public static final Operation MINUS = new Operation("Minus"); public static final Operation TIMES = new Operation("Times"); public static final Operation DEVIDE = new Operation("Devide"); public double eval (double x, double y){ if(this == Operation.PLUS){ return x + y; } else if(this == Operation.MINUS){ return x - y; } else if(this == Operation.TIMES){ return x * y; } else if(this == Operation.DEVIDE){ return x / y; } return -1; } public String toString() {return m_name; } private static int m_nextOridnal = 0; private final int m_ordinal = m_nextOridnal++; private static final Operation[] VALUES = { PLUS, MINUS, TIMES, DEVIDE }; } |
這種實現的方法和前面提到的方法的區別之處在於它消除了抽象類和抽象函式,使用了條件判斷語句來給加減乘除四個列舉值以不同的方法。從效果上看是完全可以的,但這不是面向物件程式設計所提倡的。所以這種思想沒有被引入到Java 1.5中來。
方便的Switch功能
在Java 1.5以前,Switch語句只能和int, short, char以及byte這些資料型別聯合使用。現在,在Java 1.5的列舉中,你也可以方便的使用switch 語句了。比如:
public class EnumTest { public enum Grade {A,B,C,D,E,F,Incomplete } private Grade m_grade; public EnumTest(Grade grade) { this.m_grade = grade; testing(); } private void testing(){ switch(this.m_grade){ case A: System.out.println(Grade.A.toString()); break; case B: System.out.println(Grade.B.toString()); break; case C: System.out.println(Grade.C.toString()); break; case D: System.out.println(Grade.D.toString()); break; case E: System.out.println(Grade.E.toString()); break; case F: System.out.println(Grade.F.toString()); break; case Incomplete: System.out.println(Grade.Incomplete.toString()); break; } } public static void main(String[] args){ new EnumTest(Grade.A); } } |
(注:A, B, C, D, E, F, Incomplete是美國學校裡普遍採用的學習成績等級)
看了上面這個例子,大家肯定不禁要問:“是Java 1.5的Switch功能增強了嗎?”其實事情並不是這樣,我們看到的只是一個假象。在編譯器編譯完程式後,一切又回到從前了。Switch還是在整數上進行轉跳。下面就是反編譯後的程式片斷:
…… //從略 private void testing() { static class _cls1 { static final int $SwitchMap$EnumTest1$Grade[]; static { $SwitchMap$EnumTest1$Grade = new int[Grade.values().length]; try { $SwitchMap$EnumTest1$Grade[Grade.A.ordinal()] = 1; } catch(NoSuchFieldError nosuchfielderror) { } try { $SwitchMap$EnumTest1$Grade[Grade.B.ordinal()] = 2; } catch(NoSuchFieldError nosuchfielderror1) { } try { $SwitchMap$EnumTest1$Grade[Grade.C.ordinal()] = 3; } catch(NoSuchFieldError nosuchfielderror2) { } try { $SwitchMap$EnumTest1$Grade[Grade.D.ordinal()] = 4; } catch(NoSuchFieldError nosuchfielderror3) { } try { $SwitchMap$EnumTest1$Grade[Grade.E.ordinal()] = 5; } catch(NoSuchFieldError nosuchfielderror4) { } try { $SwitchMap$EnumTest1$Grade[Grade.F.ordinal()] = 6; } catch(NoSuchFieldError nosuchfielderror5) { } try { $SwitchMap$EnumTest1$Grade[Grade.Incomplete.ordinal()] = 7; } catch(NoSuchFieldError nosuchfielderror6) { } } } switch(_cls1..SwitchMap.EnumTest1.Grade[m_grade.ordinal()]) { case 1: // '\001' System.out.println(Grade.A.toString()); break; case 2: // '\002' System.out.println(Grade.B.toString()); break; case 3: // '\003' System.out.println(Grade.C.toString()); break; case 4: // '\004' System.out.println(Grade.D.toString()); break; case 5: // '\005' System.out.println(Grade.E.toString()); break; case 6: // '\006' System.out.println(Grade.F.toString()); break; case 7: // '\007' System.out.println(Grade.Incomplete.toString()); break; } } |
Java 1.5中列舉的本質
從上面我們列舉的例子一路看過來,到了這裡,讀者一定會問,為什麼Java 1.5的列舉和Java 1.5以前的“型別安全列舉”是那麼的相似呢(拋開Java 1.5中的Generics不說)?其實這非常好理解。大家也許注意到了這樣一個細節,Java 1.5中Enum的源程式的第一作者是Josh Bloch。這位Java大師在2001年出版的那本Java經典程式設計手冊《Effective Java Programming Language Guide》中的第五章裡,就已經全面清晰地闡述了“型別安全列舉”的核心思想和實現方法。Java 1.5中的列舉不是一種嶄新的思想,而是原有思想的一個實現和完善。其進步之處就在於將這些思想體現在了Java的編譯器中,程式設計師看到的是一個簡單,直觀的列舉服務,將原先需要手工完成或是藉助於第三方工具完成的任務直接的放在了編譯器中。
當然,Java 1.5中的列舉實現也不是“完美”的。它不是在Java虛擬機器層次實現的列舉,而是在編譯器層次實現的,本質上是一種“Java程式自動生成器”。這樣的實現的好處是不言而喻的。因為它對Java的虛擬機器沒有任何新的要求,這樣極大的減輕了Java虛擬機器的壓力,Java 1.5的虛擬機器不必要做大的改動,在以有的基礎上改進和完善就可以了。同時這種做法還使得程式有比較好的向前相容性。這一指導思想和Java Generics是完全一致的,在Java 編譯器層增加功能,而不是大的更動以有的Java虛擬機器。所以我們可以將Java 1.5中新增的列舉看作是一種的“語法糖衣(Syntax Sugar)” 當然,不在虛擬機器層次實現列舉在有些情況下會暴露一些問題,有時候不免會有一些不倫不類的地方,並且我們多多少少還是犧牲了一些功能。情形和我以前提到的Java Generics一樣 (請參看http://tech.ccidnet.com/pub/article/c1078_a170543_p1.html )。下面就讓我們討論一下幾個比較顯著的問題。
特殊的Enum類
從前面的例子中大家可以看出,所有的列舉型別都是隱式的衍生於基類java.lang.Enum。從表面上看,這個Enum類和其他的Java類沒有什麼區別。如果看一下我們前面給出的Enum源程式,那麼這個問題是非常顯而易見的。既然是一個普通的Java類,那麼我們可以不可以像其它類那樣使用呢?比若說,我們擴充套件一下這個Enum類,生成一個子類:
public class MyEnum extends Enum { protected Enum(String name, int ordinal); protected Object clone( ); } |
從語法上講,這樣的定義是完全合法的。但是如果你試圖編譯這段程式,Java的編譯器就會給出你錯誤資訊。也就是說Enum類可以內部隱式的被擴充套件,去不允許你直接顯式的去擴充套件。所以說這個Enum類似比較“特殊”的一個類,編譯器對它有“特殊政策”。從理論上講,這是一種不值得推薦的做法。明明是一個非Final的類,你卻不能去Extends它,這有悖於類最基本原則的。這是Java 1.5列舉實現中的一個瑕紕,不免叫人感到有些遺憾。
無法擴充套件的Enum型別
在很多的時候,我們希望我們定義的列舉有擴充套件能力,就像我們定義的其它類那樣。不管從邏輯的角度,還是從面向物件的程式設計原則來看,這個要求都是非常合理的。比如我們前面定義的Operation列舉,在那裡我們定義了加減乘除四個基本的運算。如果有一天我們想擴充套件一下這個列舉,加入取對數和乘方的能力,我們可能會很自然的想到這樣的方法:
public enum OperationExt extends Operation { LOG { double eval(double x, double y) { return Math.log(y) / Math.log(x);} }, POWER { double eval(double x, double y) { return Math.power(x,y);} }, } |
很遺憾,當你試圖編譯這樣的程式的時候,編譯器會給出錯誤資訊,編譯不會通過。也就是說,我們失去了擴充套件一個列舉型別的能力。而在Java 1.5以前,我們是可以手工來完成這樣的工作的,比如說我們將Operation基類的建構函式定義為protected的,那麼我們就可以方便的擴充套件Operation類,其源程式為:
abstract class OperationExt extends Opetation { private final String m_name; Operation(String name) {m_name = name;} public static final LOG = new Operation("Log"){ protected double eval(double x, double y){ return Math.log(y) / Math.log(x); } } public static final POWER = new Operation("Power"){ protected double eval(double x, double y){ return Math.log(x,y); } } protected double eval (double x, double y); public String toString() {return m_name; } private static int m_nextOridnal = 0; private final int m_ordinal = m_nextOridnal++; private static final Operation[] VALUES = { LOG, POWER; } } |
從這個意義上來說,Java 1.5的列舉給了我們一些束縛,使我們不再像以前那樣可以隨意的操作我們自己定義的類,這可以算是倒退了一小步。不過從總體上來說,Java 1.5的列舉是能滿足絕大部分程式設計師的要求的,它的簡明,易用的特點是很突出的,犧牲了一些東西還是值得的。如果你對列舉有超出一般的特殊要求,那麼你還是可以回到Java 1.5以前的老路上來,手工完成你的列舉類。同時你不用擔心太多,因為無論是使用Java 1.5的列舉服務,還是手工完成,其本質都是一樣的。為了進一步方便程式設計師對列舉的操作,Java 1.5中還提供了一些輔助類。比如大家今後可能會經常用到EnumMap和EnumSet類。這些類是對列舉功能的一個補充和完善。(T117)