醉卧草庐听风雨

君子藏器于身,待时而动

0%

JAVA枚举类略解

最近突然有人问了个关于JAVA枚举类的问题,问题的大概意思是:通过IDE反编译查看的构造函数和通过编程方式反射得到的构造函数不一样。这个问题以前没见过,因此花费了些力气去学习了解了下,弄懂之后在此做个记录。

问题引出

一个简单的枚举类

1
2
3
public enum Question {
INSTANCE;
}

编译出来的字节码文件在IDE中的显示是这样的

1
2
3
4
5
6
public enum Question {
INSTANCE;

private Question() {
}
}

但通过编程反射后得到的结果是这样的private com.hst.Question(java.lang.String,int)

1
2
3
4
5
6
@Test
public void test() {
Constructor<?>[] declaredConstructors = Question.class.getDeclaredConstructors();
String string = declaredConstructors[0].toString();
System.out.println(string);
}

那么问题来了:实际使用的构造函数究竟是哪个,是IDE显示的无参构造还是反射得到的有参构造?

问题分析

在之前的博客中已经提到过,由于JVM运行的是字节码文件而非JAVA源码,所以这种问题需要从字节码入手,使用javap分析字节码得到如下内容

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
public final class com.hst.Question extends java.lang.Enum<com.hst.Question>
flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM
{
public static final com.hst.Question INSTANCE;
descriptor: Lcom/hst/Question;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=4, locals=0, args_size=0
0: new #4 // class com/hst/Question
3: dup
4: ldc #7 // String INSTANCE
6: iconst_0
7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #9 // Field INSTANCE:Lcom/hst/Question;
13: iconst_1
14: anewarray #4 // class com.hst/Question
17: dup
18: iconst_0
19: getstatic #9 // Field INSTANCE:Lcom/hst/Question;
22: aastore
23: putstatic #1 // Field $VALUES:[Lcom/hst/Question;
26: return
}

在第一行可以看到,编写的Question枚举类实际上是继承了java.lang.Enum类型,而定义的INSTANCE实际为Question中的一个静态类变量。接着可以看到枚举类还有一段static {}静态初始化块的指令,这个在之前JAVA字段初始化的文章中有提到过,静态初始化块代码是在类加载期执行,类加载之后进行实例化则不会再执行。这段静态初始化的指令整体上实际分为两个部分,这里详细介绍下这段指令

  • 0创建类Question的实例
  • 3复制实例引用至操作栈
  • 4加载常量池字符串INSTANCE至操作栈
  • 6加载int常量0至操作栈
  • 7调用构造方法(Ljava/lang/String;I)V
  • 10将实例设置为Question静态类变量INSTANCE
  • 13加载常量1
  • 14创建长度为1的Question数组实例
  • 17复制数组实例引用至操作栈
  • 18加载常量0至操作栈
  • 19获取静态类变量INSTANCE引用至操作栈
  • 22将Question数组0的位置存储静态类变量INSTANCE引用
  • 23将Question数组实例设置为Question的$VALUES静态类变量
  • 26返回

如果要用实际代码来演示的话,真实的枚举类源码应该是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class Question extends Enum<Question> {
public static final Question INSTANCE;
private static final Question[] $VALUES;

static {
INSTANCE = new Question("INSTANCE", 0);
$VALUES = new Question[1];
$VALUES[0] = INSTANCE;
}

private Question(String name, int ordinal) {
super(name, ordinal);
}
}

所以综上所述,IDE实际是优化了显示,而实际构造函数则是来源于java.lang.Enum中的protected Enum(String name, int ordinal),即使枚举类有自己的字段和构造函数,但实际的字节码中依然会加上默认的参数(String name, int ordinal),另外关于枚举类的$VALUES字段有兴趣的同学可以自己通过反射获取验证下。

写在最后

果然问题还是学习的第一驱动力,枚举类之前用的倒是挺多的,没想到里面还有这么多名堂,嗯,感谢提出问题的大哥。希望这篇文章的内容可以帮助大家。