浅析枚举

笔者前阵子写了一篇关于java集合Set的博客,在看到EnumSet的源码时,觉得还是说一说java中关于枚举这个类型,否则很难说的清楚。这片博客主要说一下Java中枚举的实际存在形式,同时再看一看EnumSet和EnumMap的源码。

Enum

我们都知道Enum类型在编译时class类型一样,都会被变异成字节码文件,我们就来看看在编译过程中编译器到底做了什么,编译成的字节码问价又是什么样的。

我们先新建一个Enum类。

1
2
3
4
5
6
7
8
9
10
public enum DateType {
YEAR,
MONTH,
DAY,
HOUR,
MINUTE,
SECOND;
private DateType() {
}
}

然后用javac工具将它编译成字节码文件,我们来看看这个字节码文件是什么样的。java提供了一个反编译工具javap,输入javap -c DateTye我们可以看到完成字节码文件。其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
D:\jad>javap -c dateType
警告: 二进制文件dateType包含com.ins.car.common.constant.DateType
Compiled from "DateType.java"
public final class com.ins.car.common.constant.DateType extends java.lang.Enum<com.ins.car.common.constant.DateType> {
public static final com.ins.car.common.constant.DateType YEAR;

public static final com.ins.car.common.constant.DateType MONTH;

public static final com.ins.car.common.constant.DateType DAY;

public static final com.ins.car.common.constant.DateType HOUR;

public static final com.ins.car.common.constant.DateType MINUTE;

public static final com.ins.car.common.constant.DateType SECOND;

public static com.ins.car.common.constant.DateType[] values();
Code:
0: getstatic #1 // Field $VALUES:[Lcom/ins/car/common/constant/DateType;
3: invokevirtual #2 // Method "[Lcom/ins/car/common/constant/DateType;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[Lcom/ins/car/common/constant/DateType;"
9: areturn

public static com.ins.car.common.constant.DateType valueOf(java.lang.String);
Code:
0: ldc #4 // class com/ins/car/common/constant/DateType
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class com/ins/car/common/constant/DateType
9: areturn

static {};
Code:
0: new #4 // class com/ins/car/common/constant/DateType
3: dup
4: ldc #7 // String YEAR
6: iconst_0
7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #9 // Field YEAR:Lcom/ins/car/common/constant/DateType;
13: new #4 // class com/ins/car/common/constant/DateType
16: dup
17: ldc #10 // String MONTH
19: iconst_1
20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
23: putstatic #11 // Field MONTH:Lcom/ins/car/common/constant/DateType;
26: new #4 // class com/ins/car/common/constant/DateType
29: dup
30: ldc #12 // String DAY
32: iconst_2
33: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
36: putstatic #13 // Field DAY:Lcom/ins/car/common/constant/DateType;
39: new #4 // class com/ins/car/common/constant/DateType
42: dup
43: ldc #14 // String HOUR
45: iconst_3
46: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
49: putstatic #15 // Field HOUR:Lcom/ins/car/common/constant/DateType;
52: new #4 // class com/ins/car/common/constant/DateType
55: dup
56: ldc #16 // String MINUTE
58: iconst_4
59: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
62: putstatic #17 // Field MINUTE:Lcom/ins/car/common/constant/DateType;
65: new #4 // class com/ins/car/common/constant/DateType
68: dup
69: ldc #18 // String SECOND
71: iconst_5
72: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
75: putstatic #19 // Field SECOND:Lcom/ins/car/common/constant/DateType;
78: bipush 6
80: anewarray #4 // class com/ins/car/common/constant/DateType
83: dup
84: iconst_0
85: getstatic #9 // Field YEAR:Lcom/ins/car/common/constant/DateType;
88: aastore
89: dup
90: iconst_1
91: getstatic #11 // Field MONTH:Lcom/ins/car/common/constant/DateType;
94: aastore
95: dup
96: iconst_2
97: getstatic #13 // Field DAY:Lcom/ins/car/common/constant/DateType;
100: aastore
101: dup
102: iconst_3
103: getstatic #15 // Field HOUR:Lcom/ins/car/common/constant/DateType;
106: aastore
107: dup
108: iconst_4
109: getstatic #17 // Field MINUTE:Lcom/ins/car/common/constant/DateType;
112: aastore
113: dup
114: iconst_5
115: getstatic #19 // Field SECOND:Lcom/ins/car/common/constant/DateType;
118: aastore
119: putstatic #1 // Field $VALUES:[Lcom/ins/car/common/constant/DateType;
122: return
}

