最近项目原因可能会用到EnumSet,于是周末利用闲暇时间阅读了一下EnumSet的源码,对源码中的类说明和方法说明根据自己的理解进行了翻译,水平有限,其中翻译不对的地方希望热心的朋友能够批评指正。
EnumSet是一个抽象类,它有两个子类:RegularEnumSet和JumboEnumSet,这两个子类具体特性以后再做分析,这里先学习一下EnumSet这个抽象类。
1、EnumSet是专门用于枚举类型的一种Set实现,EnumSet里所有的元素都来自同一个枚举类,并且在Set创建的时候就需要显式或隐式地指定这个枚举类。EnumSet内部是使用位向量实现的,这种表示方法非常的紧凑和高效。这个类的空间和时间性能都很好,而且具有高质量、类型安全等优点,足以代替之前的基于int的位标识。对于EnumSet这种Collection而言,即使像containsAll和retainAll这种算法比较复杂的运算执行起来也会特别快。
JDK原文:
A specialized implementation for use with enum types. All of the elements in an enum set must come from a single enum type that is specified, explicitly or implicitly, when the set is created. Enum sets are represented internally as bit vectors. This representation is extremely compact and efficient. The space and time performance of this class should be good enough to allow its use as a high-quality, typesafe alternative to traditional int-based “bit flags.” Even bulk operations (such as containsAll and retainAll) should run very quickly if their argument is also an enum set.
2、EnumSet的iterator方法返回的迭代器(Iterator)遍历顺序是其自然顺序(枚举常量的声明顺序)。得到的迭代器是弱一致性的:永不会抛出ConcurrentModificationException异常,迭代过程中对Set的修改可能会对Set产生影响,也可能不会。
JDK原文:
The iterator returned by the iterator method traverses the elements in their natural order (the order in which the enum constants are declared). The returned iterator is weakly consistent: it will never throw {@link ConcurrentModificationException} and it may or may not show the effects of any modifications to the set that occur while the iteration is in progress.
3、EnumSet中不允许有Null元素,插入Null元素的时候会抛出
NullPointerException异常,然而测试Set中是否有Null元素或者尝试从Set中移除一个Null元素是不会抛异常的。
JDK原文:
Null elements are not permitted. Attempts to insert a null element will throw NullPointerException. Attempts to test for the presence of a null element or to remove one will, however, function properly.
4、像其他大多数collection具体实现类一样,EnumSet是非线程安全的。如果多个线程并发访问同一个EnumSet,并且至少有一个线程对它进行修改,它就应该从外部进行同步。通常的做法是将封装了这个EnumSet的对象进行同步。如果没有这样的对象,就应该通过Collections.synchronizeSet(set)这种方式将这个set同步。最好在创建的时候就调用上面的静态方法将这个Set同步,以免发生非同步访问。就像下面这样:
Set<MyEnum> s = Collections.synchronizedSet(EnumSet.noneOf(MyEnum.class));
JDK原文:
Like most collection implementations, EnumSet is not synchronized. If multiple threads access an enum set concurrently, and at least one of the threads modifies the set, it should be synchronized externally. This is typically accomplished by synchronizing on some object that naturally encapsulates the enum set. If no such object exists, the set should be “wrapped” using the Collections#synchronizedSet method. This is best done at creation time, to prevent accidental unsynchronized access:
Set<MyEnum> s = Collections.synchronizedSet(EnumSet.noneOf(MyEnum.class));
5、实现注意:所有基本操作的时间复杂度都是O(1),这些操作执行起来比HashSet快得多(这一点并不保证)。当一些时间复杂度较高的方法的参数是EnumSet的时候,他们的实际时间复杂度也将是O(1)。
JDK原文:
Implementation note: All basic operations execute in constant time. They are likely (though not guaranteed) to be much faster than their HashSet counterparts. Even bulk operations execute in constant time if their argument is also an enum set.
原文中提到,EnumSet的存储方式是“位标志”,那么何谓“位标志”呢?我们都知道在Java中,一个byte的长度是8位,即00000000,假设每一位都能用来表示一盏灯的亮与灭,我们为这8盏灯从右向左分别从1到8编号,那么,
00000001表示1号灯亮,
00000010表示2号灯亮,
00000100表示3号灯亮,
……
将他们翻译成10进制则就是:
1表示1号灯,
2表示2号灯,
4表示3号灯,
……
我们可以定义一个枚举类:
public enum Lamp {
lamp1((byte)1), lamp2((byte)2),
lamp3((byte)4), lamp4((byte)8),
lamp5((byte)16), lamp6((byte)32),
lamp7((byte)64), lamp8((byte)128);
private byte idx;
private Lamp(byte idx) {
this.idx = idx;
}
public byte idx() {
return idx;
}
}
当需要一组来自这个枚举中的几个值的时候,比如需要将号码为奇数的灯放到一个集合的时候,我们先来看一下都需要几号灯:1、3、5、7,对应的枚举值是1、4、16、64,再来看一下他们的二进制表示:
00000001
00000100
00010000
01000000
可以发现在一列上1最多出现一次(因为我们就是按照这个规律去编号的),若将这几个数字按位或操作,会得到这样一个二进制:
01010101
再结合刚刚的命名规则,能够明显地看出来这个“集合”中都包含哪些灯。
这样我们仅仅使用一个short,就能表示拥有8个枚举值的枚举对象的集合,正如段落1中所言,占用空间非常小。那么为何说它性能优越呢?我们看一下前面所提到的containsAll方法,为了方便比较我们这里结合RegularEnumSet子类进行分析。
public boolean containsAll(Collection<?> c) {
if (!(c instanceof RegularEnumSet))
return super.containsAll(c);
RegularEnumSet<?> es = (RegularEnumSet<?>)c;
if (es.elementType != elementType)
return es.isEmpty();
return (es.elements & ~elements) == 0;
}
上面是RegularEnumSet的containsAll方法的源码,除了前面的验证性的判断之外,只需要关注最后的核心部分,那就是return的值:(es.elements & ~elements) == 0;
我们先分析一下其实现原理。
比如我们要判断集合A是否包含所有的集合B的元素,就使用A.containsAll(B)进行判断,那么,elements就是A的所有元素,es.elements就是B的所有元素,return后的表达式可以简化为B & ~A == 0。
沿用上面“灯”的实例,假设A有1、3、5、7四盏灯,B有1、3两盏灯,则A、B对应的二进制分别是:
A 和B —–> ~A和B
01010101 —-> 10101010
00000101 —-> 00000101
很明显B & ~A 结果为0。
其实从二进制来说,A包含B中所有的元素就是说B中所有是1的位置,A中也是1,将A取反,就会变成B中所有是1的位置,A中全是0,这时将B与~A进行与操作,结果必然是0。
原理上理解之后,我们发现只需要一个与操作,就能实现其他集合方法中时间复杂度非常高的containsAll操作,其执行时间也是常量级的,因此EnumSet的具有高效性。
接下来将会结合JDK源码学习一下EnumSet的各个方法。