Enum类源码剖析

Enum类源码剖析

一、Enum类的概述

Java中的枚举类型默认都继承自Enum抽象类,因此了解Enum类的原理对于枚举类型的学习大有帮助。Enum的全写是Enumeration,这个词的翻译是列举、逐条陈述、细目。枚举类型是JDK 5之后引进的一种非常重要的引用类型,可以用来定义一系列枚举常量。在没有引入enum关键字之前,要表示可枚举的变量,只能使用以下的方式。

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
public class SeasonDef {

public static final int SPRING = 1;
public static final int SUMMER = 2;
public static final int AUTUMN = 3;
public static final int WINTER = 4;

public static String getSeason(int type) {
String season = "";
switch (type) {
case SPRING:
season = "春天";
break;
case SUMMER:
season = "夏天";
break;
case AUTUMN:
season = "秋天";
break;
case WINTER:
season = "冬天";
break;
default:
throw new RuntimeException("type out of range!");
}
return season;
}

public static void main(String[] args) {
System.out.println(SeasonDef.getSeason(SeasonDef.SPRING));
System.out.println(SeasonDef.getSeason(1));
System.out.println(SeasonDef.getSeason(5));
}
}

image-20230917164601976

这种实现方式有几个弊端:

  1. 类型不安全。试想一下,有一个方法期待接受一个季节作为参数,那么只能将参数类型声明为int,但是传入的值可能是5。显然只能在运行时进行参数合理性的判断,无法在编译期间完成检查。
  2. 指意性不强,含义不明确。我们使用枚举,很多场合会用到该枚举的字串符表达,而上述的实现中只能得到一个数字,不能直观地表达该枚举常量的含义。当然也可用String常量,但是又会带来性能问题,因为比较要依赖字符串的比较操作

使用enum来表示枚举可以更好地保证程序的类型安全和可读性。

  1. enum是类型安全的。除了预先定义的枚举常量,不能将其它的值赋给枚举变量。这和用intString实现的枚举很不一样。
  2. enum有自己的名称空间,且可读性强。在创建enum时,编译器会自动添加一些有用的特性。每个enum实例都有一个名字(name)和一个序号(ordinal),可以通过toString()方法获取enum实例的字符串表示。还以通过values()方法获得一个由enum常量按顺序构成的数组。

enum还有一个特别实用的特性,可以在switch语句中使用,这也是enum最常用的使用方式了。

二、Enum类的继承关系

Enum抽象类实现了ComparableSerializable接口,其中覆写的compareTo方法比较的是两个枚举类的序号ordinal(后面会提到)。这里有意思的是泛型参数E extends Enum<E>,表明Enum类存放的是Enum<E>自身及其子类,例如声明一个枚举类型enum Color,编译器展开其实就是class Color extends Enum<Color>,这里的泛型参数起到了规定诸如compareTo方法中参数类型的作用,使其只能为Enum<E>自身及其子类

1
2
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable

image-20230917165906678

三、Enum类的字段及构造方法

Enum类的字段很简明,就是枚举常量名称name和枚举常量序号oridinal,后面分析枚举类型的反编译会看到具体细节。

1
2
3
4
5
6
7
8
9
10
11
// 表示枚举常量的名称,在枚举类型声明中定义,开发者往往使用toString方法而不是直接访问该字段。
private final String name;
// 表示枚举常量的序号(它在枚举声明中的位置,其中初始枚举常量序号为0)。大多数开发者几乎不会使用到这个字段。它被设计用于复杂的基于枚举的数据结构,如java.util.EnumSet和java.util.EnumMap。
private final int ordinal;
// 相应的get方法
public final String name() {
return name;
}
public final int ordinal() {
return ordinal;
}

注意到Enum类的构造方法是protected权限,因此子类可以直接使用(后面分析枚举类型的反编译会看到具体细节)。

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

四、Enum类的重要方法

compareTo方法将此枚举常量与指定的枚举常量比较序号。返回负整数、零或正整数,表明此对象小于、等于或大于指定对象。枚举常量仅与相同枚举类型的其他枚举常量相比较(这个正是泛型参数规定参数类型的具体表现,使得Java API更加健壮)。此方法比较的序号是声明常量的顺序

1
2
3
4
5
6
7
8
public final int compareTo(E o) {
Enum<?> other = (Enum<?>)o;
Enum<E> self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}

注意一下:compareTo方法里面的条件判断很有讲究,因为枚举类可以引入抽象方法并声明不同的枚举常量实例实现该方法,因此传入的参数可能是该枚举类的其中一个枚举常量(本质是枚举类的子类),此时调用getClass比较一定是不相等的,只能进一步调用getDeclaringClass解决这种情况。

getDeclaringClass返回与此枚举常量的枚举类型对应的Class对象。两个枚举常量e1和e2属于相同的枚举类型,当且仅当e1.getDeclaringClass()==e2.getDeclaringClass()

1
2
3
4
5
public final Class<E> getDeclaringClass() {
Class<?> clazz = getClass();
Class<?> zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
}

下面我们举一个例子论证我们的观点:

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
package test.lang;

public enum Operation {
PLUS {
public double eval(double a, double b){
return a + b;
}
},
MINUS {
public double eval(double a, double b){
return a - b;
}
},
MULTI {
public double eval(double a, double b){
return a * b;
}
},
DIVIDE {
public double eval(double a, double b){
return a / b;
}
};

//这个抽象方法由不一样的枚举常量提供实现
public abstract double eval(double a, double b);
}

image-20230917173803325

