平时在工作的开发中, 发现同事用枚举用的特别溜, 你说自己知道吗? 感觉枚举自己是会用的, 但是自己觉得就是不够深刻, 应该是内心没有真正的掌握掉这个东西, 所以总是无意识的逃避使用这个, 或者说有时候真的想不起来用这个东西.
1. 枚举的优势
那接下来不得不来叨叨枚举的优势了, 它到底有什么好的, 让那么多人用这个呢?
增强代码可读性(装逼神器)
不得不说, 如果你用了枚举, 立马感觉你跟一般的程序员不一样了, 这个绝对是你装逼和吹牛逼的一个神器吧(嘿嘿)
去除equals两者判断 由于常量值地址唯一,使用枚举可以直接通过“==”进行两个值之间的对比,性能会有所提高。
编译优势(与常量类相比)
常量类编译时,常量被直接编译进二进制代码中,常量值在升级中变化后,需要重新编译引用常量的类,因为二进制代码中存放的是旧值。枚举类编译时,没有把常量值编译到代码中,即使常量值发生改变,也不会影响引用常量的类。
修改优势(与常量类相比)
枚举类编译后默认final class,不允许继承可防止被子类修改。常量类可被继承修改、增加字段等,易导致父类不兼容。
枚举型可直接与数据库交互。
Switch语句优势
使用int、String类型switch时,当出现参数不确定的情况,偶尔会出现越界的现象,这样我们就需要做容错操作(if条件筛选等),使用枚举,编译期间限定类型,不允许发生越界。
2. 常用场景
抛开业务谈设计, 一切都是扯淡, 所以你必须知道枚举的应用场景, 最重要的是你应该知道什么时候用它, 否则, 就算你知道怎么用, 但是不知道该用它了, 它还是摆设.
2.1 如果超过两个以上的常量的定义, 可以考虑使用枚举
一般我们定义常量都是需要public static final.... , 现在你要是会使用枚举, 直接换一个高逼格的枚举来搞定吧, 而且枚举也提供了比常量更多的方法.
public enum Color {
RED, GREEN, BLANK, YELLOW
}
2.2 在switch语句中, 可以考虑使用枚举
enum Signal {
GREEN, YELLOW, RED
}
public class TrafficLight {
Signal color = Signal.RED;
public void change() {
switch (color) {
case RED:
color = Signal.GREEN;
break;
case YELLOW:
color = Signal.RED;
break;
case GREEN:
color = Signal.YELLOW;
break;
}
}
}
3. 枚举在工作中的一个应用
我们先来看这段代码吧:
我们主要是想表达一个红路灯的展示, 所以代码如下:
package com.abc.test;
import lombok.Data;
/**
* @author lingxiangxinag
*/
@Data
public class AppStatusDTO {
/** app的ID */
private String appInstanceId;
/** app的状态 */
private Status status;
/** 状态的内容 */
private String content;
/** 时间戳 */
private Integer timestamp;
// red, yellow, green, unknown代表的就是枚举出来的不同实例,传入的参数, 后面的构造函数中Status(int i)中进行的定义
public enum Status {
/** 不健康 */
red(2),
/** 良好 */
yellow(1),
/** 健康 */
green(0),
/** 未知, 最后一个一定是";", 其他的都是",", ";"代表枚举结束 */
unknown(-1);
public final int num;
private Status(int i) {
this.num = i;
}
// 自定义方法, 根据num获取枚举
public static Status valueOf(int num){
switch (num){
case 0 : return AppStatusDTO.Status.green;
case 1: return AppStatusDTO.Status.yellow;
case 2: return AppStatusDTO.Status.red;
default:return AppStatusDTO.Status.unknown;
}
}
}
}
其实我们可以这样理解, 我们把2代表是红色, 1代表是黄色, 0代表的绿色, -1代表是-1;
其实, 我个人理解, 如果枚举是有参数的, 一个或者以上, 都应给应该他内部的构造方法, 下面列举两个参数的枚举
public enum Color {
RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
// 成员变量
private String name;
private int index;
// 构造方法
private Color(String name, int index) {
this.name = name;
this.index = index;
}
//覆盖方法
@Override
public String toString() {
return this.index+"_"+this.name;
}
}
有没有发现, 我们有成员变量name和index, 那我们就有气构造方法, 本人认为这个是要一一对应上的, 方便以后我们后面的使用.不过也看你代码的需要啊, 好的, 那我在返回上面的代码中:
3.1 更改枚举的属性
那我现在问一个问题, 如果我们现在要给AppStatusDTO设置一下Status的属性, 应该如何设置呢, 获取呢?
package javatest;
import org.junit.Test;
import java.util.EnumMap;
import java.util.EnumSet;
public class STest {
@Test
public void hello() {
AppStatusDTO appStatusDTO = new AppStatusDTO();
// 设置Status的变量, 这里我们直接设计为red就好, red就包含所有的值
appStatusDTO.setStatus(AppStatusDTO.Status.red);
// 获取Status的具体的值
System.out.println(appStatusDTO.getStatus().num);
}
}
3.2 获取枚举的所有类型
我们可以通过枚举的values的方法, 来遍历所有的枚举类型, 也可以通过EnumSet来遍历所有的枚举
代码如下:
package javatest;
import org.junit.Test;
import java.util.EnumMap;
import java.util.EnumSet;
public class STest {
@Test
public void hello() {
// 通过values的方法来遍历所有的枚举类型
for (AppStatusDTO.Status status: AppStatusDTO.Status.values()) {
System.out.println(status.num);
System.out.println(status.toString());
}
// 通过EnumSet来遍历所有的枚举
EnumSet<AppStatusDTO.Status> colors = EnumSet.allOf(AppStatusDTO.Status.class);
for (AppStatusDTO.Status color: colors) {
System.out.println(color);
}
}
}
3.3 根据枚举某一个属性的值获取该枚举
像我们上面的代码中, valueOf就是通过属性获取枚举的方法, 可以参考, 你也可以通过下面的方法实现:
静态代码块,在虚拟机加载类的时候就会加载执行,而且只执行一次;, 今天新get到的一个知识点.
public enum WorkFlowCodeTwEnum {
HB("华北","huabei"),
HN("华南","huanan"),
HZ("华中","huazhong"),
XN("西南","xinan"),
HD("华东","huadong");
private String code;
private String name;
}
private static final Map<String,WorkFlowCodeTwEnum> typeMap = Maps.newHashMap();
static {
for(WorkFlowCodeTwEnum s : EnumSet.allOf(WorkFlowCodeTwEnum.class)){
typeMap.put(s.getCode(), s);
}
}
public static WorkFlowCodeTwEnum getEnum(String code) {
return typeMap.get(code);
}
4. 使用枚举做线程安全的单例
大佬和<<Effective Java>>都推荐使用枚举做单例模式
// 定义单例模式中需要完成的代码逻辑
public interface MySingleton {
void doSomething();
}
public enum Singleton implements MySingleton {
INSTANCE {
@Override
public void doSomething() {
System.out.println("complete singleton");
}
};
public static MySingleton getInstance() {
return Singleton.INSTANCE;
}
}
首先, 我们先来看INSTANCE, 我们先来看这个, 这个并不是什么关键字, 就是我们工作中常用到的枚举的值, 类似于我们的 RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);等, 这个让我这个菜逼在刚看这个代码的时候一脸懵逼.
如果想了解清楚, 请查看这篇文章, 讲的是比较清楚的:https://juejin.im/post/5b285d236fb9a00e9b39fdd2, 但是我也来带着打击分析一下吧:
回答者引用了Joshua Bloch大神在《Effective Java》中明确表达过的观点:
使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
双重校验锁”实现单例:
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
枚举实现单例:
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
相比之下, 你就会觉得枚举实现单例, 简单的多
上面的双重锁校验的代码之所以很臃肿,是因为大部分代码都是在保证线程安全。为了在保证线程安全和锁粒度之间做权衡,代码难免会写的复杂些。但是,这段代码还是有问题的,因为他无法解决反序列化会破坏单例的问题。
4.1 枚举可解决线程安全问题
上面提到过。使用非枚举的方式实现单例,都要自己来保证线程安全,所以,这就导致其他方法必然是比较臃肿的。那么,为什么使用枚举就不需要解决线程安全问题呢?
其实,并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不需要我们关心而已。也就是说,其实在“底层”还是做了线程安全方面的保证的。
那么,“底层”到底指的是什么?
这就要说到关于枚举的实现了。这部分内容可以参考我的另外一篇博文深度分析Java的枚举类型—-枚举的线程安全性及序列化问题,这里我简单说明一下:
定义枚举时使用enum和class一样,是Java中的一个关键字。就像class对应用一个Class类一样,enum也对应有一个Enum类。
通过将定义好的枚举反编译,我们就能发现,其实枚举在经过javac
的编译之后,会被转换成形如public final class T extends Enum
的定义。
而且,枚举中的各个枚举项同时通过static
来定义的。如:
public enum T {
SPRING,SUMMER,AUTUMN,WINTER;
}
反编译后代码为:
public final class T extends Enum
{
//省略部分内容
public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUM$VALUES[];
static
{
SPRING = new T("SPRING", 0);
SUMMER = new T("SUMMER", 1);
AUTUMN = new T("AUTUMN", 2);
WINTER = new T("WINTER", 3);
ENUM$VALUES = (new T[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}
了解JVM的类加载机制的朋友应该对这部分比较清楚。static
类型的属性会在类被加载之后被初始化,我们在深度分析Java的ClassLoader机制(源码级别)和Java类的加载、链接和初始化两个文章中分别介绍过,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。