为了便于理解EnumSet,需要先知道这种数据结构是为了解决什么问题而设计的。
一、场景
举例一个场景,你系统里的用户会有各种身份,你设计了一个UserDTO对象,这时候要考虑怎么来表示身份,有一种比较常用的方法,比如定义一个Long属性,long类型有64位,每一位对应一种身份,1标识拥有该身份,0标识没有该身份。这样做就比较节省空间,那么在对这个UserDTO增加身份、删除身份、判断身份时,都需要自己做位运算,比较繁琐并且容易出错,而EnumSet就是为了解决这种场景来设计的,比如在UserDTO里设计一个EnumSet类型的属性来标识身份,再定义一个枚举类来标识身份,代码如下:
public class UserDTO {
private Long userId;
private String name;
/**
* 身份
*/
private EnumSet<IdentityEnu> identities;
// 身份枚举
enum IdentityEnu{
TEACHER,DRIVER,WIFE,MAN,RICHER,VIP;
}
}
这时你对UserDTO的身份的新增可以用EnumSet的add,删除可以用remove,判断可以用contain,不用自己操作位运算!这是实际解决的场景,在脑海中有了大概的认识后再看下源码。
二、源码解析
主要也是从构造方法、add、remove、contain这几个主要方法分析下源码。
构造方法:
EnumSet使用静态工厂来代替了公共的构造方法
这里根据参数个数不同定义了多个,拿其中一个来看下就可以了。代码如下:
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3) {
EnumSet<E> result = noneOf(e1.getDeclaringClass());
result.add(e1);
result.add(e2);
result.add(e3);
return result;
}
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
// 获取枚举类里定义的枚举个数
Enum<?>[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");
if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe);
else
return new JumboEnumSet<>(elementType, universe);
}
这里getUniverse方法的含义是判断下你传入的枚举里面定义了多少个枚举值,比如我的代码里的IdentityEnu 里定义了6个,那么这时候返回的就是RegularEnumSet这个子类,这里有两个疑问点:
1、为什么和64比较
2、为什么返回的是两个子类,有什么区别?
带着这两个疑问进到这两个子类源码里看下
截图里狂欢粗了两个最主要的区别,RegularEnumSet使用的是一个long属性来存储,而JumboEnumSet是用一个long的数组来存储,这也是为什么和64做比较的原因,因为long只有64位,最多标识64个枚举值,多的只能用数组来存储,这里解释了上面的两个问题,下面再看下是怎么来存储的?
这个可以从add、remove方法的源码里找答案,以RegularEnumSet为例:
add方法
/**
* Adds the specified element to this set if it is not already present.
*
* @param e element to be added to this set
* @return <tt>true</tt> if the set changed as a result of the call
*
* @throws NullPointerException if <tt>e</tt> is null
*/
public boolean add(E e) {
typeCheck(e);
long oldElements = elements;
// 通过传入的Enum值得下标来做位运算
elements |= (1L << ((Enum<?>)e).ordinal());
return elements != oldElements;
}
这里可以看到add操作实际是用位运算,将这个long值对应你传入的枚举值的下标的那个bit位改成1,比如我传入的是IdentityEnu.DRIVER, 对应的ordinal是1,则是将elements的bit位中的第2位改成1,对于位运算符看不懂的同学可以自己查下就能理解了,看到这里应该能明白EnumSet的原理及使用场景了,而对于remove和contain就不用详细解释了。好处就是只用long就可以标识64中状态,节省空间,并且通过位操作来做处理,性能也很快。
题外思考:
为什么EnumSet提供的静态工厂类返回的实际是两个子类RegularEnumSet和JumboEnumSet,并且这两个类是protected的?
这两个子类只是对于EnumSet的接口的实现方式不同,不同的地方就是根据调用方的Enum的值得个数来决定的,对于调用方来说是不需要关心这些细节的,所以这两个子类只需要是protected就可以,并且通过静态工厂类返回给调用方,以后如果有更牛逼的算法出现时,只需要再扩展出一个子类就可以了,而调用方是无感知的,比较符合开闭原则。