枚举类的使用

定义一个简单的枚举类,其中包含若干枚举常量,示例如下:

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;
}
作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15851123.html

版权:本文版权归作者和博客园共有

转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

枚举类底层原理探究

研究一个问题,最好是从现象出发去看本质,先知道有哪些现象,再看看它们的本质原因是什么。对于枚举类来说,它和普通类的不同之处就是现象:

  • 枚举类不能由外界创建对象
  • 枚举类默认继承了 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$1Color$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 的构造器额外添加两个方法参数:Stringint 型呢?

来看看 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;
}

如果读者想进一步刨根问底,可以研究一下传递给 nameordinal 的值是什么。这里先给出答案:因为调用构造函数创建的枚举常量,是由 static final 修饰的,所以调用的时机发生在类加载的初始化阶段,这时编译器会按顺序收集所有 static 赋值语句和 static 块,生成一个 <clinit> 方法,然后去执行这个方法。所以,nameordinal 参数可以在该方法中分析字节码找到。name 实际上就是枚举常量名,ordinal 就是枚举常量在枚举类中声明的位置,从0开始计数。记录这两个参数是为了方便 EnumtoStringordinalcompareTo 等方法的调用

能够看到这里,想必也就明白了枚举类这个 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):根据枚举常量名,返回该枚举类中特定的枚举常量,内部调用的就是 EnumvalueOf 方法

字节码层面深入分析Java枚举类的更多相关文章

  1. 透过字节码生成审视Java动态代理运作机制

    对于动态代理我想应该大家都不陌生,就是可以动态去代理实现某个接口的类来干一些我们自己想要的功能,但是在字节码层面它的表现是如何的呢?既然目前刚好在研究字节码相关的东东,有必要对其从字节码角度来审视一下 ...

  2. i++ 与 ++i 的从字节码层面看二者的区别

    /** * javap命令可以对class反汇编得到其字节码文件(此命令并不是jdk8开始的,只不过jdk8中对工具进行加强,增加了一些参数,可通过 javap -help了解) * * 注意: * ...

  3. Java枚举类在生产环境中的使用方式

    前言   Java枚举在项目中使用非常普遍,许多人在做项目时,一定会遇到要维护某些业务场景状态的时候,往往会定义一个常量类,然后添加业务场景相关的状态常量.但实际上,生产环境的项目中业务状态的定义大部 ...

  4. i = i++ 在java字节码层面的分析

    有这么一段代码: package zl.test; public class PcodeTest { /** * @param args */ public static void main(Stri ...

  5. 从字节码层面,解析 Java 布尔型的实现原理

    最近在系统回顾学习 Java 虚拟机方面的知识,其中想到一个很有意思的问题:布尔型在虚拟机中到底是什么类型? 要想解答这个问题,我们看 JDK 的源码是无法解决源码的,我们必须深入到 class 文件 ...

  6. 《深入理解Java虚拟机》-----第8章 虚拟机字节码执行引擎——Java高级开发必须懂的

    概述 执行引擎是Java虚拟机最核心的组成部分之一.“虚拟机”是一个相对于“物理机”的概念 ,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的,而 ...

  7. 从字节码层面看“HelloWorld” (转)

    一.HelloWorld 字节码生成 众所周知,Java 程序是在 JVM 上运行的,不过 JVM 运行的其实不是 Java 语言本身,而是 Java 程序编译成的字节码文件.可能一开始 JVM 是为 ...

  8. Java基础15:深入剖析Java枚举类

    更多内容请关注微信公众号[Java技术江湖] 这是一位阿里 Java 工程师的技术小站,作者黄小斜,专注 Java 相关技术:SSM.SpringBoot.MySQL.分布式.中间件.集群.Linux ...

  9. 从字节码层面看“HelloWorld”

    一.HelloWorld 字节码生成 众所周知,Java 程序是在 JVM 上运行的,不过 JVM 运行的其实不是 Java 语言本身,而是 Java 程序编译成的字节码文件.可能一开始 JVM 是为 ...

随机推荐

  1. 【LeetCode】323. Number of Connected Components in an Undirected Graph 解题报告 (C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 并查集 日期 题目地址:https://leetcod ...

  2. 【LeetCode】295. Find Median from Data Stream 解题报告(C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 大根堆+小根堆 日期 题目地址:https://le ...

  3. 【LeetCode】456. 132 Pattern 解题报告(Python)

    [LeetCode]456. 132 Pattern 解题报告(Python) 标签(空格分隔): LeetCode 作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fu ...

  4. codeforces(Intel Code Challenge Elimination Round (Div.1 + Div.2, combined) )(C,D)

    C. Destroying Array time limit per test 1 second memory limit per test 256 megabytes input standard ...

  5. APP打开(四)—Deeplink推广,打开率很低怎么排查

    在开始正文之前,先解释一下,这里提到的打开率指的是点击了Deeplink之后正常打开了APP和点击量的比值. 开始正文: 但凡做TOC业务的多多少少都会用到Deeplink,这是一个重要的运营手段.但 ...

  6. toString()、String.valueOf、(String)强转

    1.基本类型 (1)基本类型没有toString()方法 (2)推荐使用String.valueOf(); (3)无法强转 =========补========= (String)是标准的类型转换,将 ...

  7. 「算法笔记」旋转 Treap

    一.引入 随机数据中,BST 一次操作的期望复杂度为 \(\mathcal{O}(\log n)\). 然而,BST 很容易退化,例如在 BST 中一次插入一个有序序列,将会得到一条链,平均每次操作的 ...

  8. Proximal Algorithms 6 Evaluating Proximal Operators

    目录 一般方法 二次函数 平滑函数 标量函数 一般的标量函数 多边形 对偶 仿射集合 半平面 Box Simplex Cones 二阶锥 半正定锥 指数锥 Pointwise maximum and ...

  9. Redisson分布式锁学习总结:可重入锁 RedissonLock#lock 获取锁源码分析

    原文:Redisson分布式锁学习总结:可重入锁 RedissonLock#lock 获取锁源码分析 一.RedissonLock#lock 源码分析 1.根据锁key计算出 slot,一个slot对 ...

  10. Go项目开源规范

    我们为什么一定要知道开源规范呢? 一是,开源项目在代码质量.代码规范.文档等方面,要比非开源项目要求更高,在项目开发中按照开源项目的要求来规范自己的项目,可以更好地驱动项目质量的提高: 二是,一些大公 ...