字节码层面深入分析Java枚举类
枚举类的使用
定义一个简单的枚举类,其中包含若干枚举常量,示例如下:
public enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY,THURSDAY, FRIDAY, SATURDAY
}
Java 的 switch
语句参数支持使用枚举类
// day是Day类型变量
switch (day) {
case MONDAY:
System.out.println("要开组会了好难受");
break;
case THURSDAY:
System.out.println("肯德基疯狂星期四");
break;
case SATURDAY: case SUNDAY:
System.out.println("不浪对不起周末");
break;
default:
System.out.println("摸摸鱼");
break;
}
其实这个用 static final
也可以,但是用枚举类的好处在于:
- 用
static final
的话,传入的变量就需要进行参数检查,而枚举类不用,因为肯定在枚举的范围中,或为null
static final
不支持属性扩展,每个变量名对应一个值,而每一个枚举值可以拥有自己的多个属性(字段)
枚举类可以添加属性及相应的构造器,以及方法,不过枚举类中的构造器默认也必须是 private
的。示例如下:
public enum Day {
MONDAY(1, "星期一"),
TUESDAY(2, "星期二"),
// ......
SUNDAY(7, "星期日");
private int index;
private String name;
Day(int index, String name) {
this.index = index;
this.name = name;
}
// 针对index、name的getter方法
// ......
}
一般来说,不会为枚举类添加 setter 方法,因为这样会枚举一般只用来做常量,setter 会破坏它的常量特性
枚举类中的每个枚举常量都可以实现枚举主类定义的(abstract)方法,也可以拥有各自的内部方法,如下:
public enum Day {
MONDAY(1, "星期一"){
@Override
public Day getNext() {
return TUESDAY;
}
},
TUESDAY(2, "星期二"){
@Override
public Day getNext() {
return WEDNESDAY;
}
},
// ......
SUNDAY(7, "星期日"){
@Override
public Day getNext() {
return MONDAY;
}
};
private int index;
private String name;
Day(int index, String name) {
this.index = index;
this.name = name;
}
// 在主类中定义抽象方法
public abstract Day getNext();
// 针对index、name的getter方法
// ......
}
虽然也可以在每个枚举常量中自定义任何方法,但是如果没有在主类中声明,就不能访问到,这个暂且按下不表,原因在后面会解释
所有枚举类都默认继承了 Enum
类,可以直接使用 Enum
提供的实用方法。由于 Java 只支持单继承,所以枚举类不能再继承别的父类,只能实现接口。一些使用的 Enum
类的方法,都贴在了文末_
枚举类的枚举常量全局唯一,不存在并发安全性问题,且不会被反射、序列化方式恶意创建新的枚举常量对象,很适合用来实现单例模式。这里可以参加博主的另一篇文章:单例模式的各种实现方式(Java)
最后再补充一点,博主发现某书和很多博客都说:在比较两个枚举类型的值时 , 永远不需要调用 equals
方法, 而直接使用 ==
就可以了。但是我看了下 Enum
类中给到的 equals
源码(贴在下面),实际上用的也是 ==
,我自己手动测试了也没问题。但不知道为什么,大家的博客上都这么写的,难道真就是人云亦云吗-_-||
public final boolean equals(Object other) {
return this==other;
}
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
枚举类底层原理探究
研究一个问题,最好是从现象出发去看本质,先知道有哪些现象,再看看它们的本质原因是什么。对于枚举类来说,它和普通类的不同之处就是现象:
- 枚举类不能由外界创建对象
- 枚举类默认继承了
Enum
类,不可以再继承其他类,且枚举类不可以被继承 - 虽然
Enum
类中并没有values()
和valueOf(String)
方法,但是枚举类也可以调用 - 枚举主类可以定义
abstract
方法,每个枚举常量都可以分别对其提供实现,不过它们也可以自定义其他方法。但只有在枚举主类定义过的方法,才能通过枚举常量调用到,否则不能 - 定义在每个枚举主类中的字段,在各个枚举常量中却是不同的
- 枚举类的所有枚举常量都是一开始就创建好的,全局唯一、不可变且线程安全
先编写一段普通的枚举类作为示例,代码如下:
public enum Color {
red("红", 0) {
@Override
public void print() {
System.out.println(getName() + ":" + getIndex());
}
},
green("绿", 1) {
@Override
public void print() {
System.out.println(getName() + " " + getIndex());
}
};
private String name;
private int index;
Color() {
}
Color(String name, int index) {
this.name = name;
this.index = index;
}
// 枚举主类中定义的抽象方法
public abstract void print();
// name、index的getterr方法
}
先对 Color.class 进行反编译(javap 不加参数 -v
):
Compiled from "Color.java"
public abstract class com.duyuzhou.enumTest.Color extends java.lang.Enum<com.duyuzhou.enumTest.Color> {
public static final com.duyuzhou.enumTest.Color red;
public static final com.duyuzhou.enumTest.Color green;
public static com.duyuzhou.enumTest.Color[] values();
public static com.duyuzhou.enumTest.Color valueOf(java.lang.String);
public java.lang.String getName();
public int getIndex();
public abstract void print();
public static void main(java.lang.String[]);
com.duyuzhou.enumTest.Color(java.lang.String, int, java.lang.String, int, com.duyuzhou.enumTest.Color$1);
static {};
}
仔细分析,就可以得出以下结论:
- 枚举类继承了
Enum
类,以及Enum
类的各种方法。编译器为枚举类添加了final
关键字,使得枚举类不能被其他类继承 - 枚举类被编译器额外添加了两个
static
方法:values()
和valueOf(String)
- 枚举类会被编译成
abstract
类,因此枚举类可以定义抽象方法 - 枚举类的构造器都被改为了
private
,因此外界不可以使用枚举类创建对象 - 所有枚举常量都会被编译成枚举主类的
static final
属性,那么在类加载的初始化阶段就会将所有枚举常量创建好,而且只会创建一次。final
也能保证枚举常量一旦被创建好后,对于所有线程都是可见的,不会存在线程安全问题
如果反编译加上 -v
参数,可以看到 Color
有两个静态内部类,分别是 Color$1
和 Color$2
,对其中一个进行反编译(不加 -v
):
Compiled from "Color.java"
final class com.duyuzhou.enumTest.Color$1 extends com.duyuzhou.enumTest.Color {
com.duyuzhou.enumTest.Color$1(java.lang.String, int, java.lang.String, int);
public void print();
}
其实每个静态内部类都对应了一个枚举常量,这些静态内部类都继承了枚举主类,所以枚举常量中可以实现主类中定义的 abstract 方法。而且,在枚举主类中,每个枚举常量都变成了一个枚举主类类型的字段,因此外界不可能调用一个枚举常量中私自定义但枚举主类中没定义的方法
此外,由于每个枚举常量都是不同枚举子类的一个对象,所以它们各自继承了父类定义的字段,且观察枚举常量的反编译结果会发现,编译器为每个枚举子类都添加了一个构造函数,所以枚举主类中定义的字段是在各个枚举常量中分开赋值的
至此,上面的所有“现象”,都通过反编译查看字节码的方式得到了解答,本质上是编译器帮我们做好了幕后工作,所以才有了这些代码中看不到却实际存在的规则。不过,还遗留了一个小问题——在 Color.class
中,为什么编译器会为 Color
的构造器额外添加两个方法参数:String
和 int
型呢?
来看看 Color
的构造器 com.duyuzhou.enumTest.Color(java.lang.String, int, java.lang.String, int, com.duyuzhou.enumTest.Color$1)
反编译出的字节码:
0 aload_0
1 aload_1
2 iload_2
3 aload_3
4 invokespecial #2 <enumtest/Color.<init> : (Ljava/lang/String;ILjava/lang/String;)V>
7 return
这里会调用 Color.<init>
方法,该方法的字节码需要借助 Jclasslib 工具查看,如下:
0 aload_0
1 aload_1
2 iload_2
3 invokespecial #9 <java/lang/Enum.<init> : (Ljava/lang/String;I)V>
6 return
看到这里应该就能懂了,这里实际上会将额外添加的两个方法参数传递给父类 Enum
的构造器,那么看一下 Enum
中接收一个 String
和一个 int
型的构造器是怎样的:
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
如果读者想进一步刨根问底,可以研究一下传递给 name
和 ordinal
的值是什么。这里先给出答案:因为调用构造函数创建的枚举常量,是由 static final
修饰的,所以调用的时机发生在类加载的初始化阶段,这时编译器会按顺序收集所有 static
赋值语句和 static
块,生成一个 <clinit>
方法,然后去执行这个方法。所以,name
和 ordinal
参数可以在该方法中分析字节码找到。name
实际上就是枚举常量名,ordinal
就是枚举常量在枚举类中声明的位置,从0开始计数。记录这两个参数是为了方便 Enum
中 toString
、ordinal
、compareTo
等方法的调用
能够看到这里,想必也就明白了枚举类这个 JDK5 才加入的新特性,就是一颗“语法糖”罢了。为了保持向后兼容性,Java 编译器做了很多幕后工作。根据这样的思路,我们也可以探究一下其他 Java 语法糖是如何实现的,比如 forEach
方法、自动装箱/拆箱、泛型为什么会擦除类型等
最后总结一下比较实用的 Enum
类提供的方法,和编译器为每个枚举类自动添加的两个方法(values()
和 valueOf(String)
)
Enum
类提供的方法:
String name()
:等同于toString()
int ordinal()
:返回枚举常量在枚举类中声明的位置,从0开始计数String toString()
:返回枚举常量名int compareTo(E other)
:比较两个枚举常量声明的位置谁更靠前,其实靠的就是比较ordinal
的大小static Enum valueOf(Class clz, String name)
:根据枚举类和枚举常量名,返回特定的枚举常量
编译器为每个枚举类自动添加的方法:
Enum[] values()
:返回枚举常量数组,实际上编译器在<clinit>
中创建各个枚举常量时,也会创建一个字段$VALUES
,其中就保存了这个数组Enum valueOf(String name)
:根据枚举常量名,返回该枚举类中特定的枚举常量,内部调用的就是Enum
的valueOf
方法
字节码层面深入分析Java枚举类的更多相关文章
- 透过字节码生成审视Java动态代理运作机制
对于动态代理我想应该大家都不陌生,就是可以动态去代理实现某个接口的类来干一些我们自己想要的功能,但是在字节码层面它的表现是如何的呢?既然目前刚好在研究字节码相关的东东,有必要对其从字节码角度来审视一下 ...
- i++ 与 ++i 的从字节码层面看二者的区别
/** * javap命令可以对class反汇编得到其字节码文件(此命令并不是jdk8开始的,只不过jdk8中对工具进行加强,增加了一些参数,可通过 javap -help了解) * * 注意: * ...
- Java枚举类在生产环境中的使用方式
前言 Java枚举在项目中使用非常普遍,许多人在做项目时,一定会遇到要维护某些业务场景状态的时候,往往会定义一个常量类,然后添加业务场景相关的状态常量.但实际上,生产环境的项目中业务状态的定义大部 ...
- i = i++ 在java字节码层面的分析
有这么一段代码: package zl.test; public class PcodeTest { /** * @param args */ public static void main(Stri ...
- 从字节码层面,解析 Java 布尔型的实现原理
最近在系统回顾学习 Java 虚拟机方面的知识,其中想到一个很有意思的问题:布尔型在虚拟机中到底是什么类型? 要想解答这个问题,我们看 JDK 的源码是无法解决源码的,我们必须深入到 class 文件 ...
- 《深入理解Java虚拟机》-----第8章 虚拟机字节码执行引擎——Java高级开发必须懂的
概述 执行引擎是Java虚拟机最核心的组成部分之一.“虚拟机”是一个相对于“物理机”的概念 ,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的,而 ...
- 从字节码层面看“HelloWorld” (转)
一.HelloWorld 字节码生成 众所周知,Java 程序是在 JVM 上运行的,不过 JVM 运行的其实不是 Java 语言本身,而是 Java 程序编译成的字节码文件.可能一开始 JVM 是为 ...
- Java基础15:深入剖析Java枚举类
更多内容请关注微信公众号[Java技术江湖] 这是一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM.SpringBoot.MySQL.分布式.中间件.集群.Linux ...
- 从字节码层面看“HelloWorld”
一.HelloWorld 字节码生成 众所周知,Java 程序是在 JVM 上运行的,不过 JVM 运行的其实不是 Java 语言本身,而是 Java 程序编译成的字节码文件.可能一开始 JVM 是为 ...
随机推荐
- 【LeetCode】219. Contains Duplicate II 解题报告(Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 使用set 使用字典 日期 题目地址:https:/ ...
- 【剑指Offer】链表中环的入口结点 解题报告(Python)
[剑指Offer]链表中环的入口结点 解题报告(Python) 标签(空格分隔): 剑指Offer 题目地址:https://www.nowcoder.com/ta/coding-interviews ...
- codeforces B. Island Puzzle
B. Island Puzzle time limit per test 2 seconds memory limit per test 256 megabytes input standard in ...
- CRB and His Birthday(hdu 5410)
CRB and His Birthday Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Oth ...
- NetCore实现全局模型绑定异常信息统一处理
本文主要讲解NetCore如何使用中间件捕获模型绑定的异常信息 场景 在.NET Core 中请求中,如果参数的类型错误,我们在控制器的定义的方法是不会执行的,当我们需要捕获模型绑定的异常信息时,可以 ...
- 第四十三个知识点:为AES描述一些基础的(可能无效)的对抗侧信道攻击的防御
第四十三个知识点:为AES描述一些基础的(可能无效)的对抗侧信道攻击的防御 原文地址:http://bristolcrypto.blogspot.com/2015/07/52-things-numbe ...
- Adversarial Self-Supervised Contrastive Learning
目录 概 主要内容 Linear Part 代码 Kim M., Tack J. & Hwang S. Adversarial Self-Supervised Contrastive Lear ...
- [AllError错误填坑大全]Jsoncpp logicError
在将Json::Value添加元素子项的时候,不要与(key,Value)的形式共有.容易形成逻辑错误. 举例如下: Json::Value output; Json::Value people; n ...
- 基于Spring MVC + Spring + MyBatis的【学生信息管理系统】
资源下载:https://download.csdn.net/download/weixin_44893902/45603211 练习点设计: 模糊查询.删除.新增 一.语言和环境 实现语言:JAVA ...
- SpringMVC+Spring+Mybatis实现登录注册Demo
使用环境:MyEclipse/Eclipse + Tomcat + MySql. 使用技术:SpringMVC + Spring + Mybatis. 实现效果 登录页面: 密码错误提示 登录成功后 ...