為了更好地使用枚舉,Java提供了兩個枚舉集合:EnumSet和EnumMap,這兩個集合的使用方法都比較簡單,EnumSet表示其元素必須是某一枚舉的枚舉項,EnumMap表示Key值必須是某一枚舉的枚舉項,由於枚舉類型的實例數量固定並且有限,相對來說EnumSet和EnumMap的效率會比其他Set和Map要高。
雖然EnumSet很好用,但是它有一個隱藏的特點,我們逐步分析。在項目中一般會把枚舉用作常量定義,可能會定義非常多的枚舉項,然後通過EnumSet訪問、遍歷,但它對不同的枚舉數量有不同的處理方式。為了進行對比,我們定義兩個枚舉,一個數量等於64,一個是65(大於64即可,為什麼是64而不是128、512呢?稍後解釋),代碼如下:
//普通枚舉項,數量等於64
enum Const{
A, B,C,……,PC, QC, RC;
}
//大枚舉,數量超過64
enum LargeConst{
A, B,C,……,KB, LB, MB;
}
Const中的枚舉項數量是64,LargeConst的數量是65,其中的……號代表省略的枚舉項(注意此處只是省略了,Java不支持省略號)。接下來我們希望把這兩個枚舉轉換為EnumSet,然後判斷一下它們的class類型是否相同,代碼如下:
public static void main(Stringargs){
//創建包含所有枚舉項的EnumSet
EnumSet<Const>cs=EnumSet.allOf(Const.class);
EnumSet<LargeConst>lcs=EnumSet.allOf(LargeConst.class);
//打印出枚舉項數量
System.out.println(\"Const枚舉項數量:\"+cs.size());
System.out.println(\"LargeConst枚舉項數量:\"+lcs.size());
//輸出兩個EnumSet的class
System.out.println(cs.getClass());
System.out.println(lcs.getClass());
}
程序很簡單,現在的問題是:cs和lcs的class類型是否相同?應該相同吧,都是EnumSet類的工廠方法allOf生成的EnumSet類,而且JDK API也沒有提示EnumSet有子類。我們來看輸出結果:
Const枚舉項數量:64
LargeConst枚舉項數量:65
class java.util.RegularEnumSet
class java.util.JumboEnumSet
很遺憾,兩者不相等。就差1個元素,兩者就不相等了?確實如此,這也是我們要重點關注枚舉項數量的原因。先來看看Java是如何處理的,首先跟蹤allOf方法,其源代碼如下:
public static<E extends Enum<E>>EnumSet<E>allOf(Class<E>elementType){
//生成一個空EnumSet
EnumSet<E>result=noneOf(elementType);
//加入所有的枚舉項
result.addAll();
return result;
}
allOf通過noneOf方法首先生成一個EnumSet對象,然後把所有的枚舉項都加進去,問題可能就出在EnumSet的生成上了,我們來看noneOf的代碼:
public static<E extends Enum<E>>EnumSet<E>noneOf(Class<E>elementType){
//獲得所有枚舉項
Enumuniverse=getUniverse(elementType);
if(universe==null)
throw new ClassCastException(elementType+\"not an enum\");
if(universe.length<=64)
//枚舉數量小於等於64
return new RegularEnumSet<E>(elementType, universe);
else
//枚舉數量大於64
return new JumboEnumSet<E>(elementType, universe);
}
看到這裡恍然大悟,Java原來是如此處理的:當枚舉項數量小於等於64時,創建一個RegularEnumSet實例對象,大於64時則創建一個JumboEnumSet實例對象。
緊接著的問題是:為什麼要如此處理呢?這還要看看這兩個類之間的差異,首先看RegularEnumSet類,代碼如下:
class RegularEnumSet<E extends Enum<E>>extends EnumSet<E>{
//記錄所有枚舉排序號,注意是long型
private long elements=0L;
//構造函數
RegularEnumSet(Class<E>elementType, Enumuniverse){
super(elementType, universe);
}
//加入所有元素
void addAll(){
if(universe.length!=0)
elements=-1L>>>-universe.length;
}
}
我們知道枚舉項的排序值ordinal是從0、1、2……依次遞增的,沒有重號,沒有跳號,RegularEnumSet就是利用這一點把每個枚舉項的ordinal映射到一個long類型的每個位上的,注意看addAll方法的elments元素,它使用了無符號右移操作,並且操作數是負值,位移也是負值,這表示是負數(符號位是1)的「無符號左移」:符號位為0,並補充低位,簡單地說,Java把一個不多於64個枚舉項的枚舉映射到了一個long類型變量上。這才是EnumSet處理的重點,其他的size方法、constains方法等都是根據elements計算出來的。想想看,一個long類型的數字包含了所有的枚舉項,其效率和性能肯定是非常優秀的。
我們知道long類型是64位的,所以RegularEnumSet類型也就只能負責枚舉項數量不大於64的枚舉(這也是我們以64來舉例,而不以128或512舉例的原因),大於64則由JumboEnumSet處理,我們看它是怎麼處理的:
class JumboEnumSet<E extends Enum<E>>extends EnumSet<E>{
//映射所有的枚舉項
private long elements;
//構造函數
JumboEnumSet(Class<E>elementType, Enumuniverse){
super(elementType, universe);
//默認長度是枚舉項數量除以64再加1
elements=new long[(universe.length+63)>>>6];
}
void addAll(){
//elements中每個元素表示64個枚舉項
for(int i=0;i<elements.length;i++)
elements[i]=-1;
elements[elements.length-1]>>>=-universe.length;
size=universe.length;
}
}
JumboEnumSet類把枚舉項按照64個元素一組拆分成了多組,每組都映射到一個long類型的數字上,然後該數組再放置到elements數組中。簡單來說JumboEnumSet類的原理與RegularEnumSet相似,只是JumboEnumSet使用了long數組容納更多的枚舉項。
不過,你會不會覺得這兩段程序看著很讓人鬱悶呢?其實這是因為我們在開發中很少用到位移操作。讀者可以這樣理解,RegularEnumSet是把每個枚舉項編碼映射到一個long類型數字的每個位上,JumboEnumSet是先按照64個一組進行拆分,然後每個組再映射到一個long類型數字的每個位上,從這裡我們也可以看出數字編碼的奧秘!
從以上的分析可以看出,EnumSet提供的兩個實現都是基本的數字類型操作,其性能肯定比其他的Set類型要好很多,特別是Enum的數量少於64的時候,那簡直就是飛一般的速度。
注意 枚舉項數量不要超過64,否則建議拆分。