上面都是些用于jvm执行的字节码指令,笔者是完全看不懂,但javap还可以看到反编译结果,输入命令javap DateType,可以看到下面这些输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
D:\jad>javap dateType
警告: 二进制文件dateType包含com.ins.car.common.constant.DateType
Compiled from "DateType.java"
public final class com.ins.car.common.constant.DateType extends java.lang.Enum<com.ins.car.common.constant.DateType> {
public static final com.ins.car.common.constant.DateType YEAR;
public static final com.ins.car.common.constant.DateType MONTH;
public static final com.ins.car.common.constant.DateType DAY;
public static final com.ins.car.common.constant.DateType HOUR;
public static final com.ins.car.common.constant.DateType MINUTE;
public static final com.ins.car.common.constant.DateType SECOND;
public static com.ins.car.common.constant.DateType[] values();
public static com.ins.car.common.constant.DateType valueOf(java.lang.String);
static {};
}

这看着就清爽多了,我们可以看到Enum类型其实本质还是class类型。它继承了java中Enum类型,并且枚举中的每一个实例都是以它本身类型的内部类实现的。但看到这里比较尴尬的是这些只有一些声明的信息,具体的实现我们看不到。但是最后笔者并没有找到javap还有其他的参数能查看到具体的实现信息,只能归咎于javap的功能有限。不过所幸的是还有网络,通过搜索笔者找到了一个第三方工具jad。它可以直接将一个class文件反编译成java文件,下面就是这个反编译成的java文件的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: DateType.java
public final class DateType extends Enum
{

public static DateType[] values()
{
return (DateType[])$VALUES.clone();
}

public static DateType valueOf(String name)
{
return (DateType)Enum.valueOf(com/ins/car/common/constant/DateType, name);
}

private DateType(String s, int i)
{
super(s, i);
}

public static final DateType YEAR;
public static final DateType MONTH;
public static final DateType DAY;
public static final DateType HOUR;
public static final DateType MINUTE;
public static final DateType SECOND;
private static final DateType $VALUES[];

static
{
YEAR = new DateType("YEAR", 0);
MONTH = new DateType("MONTH", 1);
DAY = new DateType("DAY", 2);
HOUR = new DateType("HOUR", 3);
MINUTE = new DateType("MINUTE", 4);
SECOND = new DateType("SECOND", 5);
$VALUES = (new DateType[] {
YEAR, MONTH, DAY, HOUR, MINUTE, SECOND
});
}
}

这次反编译的结果就具体多了,我们可以看到编译器自动给我们增加了valueOf和values两个函数,这两个是静态方法,可以直接通过类名调用。所有的内部枚举类型都被编译成了静态的内部类,并且在一个static块中初始化了它们。我们还看到他还有一个私有的构造函数,熟悉单例模式的同学都知道有一种单例模式就是通过枚举实现的,从上面的代码我们能够看出它完全符合单例模式的定义,并且是线程安全的。既然枚举都是继承于Enum的,我们就看一下Enum的代码,了解一下初始化时传入的两个参数是啥。

1
2
3
4
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}

由于代码篇幅较长,笔者这里只粘一下构造函数的代码简单了解一下。可以看出之前反编译的代码中的构造函数传入的参数分别是枚举类名,和它对应的序号,序号从0开始。便于通过values数组来访问它们。

