讀古今文學網 > 編寫高質量代碼:改善Java程序的151個建議 > 建議89:枚舉項的數量限制在64個以內 >

建議89:枚舉項的數量限制在64個以內

為了更好地使用枚舉,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,否則建議拆分。