valueOf是Enum类的静态方法,与具体的枚举实例无关,如果眼尖的话可以看到该方法的泛型参数变为T extends Enum<T>而不是之前的实例方法的E,因此enumType参数接受任何枚举类型的Class对象,valueOf方法的作用也就是返回指定枚举类的某个枚举常量(名称为参数name)。

image-20230917174555134

1
2
3
4
5
6
7
8
9
10
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}

我们再用两行代码验证valueOf方法的使用:

1
2
Operation plus = Operation.valueOf("PLUS");
System.out.println(plus == Operation.PLUS); // true

五、Enum类的其他方法

image-20230917175059670

1
2
3
public String toString() {
return name;
}

以下是Enum类中一些不支持的方法:

1
2
3
4
5
6
7
8
9
10
11
// 这保证了枚举永远不会被克隆,这对于保持其“单例”状态是必要的。
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
// 枚举类不能有finalize方法。
protected final void finalize() { }
// 阻止默认的反序列化方法
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}

六、Enum类的反编译1

首先,我们声明一个Season枚举类代表一年四季:

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
package test.lang;

public enum Season {
SPRING(1, "春天"),
SUMMER(2, "夏天"),
AUTUMN(3, "秋天"),
WINTER(4, "冬天");

// 额外定义的字段
public int code;
public String name;

// 枚举类的构造方法默认是private权限
Season(int code, String name) {
this.code = code;
this.name = name;
}

public static Season getSeasonByCode(int code) {
for (Season season : Season.values()) {
if (season.code == code) {
return season;
}
}
return null;
}

}

然后,我们使用javac命令编译Season.javaSeason.class字节码文件,接着使用jad工具反编译字节码文件得到Season.jad

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
// 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: Season.java

package test.decompile;


public final class Season extends Enum
{

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

public static Season valueOf(String name)
{
// 调用父类Enum的valueOf方法并自动填充Class对象为test.lang.Season
return (Season)Enum.valueOf(test/lang/Season, name);
}

private Season(String s, int i, int code, String name)
{
// 调用父类Enum的protected构造方法
super(s, i);
this.code = code;
this.name = name;
}

public static Season getSeasonByCode(int code)
{
Season aseason[] = values();
int i = aseason.length;
for(int j = 0; j < i; j++)
{
Season season = aseason[j];
if(season.code == code)
return season;
}

return null;
}

public static final Season SPRING;
public static final Season SUMMER;
public static final Season AUTUMN;
public static final Season WINTER;
public int code;
public String name;
// 编译器添加的枚举常量的数组
private static final Season $VALUES[];

static
{
SPRING = new Season("SPRING", 0, 1, "\u6625\u5929");
SUMMER = new Season("SUMMER", 1, 2, "\u590F\u5929");
AUTUMN = new Season("AUTUMN", 2, 3, "\u79CB\u5929");
WINTER = new Season("WINTER", 3, 4, "\u51AC\u5929");
$VALUES = (new Season[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}
  1. Season在底层其实还是用class修饰的,说明枚举类本质上还是一个Class,而且Season被JVM隐式的用final关键字修饰起来,那么这个类不能被任何类继承,这也在一定程度上保证了类的安全。
  2. values方法:默认生成了一个values方法,它返回了当前类型枚举的数组,这也解释了ordinal的来源。
  3. valueOf方法:在具体的每个自定义枚举类里面生成了一个根据枚举名返回枚举实例的方法,这其实是对java.lang.EnumvalueOf方法的具体重写,它只限于当前枚举类
  4. 静态代码块:Season中所有的enum实例在这里初始化完成。

注意:当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以创建一个enum类型是线程安全的。

七、Enum类的反编译2

我们再看一个枚举类型反编译的情况,第一步还是创建Operation枚举类(我们之前用过):

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
package test.lang;

public enum Operation {
PLUS {
public double eval(double a, double b){
return a + b;
}
},
MINUS {
public double eval(double a, double b){
return a - b;
}
},
MULTI {
public double eval(double a, double b){
return a * b;
}
},
DIVIDE {
public double eval(double a, double b){
return a / b;
}
};

//这个抽象方法由不一样的枚举常量提供实现
public abstract double eval(double a, double b);
}

然后,我们使用javac命令编译Operation.javaOperation.class字节码文件,接着使用jad工具反编译字节码文件得到Opeartion.jad

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
// 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: Operation.java

package test.lang;


public abstract class Operation extends Enum
{

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

public static Operation valueOf(String name)
{
return (Operation)Enum.valueOf(test/lang/Operation, name);
}

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

// 抽象方法,每个具体的枚举常量都会实现
public abstract double eval(double d, double d1);

public static final Operation PLUS;
public static final Operation MINUS;
public static final Operation MULTI;
public static final Operation DIVIDE;
private static final Operation $VALUES[];

static
{
PLUS = new Operation("PLUS", 0) {

public double eval(double a, double b)
{
return a + b;
}

};
MINUS = new Operation("MINUS", 1) {

public double eval(double a, double b)
{
return a - b;
}

};
MULTI = new Operation("MULTI", 2) {

public double eval(double a, double b)
{
return a * b;
}

};
DIVIDE = new Operation("DIVIDE", 3) {

public double eval(double a, double b)
{
return a / b;
}

};
$VALUES = (new Operation[] {
PLUS, MINUS, MULTI, DIVIDE
});
}
}

可以看出,枚举类型可以包含抽象方法,声明enum时不需要加abstract关键字,Java编译器默认会帮我们完成所有累活,这个例子告诉了我们枚举常量可能是所属枚举类的实现子类,因此进一步论证了我们之前谈到的Enum类的getDeclaringClass方法。