首页 » 编写高质量代码:改善Java程序的151个建议 » 编写高质量代码:改善Java程序的151个建议全文在线阅读

《编写高质量代码:改善Java程序的151个建议》建议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,否则建议拆分。