笔者前阵子写了一篇关于java集合Set的博客,在看到EnumSet的源码时,觉得还是说一说java中关于枚举这个类型,否则很难说的清楚。这片博客主要说一下Java中枚举的实际存在形式,同时再看一看EnumSet和EnumMap的源码。
Enum
我们都知道Enum类型在编译时class类型一样,都会被变异成字节码文件,我们就来看看在编译过程中编译器到底做了什么,编译成的字节码问价又是什么样的。
我们先新建一个Enum类。
1 | public enum DateType { |
然后用javac工具将它编译成字节码文件,我们来看看这个字节码文件是什么样的。java提供了一个反编译工具javap,输入javap -c DateTye我们可以看到完成字节码文件。其内容如下:
1 | D:\jad>javap -c dateType |
上面都是些用于jvm执行的字节码指令,笔者是完全看不懂,但javap还可以看到反编译结果,输入命令javap DateType,可以看到下面这些输出:
1 | D:\jad>javap dateType |
这看着就清爽多了,我们可以看到Enum类型其实本质还是class类型。它继承了java中Enum类型,并且枚举中的每一个实例都是以它本身类型的内部类实现的。但看到这里比较尴尬的是这些只有一些声明的信息,具体的实现我们看不到。但是最后笔者并没有找到javap还有其他的参数能查看到具体的实现信息,只能归咎于javap的功能有限。不过所幸的是还有网络,通过搜索笔者找到了一个第三方工具jad。它可以直接将一个class文件反编译成java文件,下面就是这个反编译成的java文件的内容。
1 | // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. |
这次反编译的结果就具体多了,我们可以看到编译器自动给我们增加了valueOf和values两个函数,这两个是静态方法,可以直接通过类名调用。所有的内部枚举类型都被编译成了静态的内部类,并且在一个static块中初始化了它们。我们还看到他还有一个私有的构造函数,熟悉单例模式的同学都知道有一种单例模式就是通过枚举实现的,从上面的代码我们能够看出它完全符合单例模式的定义,并且是线程安全的。既然枚举都是继承于Enum的,我们就看一下Enum的代码,了解一下初始化时传入的两个参数是啥。
1 | protected Enum(String name, int ordinal) { |
由于代码篇幅较长,笔者这里只粘一下构造函数的代码简单了解一下。可以看出之前反编译的代码中的构造函数传入的参数分别是枚举类名,和它对应的序号,序号从0开始。便于通过values数组来访问它们。
EnumSet
EnumSet是Jdk1.5引入的,用来存在枚举类型的Set,EnumSet是个抽象类,具体实现在它的子类中,JDK中子类有两个,RegularEnumSet和JumboEnumSet。EnumSet,没有提供公有的额构造方法,要创建EnumSet需要调用它提供的一些静态方法。它有一个主要的静态方法,其余方法皆要调用此方法实现。下面看看这个方法:
1 | public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) { |
这个方法用于根据传入的枚举类型生成一个空的EnumSet。可见它首先调用了getUniverse方法,这个方法用来返回这个枚举类型中的实例数组。下面看看这个方法:
1 | /** |
这段代码点进去看的话,会发现有点复杂,这里我们就不继续深入了。我们看看这个注释,可以明白这个函数返回这个枚举类型中的实例数组(可能调用了枚举的values方法),并且这些实例采用了堆内缓存来提升性能。
继续看上面那段代码,发现它根据枚举实例的数目做了分叉,若在64之内,则生成RegularEnumSet,否则,生成JumboEnumSet。一般情况下应该第一个调用的比较多(数目大于64的枚举笔者还没遇到过)。值得一提的是在这些Enum集合中枚举实例其实都是存放在一个枚举数组中的(比如这里的universe和下面map的keyUniverse),而这个数组其实在EnumSet被初始化时就已经被创建,那么EnumSet的add操作到底做了些啥呢?我们来看看下面的代码:
1 | public boolean add(E e) { |
在RegularEnumSet中有一个变量elements,这个变量是一个long型的二进制数(JumboEnumSet是一个long型数组),即是一串64位的二进制数,说到这里有些同学可能已经猜到EnumSet是如何判断某个枚举是否存进set的了。起始这个elements全是0,当add进来一个枚举时,用这个枚举的序号对1进行左移运算,然后再与elements进行或运算。这个运算的结果就是,这个64位的二进制数分别对应着枚举的序号,如果相应枚举序号对应的位置变为1,则说明这个位置已经存在枚举值了。
EnumMap
EnumMap是一种以枚举为key的map,平常用的不多。但在某些特殊的场景下还是能发挥作用,当然可能我们也可以用数组来替代它,但终究不如枚举来的清晰明朗。下面我们通过源码来认识一下这个map。
1 | /** |
上面列出的是EnumMap的几个比较重要的属性。通过注释我们能明白,keyType表示key对应的枚举类型,keyUniverse表示这个枚举类型中所有的枚举值,vals是一个存储value的数组,它的长度和keyUniverse是一样的。因为key是无法重复的,它的值只能是这个枚举类型中的枚举值。size表示数组的长度,它不会大于keyUniverse的长度。
我们来看一下EnumMap的构造函数。EnumMap有三个公有的构造函数,我们只看其中一个。其实大同小异,主要也都是对上面四个属性的初始化。源码如下:
1 | /** |
可见vals的长度和keyUniverse的长度是一致的。同时还有一点要提的是,在向EnumMap中放入为null的value值时,EnumMap会将它替换为NULL对象,这是EnumMap的一个内部类。源码如下:
1 | /** |
并且EnumMap定义了两个方法用于null与NULL对象之间的替换。源码如下:
1 | private Object maskNull(Object value) { |
既然是集合,下面我们看看它的增删查方法。
1 | public V put(K key, V value) { |
可见value是存在val数组中的,并且数组的下标是枚举类型中key的序号。在存入value值时会掉用maskNull方法,将null转化为NULL对象,在返回时会将NULL对象转为null。因为null和Null的不同,所以检查oldValue是否为null,可以判断当前位置是否是第一次存入值,而不是新旧值的替换,以此来改变size的值。同时我们发现它还会对key进行校验,我们来看看typeCheck的代码:
1 | /** |
可见这个方法是判断当前key的类型是否是在初始化时传入的枚举类型或者是它的子类。同时我们可以看到它直接调用了key的getclass方法,所以传入的也key不能为null。否者会抛出空指针异常。
下面看看get方法:
1 | public V get(Object key) { |
可见它会判断key是否合法,合法这返回value值,否则返回null。我们来看看isValidKey方法。
1 | private boolean isValidKey(Object key) { |
也是判空和枚举类型校验。下面看看remove方法:
1 | public V remove(Object key) { |
我们来分析一下这段代码,首先它会判断key的合法性,如何判断上面已经说了,这里不再赘述。然后找到key所在的vals下标,将它赋值为null。下面会进行一个判断,如果oldValue本来就为null,说明这个key所对应的vals位置本来就没有值(即还没有put进去过),此时相当于什么都没做,那么size值不变。否则改变size的值。
结束语
这篇博客对枚举和它对应的集合类进行了简单介绍。由于笔者也是边看边写,逻辑可能有些混乱;并且由于笔者水平有限,很多地方没有深耕,可能还有些错误疏漏。读者不可全信,当自己查证。若发现疏漏处,欢迎在评论处指出,不胜感激。