EnumSet

EnumSet是Jdk1.5引入的,用来存在枚举类型的Set,EnumSet是个抽象类,具体实现在它的子类中,JDK中子类有两个,RegularEnumSet和JumboEnumSet。EnumSet,没有提供公有的额构造方法,要创建EnumSet需要调用它提供的一些静态方法。它有一个主要的静态方法,其余方法皆要调用此方法实现。下面看看这个方法:

1
2
3
4
5
6
7
8
9
10
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);
}

这个方法用于根据传入的枚举类型生成一个空的EnumSet。可见它首先调用了getUniverse方法,这个方法用来返回这个枚举类型中的实例数组。下面看看这个方法:

1
2
3
4
5
6
7
8
/**
* Returns all of the values comprising E.
* The result is uncloned, cached, and shared by all callers.
*/
private static <E extends Enum<E>> E[] getUniverse(Class<E> elementType) {
return SharedSecrets.getJavaLangAccess()
.getEnumConstantsShared(elementType);
}

这段代码点进去看的话,会发现有点复杂,这里我们就不继续深入了。我们看看这个注释,可以明白这个函数返回这个枚举类型中的实例数组(可能调用了枚举的values方法),并且这些实例采用了堆内缓存来提升性能。

继续看上面那段代码,发现它根据枚举实例的数目做了分叉,若在64之内,则生成RegularEnumSet,否则,生成JumboEnumSet。一般情况下应该第一个调用的比较多(数目大于64的枚举笔者还没遇到过)。值得一提的是在这些Enum集合中枚举实例其实都是存放在一个枚举数组中的(比如这里的universe和下面map的keyUniverse),而这个数组其实在EnumSet被初始化时就已经被创建,那么EnumSet的add操作到底做了些啥呢?我们来看看下面的代码:

1
2
3
4
5
6
7
public boolean add(E e) {
typeCheck(e);

long oldElements = elements;
elements |= (1L << ((Enum<?>)e).ordinal());
return elements != oldElements;
}

在RegularEnumSet中有一个变量elements,这个变量是一个long型的二进制数(JumboEnumSet是一个long型数组),即是一串64位的二进制数,说到这里有些同学可能已经猜到EnumSet是如何判断某个枚举是否存进set的了。起始这个elements全是0,当add进来一个枚举时,用这个枚举的序号对1进行左移运算,然后再与elements进行或运算。这个运算的结果就是,这个64位的二进制数分别对应着枚举的序号,如果相应枚举序号对应的位置变为1,则说明这个位置已经存在枚举值了。

EnumMap

EnumMap是一种以枚举为key的map,平常用的不多。但在某些特殊的场景下还是能发挥作用,当然可能我们也可以用数组来替代它,但终究不如枚举来的清晰明朗。下面我们通过源码来认识一下这个map。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* The <tt>Class</tt> object for the enum type of all the keys of this map.
*
* @serial
*/
private final Class<K> keyType;

/**
* All of the values comprising K. (Cached for performance.)
*/
private transient K[] keyUniverse;

/**
* Array representation of this map. The ith element is the value
* to which universe[i] is currently mapped, or null if it isn't
* mapped to anything, or NULL if it's mapped to null.
*/
private transient Object[] vals;

/**
* The number of mappings in this map.
*/
private transient int size = 0;

上面列出的是EnumMap的几个比较重要的属性。通过注释我们能明白,keyType表示key对应的枚举类型,keyUniverse表示这个枚举类型中所有的枚举值,vals是一个存储value的数组,它的长度和keyUniverse是一样的。因为key是无法重复的,它的值只能是这个枚举类型中的枚举值。size表示数组的长度,它不会大于keyUniverse的长度。

我们来看一下EnumMap的构造函数。EnumMap有三个公有的构造函数,我们只看其中一个。其实大同小异,主要也都是对上面四个属性的初始化。源码如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* Creates an empty enum map with the specified key type.
*
* @param keyType the class object of the key type for this enum map
* @throws NullPointerException if <tt>keyType</tt> is null
*/
public EnumMap(Class<K> keyType) {
this.keyType = keyType;
keyUniverse = getKeyUniverse(keyType);
vals = new Object[keyUniverse.length];
}

可见vals的长度和keyUniverse的长度是一致的。同时还有一点要提的是,在向EnumMap中放入为null的value值时,EnumMap会将它替换为NULL对象,这是EnumMap的一个内部类。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Distinguished non-null value for representing null values.
*/
private static final Object NULL = new Object() {
public int hashCode() {
return 0;
}

public String toString() {
return "java.util.EnumMap.NULL";
}
};

并且EnumMap定义了两个方法用于null与NULL对象之间的替换。源码如下:

1
2
3
4
5
6
7
8
private Object maskNull(Object value) {
return (value == null ? NULL : value);
}

@SuppressWarnings("unchecked")
private V unmaskNull(Object value) {
return (V)(value == NULL ? null : value);
}

既然是集合,下面我们看看它的增删查方法。

1
2
3
4
5
6
7
8
9
10
public V put(K key, V value) {
typeCheck(key);

int index = key.ordinal();
Object oldValue = vals[index];
vals[index] = maskNull(value);
if (oldValue == null)
size++;
return unmaskNull(oldValue);
}

可见value是存在val数组中的,并且数组的下标是枚举类型中key的序号。在存入value值时会掉用maskNull方法,将null转化为NULL对象,在返回时会将NULL对象转为null。因为null和Null的不同,所以检查oldValue是否为null,可以判断当前位置是否是第一次存入值,而不是新旧值的替换,以此来改变size的值。同时我们发现它还会对key进行校验,我们来看看typeCheck的代码:

1
2
3
4
5
6
7
8
/**
* Throws an exception if e is not of the correct type for this enum set.
*/
private void typeCheck(K key) {
Class<?> keyClass = key.getClass();
if (keyClass != keyType && keyClass.getSuperclass() != keyType)
throw new ClassCastException(keyClass + " != " + keyType);
}

可见这个方法是判断当前key的类型是否是在初始化时传入的枚举类型或者是它的子类。同时我们可以看到它直接调用了key的getclass方法,所以传入的也key不能为null。否者会抛出空指针异常。

下面看看get方法:

1
2
3
4
public V get(Object key) {
return (isValidKey(key) ?
unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
}

可见它会判断key是否合法,合法这返回value值,否则返回null。我们来看看isValidKey方法。

1
2
3
4
5
6
7
8
private boolean isValidKey(Object key) {
if (key == null)
return false;

// Cheaper than instanceof Enum followed by getDeclaringClass
Class<?> keyClass = key.getClass();
return keyClass == keyType || keyClass.getSuperclass() == keyType;
}

也是判空和枚举类型校验。下面看看remove方法:

1
2
3
4
5
6
7
8
9
10
public V remove(Object key) {
if (!isValidKey(key))
return null;
int index = ((Enum<?>)key).ordinal();
Object oldValue = vals[index];
vals[index] = null;
if (oldValue != null)
size--;
return unmaskNull(oldValue);
}

我们来分析一下这段代码,首先它会判断key的合法性,如何判断上面已经说了,这里不再赘述。然后找到key所在的vals下标,将它赋值为null。下面会进行一个判断,如果oldValue本来就为null,说明这个key所对应的vals位置本来就没有值(即还没有put进去过),此时相当于什么都没做,那么size值不变。否则改变size的值。

结束语

这篇博客对枚举和它对应的集合类进行了简单介绍。由于笔者也是边看边写,逻辑可能有些混乱;并且由于笔者水平有限,很多地方没有深耕,可能还有些错误疏漏。读者不可全信,当自己查证。若发现疏漏处,欢迎在评论处指出,不胜感激。