Java 5 中新增了枚举类型,它是一种特殊的数据类型,之所以特殊是因为它是一种 class 类型却又比 class 类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。🚀本篇章代码 Demo

# 定义枚举

下面这是在没有枚举类型的情况下定义常量常见的方法如下:

OrdinaryConstantDemo.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 普通方式定义常量
 * @DateTime: 2023-12-05 21:14
 * @Version:1.0
 **/
public class OrdinaryConstantDemo {
    /**
     * 星期一
     */
    public static final int MONDAY = 1;
    /**
     * 星期二
     */
    public static final int TUESDAY = 2;
    /**
     * 星期三
     */
    public static final int WEDNESDAY = 3;
    /**
     * 星期四
     */
    public static final int THURSDAY = 4;
    /**
     * 星期五
     */
    public static final int FRIDAY = 5;
    /**
     * 星期六
     */
    public static final int SATURDAY = 6;
    /**
     * 星期天
     */
    public static final int SUNDAY = 7;
}

在上述中的常量定义的方式称为 int 枚举模式,这种定义方法并没有什么错,但它存在许多不足的地方,如:类型安全和使用方便性上并没有多少好处,如果存在定义 int 值相同的变量,混淆的几率还是很大的,编译器并不会做出任何警告提示,因此这种方式在枚举出现后并不提倡,现在我们利用枚举类型来重新定义上述常量,定义方式如下:

DayType.java本示例使用JAVA 8
package top.rem.rain;
/**
 * 枚举定义,枚举类型使用 enum 关键字
 * @author LightRain
 */
public enum DayType {
    /**
     * 星期一
     */
    MONDAY,
    /**
     * 星期二
     */
    TUESDAY,
    /**
     * 星期三
     */
    WEDNESDAY,
    /**
     * 星期四
     */
    THURSDAY,
    /**
     * 星期五
     */
    FRIDAY,
    /**
     * 星期六
     */
    SATURDAY,
    /**
     * 星期天
     */
    SUNDAY
}

在定义枚举类型时我们使用的关键字是 enumclass 关键字类似,枚举类型 DayType 中分别定义了从周一到周日的值,注意: 一般是大写字母,多个值之间使用逗号分隔,同时我们应该知道的是枚举类型可以像 class 类型一样,定义为一个独立的文件,当然也可以定义在其它类内部,更重要的是枚举常量在类型安全性和便捷性都有保证,如果类型出现问题编译器会提示警告信息,请务必记住枚举类型表示的类型其取值是有限的,也就是说每个值都是可以枚举出来的,如何使用枚举描述上述的一周呢,代码如下:

DayTypeTest.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: DayType 测试
 * @DateTime: 2023-12-05 21:54
 * @Version:1.0
 **/
public class DayTypeTest {
  public static void main(String[] args) {
    // 引用内部枚举,在 cmd 中使用 javac 编译成 class 文件时不可引用外部枚举会显示错误:找不到符合错误
//        InternalDayType monday = InternalDayType.MONDAY;
    // 创建枚举数组
    InternalDayType[] days = new InternalDayType[]{InternalDayType.MONDAY, InternalDayType.TUESDAY, InternalDayType.WEDNESDAY, InternalDayType.THURSDAY, InternalDayType.FRIDAY, InternalDayType.SATURDAY, InternalDayType.SUNDAY};
    for (int i = 0; i < days.length; i++) {
      System.out.println("day[" + i + "].ordinal():" + days[i].ordinal());
    }
    System.out.println("-------------------------------------");
    // 通过 compareTo 方法比较,实际上其内部是通过 ordinal () 值比较的
    System.out.println("days[0].compareTo(days[1]):" + days[0].compareTo(days[1]));
    System.out.println("days[0].compareTo(days[1]):" + days[0].compareTo(days[2]));
    // 获取该枚举对象的 Class 对象引用,当然也可以通过 getClass 方法
    Class<?> clazz = days[0].getDeclaringClass();
    System.out.println("clazz:" + clazz);
    System.out.println("-------------------------------------");
    //name()
    System.out.println("days[0].name():" + days[0].name());
    System.out.println("days[1].name():" + days[1].name());
    System.out.println("days[2].name():" + days[2].name());
    System.out.println("days[3].name():" + days[3].name());
    System.out.println("-------------------------------------");
    System.out.println("days[0].toString():" + days[0].toString());
    System.out.println("days[1].toString():" + days[1].toString());
    System.out.println("days[2].toString():" + days[2].toString());
    System.out.println("days[3].toString():" + days[3].toString());
    System.out.println("-------------------------------------");
    InternalDayType d = Enum.valueOf(InternalDayType.class, days[0].name());
    InternalDayType d2 = InternalDayType.valueOf(InternalDayType.class, days[0].name());
    System.out.println("d:" + d);
    System.out.println("d2:" + d2);
        /*
        执行结果:
        day [0].ordinal ():0
        day [1].ordinal ():1
        day [2].ordinal ():2
        day [3].ordinal ():3
        day [4].ordinal ():4
        day [5].ordinal ():5
        day [6].ordinal ():6
        -------------------------------------
        days [0].compareTo (days [1]):-1
        days [0].compareTo (days [1]):-2
        clazz:class top.rem.rain.InternalDayType
        -------------------------------------
        days [0].name ():MONDAY
        days [1].name ():TUESDAY
        days [2].name ():WEDNESDAY
        days [3].name ():THURSDAY
        -------------------------------------
        days [0].toString ():MONDAY
        days [1].toString ():TUESDAY
        days [2].toString ():WEDNESDAY
        days [3].toString ():THURSDAY
        -------------------------------------
        d:MONDAY
        d2:MONDAY
         */
  }
  /**
   * 此处使用内部方式定义并使用 javac 来变成成 class 文件
   * 使用外部类会显示找不到文件
   */
  private enum InternalDayType {
    /**
     * 星期一
     */
    MONDAY,
    /**
     * 星期二
     */
    TUESDAY,
    /**
     * 星期三
     */
    WEDNESDAY,
    /**
     * 星期四
     */
    THURSDAY,
    /**
     * 星期五
     */
    FRIDAY,
    /**
     * 星期六
     */
    SATURDAY,
    /**
     * 星期天
     */
    SUNDAY
  }
}

直接引用枚举的值即可,这便是枚举类型的最简单的模型。

# 枚举实现原理

  • 我们了解了枚举类型的定义与简单使用后,现在有必要来了解一下枚举类型的基本实现原理。
  • 在使用关键字 enum 创建枚举类型并编译后,编译器会为我们生成一个相关的类。
  • 这个类继承了 Java API 中的 java.lang.Enum 类,通过关键字 enum 创建枚举类型在编译后事实上就是一个类类型。

使用 cmd 首先 cd 到存放 .java 文件的目录下后使用命令,使用 javac 命令在 cmd 窗口中编译 DayTypeTest.java 文件

命令行提示符
javac DayTypeTest.java -encoding UTF-8

利用 javac 将前面的 DayTypeTest.java 文件进行编译,编译后会在当前目录下出现生成的两个文件一个是 DayTypeTest.class 另一个则是它的枚举类型 InternalDayType.class ,也就验证前面说的使用关键字 enum 定义枚举类型并编译后,编译器会自动帮助我们生成一个与枚举相关的类,反编译文件如下:

  • IntelliJ IDEA 无法反编译出以下效果。
  • 工具使用 cfr-0.152.jar ,下载地址 https://www.benf.org/other/cfr/
  • cmd 窗口中使用的命令:
    命令行提示符
    java -jar cfr-0.152.jar --sugarenums false -outputpath 请将此处替换为反编译后的储存路径 InternalDayType.class
  • --sugarenums false 代表将使用 CFR 0_6 反编译,详细介绍请看此处 Java 1.5 枚举是如何实现的?
  • -outputpath 代表要保存反编译后的文件储存路径。
InternalDayType.java反编译工具下载
/*
 * IntelliJ IDEA 无法反编译出此效果
 * 工具使用 cfr-0.152.jar
 * 工具下载地址 https://www.benf.org/other/cfr/
 * cmd 窗口中使用的命令 java -jar cfr-0.152.jar --sugarenums false -outputpath D:\ InternalDayType.class
 * 反编译出如下效果请添加 --sugarenums false 语法
 * -outputpath 代表要保存反编译后的文件储存路径
 * Decompiled with CFR 0.152.
 */
package top.rem.rain;
// 反编译 InternalDayType.class
final class InternalDayType extends Enum<InternalDayType> {
    
    // 前面咱们自己定义的 7 种枚举实例,并实例化枚举
    public static final /* enum */ InternalDayType MONDAY = new InternalDayType("MONDAY", 0);
    public static final /* enum */ InternalDayType TUESDAY = new InternalDayType("TUESDAY", 1);
    public static final /* enum */ InternalDayType WEDNESDAY = new InternalDayType("WEDNESDAY", 2);
    public static final /* enum */ InternalDayType THURSDAY = new InternalDayType("THURSDAY", 3);
    public static final /* enum */ InternalDayType FRIDAY = new InternalDayType("FRIDAY", 4);
    public static final /* enum */ InternalDayType SATURDAY = new InternalDayType("SATURDAY", 5);
    public static final /* enum */ InternalDayType SUNDAY = new InternalDayType("SUNDAY", 6);
    private static final /* synthetic */ InternalDayType[] $VALUES;
    // 编译器为我们添加的静态 values () 方法
    public static InternalDayType[] values() {
        return (InternalDayType[])$VALUES.clone();
    }
    // 编译器为我们添加的静态 valuesOf () 方法
    // 注意返回时调用了 Enum 类的 valueOf () 方法
    public static InternalDayType valueOf(String string) {
        return Enum.valueOf(InternalDayType.class, string);
    }
    // 私有构造函数
    private InternalDayType(String string, int n) {
        super(string, n);
    }
    // 将实例化后的枚举添加到了静态块中,也就是静态常量池中
    static {
        $VALUES = new InternalDayType[]{MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY};
    }
}
  • 从上面反编译的代码可以看出编译器确实帮助我们生成了一个 InternalDayType 类 (注意:该类型是 final 类型的,将无法被继承) 而且该类继承自 java.lang.Enum 类,该类是一个抽象类。
  • 编译器还帮助我们生成了 7InternalDayType 类型的实例对象分别对应枚举中定义的 7 个日期,也就是使用关键字 enum 定义的 InternalDayType 类型中的每种日期枚举常量也是实实在在的 InternalDayType 实例对象,只不过代表的内容不一样。
  • 注意:编译器还为我们生成了两个静态方法,分别是 values()valuesOf() ,稍后分析它们的用法,使用关键字 enum 定义的枚举类型,在编译后也将转换为一个实实在在的类,而在该类中会存在每个在枚举类型中定义好变量的对应实例对象。
  • 如上述 MONDAY 枚举类型对应 public static final InternalDayType MONDAY = new InternalDayType("MONDAY", 0); ,同时编译器会为该类创建两个方法,分别是 values()valueOf()

下面我们深入来探究一下 java.lang.Enum 类以及 values()valueOf() 方法的用途。

# 枚举常见方法

# Enum 抽象类常见方法

Enum 是所有 Java 语音枚举类型的公共基本类 (注意: Enum 是抽象类),它的常见方法如下:

返回类型方法名称方法说明
intcompareTo(E o)比较此枚举与指定对象的顺序
booleanequals(Object other)当指定对象等于此枚举常量是,返回 true
Class<?>getDeclaringClass()返回与此枚举常量的枚举类型相对应的 Class 对象
Stringname()返回此枚举常量的名称,在其枚举声明中对其进行声明
intordinal()返回枚举常量的序数 (它在枚举声明中的位置,其中初始常量序数为零)
StringtoString()返回枚举常量的名称,它包含在声明中
static<T extends Enum<T>> Tstatic valueOf(Class<T> enumType,String name)返回带指定名称的指定枚举类型的枚举常量
  • 主要说明一下 ordinal() 方法,该方法获取的是枚举变量在枚举类中声明的顺序,下标从 0 开始。
    • 如:日期中的 MONDAY 在第一个位置,那么 MONDAYordinal 值就是 0
    • 如果 MONDAY 的声明位置发生变化,那么使用 ordinal() 方法获取到的值也随之变化,注意在大多数情况下我们都不应该首选使用该方法。
  • compareTo(E o) 方法则是比较枚举的大小,注意:其内部实现是根据每个枚举的 ordinal 值大小来进行比较的。
  • name() 方法与 toString() 几乎是等同的,都是输出变量的字符串形式。
  • valueOf(Class<T> enumType,String name) 方法是根据枚举类的 Class 对象和枚举名称获取枚举常量,该方法是静态的。
DayTypeTest.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: DayType 测试
 * @DateTime: 2023-12-05 21:54
 * @Version:1.0
 **/
public class DayTypeTest {
    public static void main(String[] args) {
        // 创建枚举数组
        InternalDayType[] days=new InternalDayType[]{InternalDayType.MONDAY, InternalDayType.TUESDAY, InternalDayType.WEDNESDAY, InternalDayType.THURSDAY, InternalDayType.FRIDAY, InternalDayType.SATURDAY, InternalDayType.SUNDAY};
        for (int i = 0; i <days.length ; i++) {
            System.out.println("day["+i+"].ordinal():"+days[i].ordinal());
        }
        System.out.println("-------------------------------------");
        // 通过 compareTo 方法比较,实际上其内部是通过 ordinal () 值比较的
        System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[1]));
        System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[2]));
        // 获取该枚举对象的 Class 对象引用,当然也可以通过 getClass 方法
        Class<?> clazz = days[0].getDeclaringClass();
        System.out.println("clazz:"+clazz);
        System.out.println("-------------------------------------");
        //name()
        System.out.println("days[0].name():"+days[0].name());
        System.out.println("days[1].name():"+days[1].name());
        System.out.println("days[2].name():"+days[2].name());
        System.out.println("days[3].name():"+days[3].name());
        System.out.println("-------------------------------------");
        System.out.println("days[0].toString():"+days[0].toString());
        System.out.println("days[1].toString():"+days[1].toString());
        System.out.println("days[2].toString():"+days[2].toString());
        System.out.println("days[3].toString():"+days[3].toString());
        System.out.println("-------------------------------------");
        InternalDayType d= Enum.valueOf(InternalDayType.class,days[0].name());
        InternalDayType d2=InternalDayType.valueOf(InternalDayType.class,days[0].name());
        System.out.println("d:"+d);
        System.out.println("d2:"+d2);
        /*
                执行结果:
                day [0].ordinal ():0
                day [1].ordinal ():1
                day [2].ordinal ():2
                day [3].ordinal ():3
                day [4].ordinal ():4
                day [5].ordinal ():5
                day [6].ordinal ():6
                -------------------------------------
                days [0].compareTo (days [1]):-1
                days [0].compareTo (days [1]):-2
                clazz:class top.rem.rain.InternalDayType
                -------------------------------------
                days [0].name ():MONDAY
                days [1].name ():TUESDAY
                days [2].name ():WEDNESDAY
                days [3].name ():THURSDAY
                -------------------------------------
                days [0].toString ():MONDAY
                days [1].toString ():TUESDAY
                days [2].toString ():WEDNESDAY
                days [3].toString ():THURSDAY
                -------------------------------------
                d:MONDAY
                d2:MONDAY
         */
    }
}
/**
 * 此处使用内部方式定义并使用 javac 来变成成 class 文件
 * 使用外部类会显示找不到文件
 */
enum InternalDayType {
    /**
     * 星期一
     */
    MONDAY,
    /**
     * 星期二
     */
    TUESDAY,
    /**
     * 星期三
     */
    WEDNESDAY,
    /**
     * 星期四
     */
    THURSDAY,
    /**
     * 星期五
     */
    FRIDAY,
    /**
     * 星期六
     */
    SATURDAY,
    /**
     * 星期天
     */
    SUNDAY
}

抽象类 Enum 类的基本内容到此就介绍完了,这里提醒一点就是, Enum 类内部会有一个构造函数,该类构造只能有编译器调用,无法手动操作,来看 Enum 类的主要源码。

Enum.java本示例使用JAVA 8
package java.lang;
// 实现了 Comparable
public abstract class Enum<E extends Enum<E>>
        implements Constable, Comparable<E>, Serializable {
    /**
     * 枚举字符串名称
     */
    private final String name;
  
    public final String name() {
        return name;
    }
    /**
     * 枚举顺序值
     */
    private final int ordinal;
  
    public final int ordinal() {
        return ordinal;
    }
    /**
     * 枚举的构造方法,只能由编译器调用
     * @param name 枚举字符串名称
     * @param ordinal 枚举顺序值
     */
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    public String toString() {
        return name;
    }
    public final boolean equals(Object other) {
        return this==other;
    }
    public final int hashCode() {
        return super.hashCode();
    }
    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
    /**
     * 比较的是 ordinal 的值
     * @param o 任意枚举类型
     * @return int
     */
    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();
        // 根据 ordinal 值比较大小
        return self.ordinal - other.ordinal;
    }
    /**
     * 获取声明类
     * @return Class<E>
     */
    @SuppressWarnings("unchecked")
    public final Class<E> getDeclaringClass() {
        // 获取 class 对象引用,getClass () 是 Object 方法
        Class<?> clazz = getClass();
        // 获取父类 Class 对象引用
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }
    /**
     * 返回带指定名称的指定枚举类型的枚举常量
     * @param enumClass
     * @param name
     * @return
     * @param <T>
     */
     public static <T extends Enum<T>> T valueOf(Class<T> enumClass,
                                                String name) {
         //enumClass.enumConstantDirectory ():获取到的是一个 map 集合,key 值就是 name 值,value 则是枚举变量值
         //enumConstantDirectory 是 class 对象内部的方法,根据 class 对象获取一个 map 集合的值 
         T result = enumClass.enumConstantDirectory().get(name);
         if (result != null)
            return result;
         if (name == null)
            throw new NullPointerException("Name is null");
         throw new IllegalArgumentException(
            "No enum constant " + enumClass.getCanonicalName() + "." + name);
     }
}

通过 Enum 源码可以知道, Enum 实现了 Comparable 接口,这也是可以使用 compareTo 比较的原因,当然 Enum 构造函数也是存在的,该构造函数只能由编译器调用,毕竟我们只能使用 enum 关键字来定义枚举,其它就交给编译器了。

Enum.java本示例使用JAVA 8
package java.lang;
// 实现了 Comparable
public abstract class Enum<E extends Enum<E>>
        implements Constable, Comparable<E>, Serializable {
    /**
     * 枚举字符串名称
     */
    private final String name;
  
    /**
     * 枚举顺序值
     */
    private final int ordinal;
    /**
     * 枚举的构造方法,只能由编译器调用
     * @param name 枚举字符串名称
     * @param ordinal 枚举顺序值
     */
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
}

# Values ()&ValueOf () 方法

  • values()valueOf(String name) 方法是编译器生成的 static 方法。
  • 从前面分析中,在 Enum 类中并没有出现 values() 方法,但 valueOf() 方法还是有出现的。
  • 只不过编译器生成的 valueOf() 方法需要传递一个 name 参数,而 Enum 自带的静态方法 valueOf() 则需要传递两个参数。
  • 从前面反编译后的代码可以看到,编译器生成的 valueOf() 方法最终还是调用了 Enum 类的 valueOf() 方法。
EnumDemo.java本示例使用JAVA 8
package top.rem.rain;
import java.util.Arrays;
/**
 * @Author: LightRain
 * @Description:
 * @DateTime: 2023-12-06 23:45
 * @Version:1.0
 **/
public class EnumDemo {
  public static void main(String[] args) {
    DayType[] days2 = DayType.values();
    System.out.println("day2:" + Arrays.toString(days2));
    DayType day = DayType.valueOf("MONDAY");
    System.out.println("day:" + day);
        /*
        执行结果:
        day2:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
        day:MONDAY
         */
  }
}

从结果上看就可以知道, values() 方法的作用就是获取枚举类中的所有变量,作为数组返回,而 valueOf(String name) 方法与 Enum 类中的 valueOf(String name) 方法的作用类似,根据名称获取枚举变量,只不过编译器生成的 valueOf(String name) 方法更简洁一些只需要传递一个参数,注意: values() 方法是由编译器插入到枚举类中的 static 方法,所以如果我们将枚举实例向上转型为 Enum ,那么 values() 方法将无法被调用,因为 Enum 类中并没有 values() 方法, valueOf(String name) 方法也是同样的道理。

EnumDemo2.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description:
 * @DateTime: 2023-12-07 20:41
 * @Version:1.0
 **/
public class EnumDemo2 {
  public static void main(String[] args) {
    // 正常使用
    DayType[] ds = DayType.values();
    // 向上转型 Enum
    Enum<DayType> e = DayType.MONDAY;
    // 无法调用,没有此方法
    //e.values();
  }
}

# Class 对象 & 枚举

在上述我们提到当枚举实例向上转型为 Enum 类型后, values() 方法将会失效,也就无法一次性获取所有枚举实例变量,但是由于 Class 对象的存在,即使不使用 values() 方法,还是可以一次获取到所有枚举实例变量的, Class 对象中存在如下方法:

返回类型方法名称方法说明
T[]getEnumConstants()返回该类枚举类型的所有元素,如果 Class 对象不是枚举类型则返回 null
booleanisEnum当且仅当该类声明为源代码中的枚举是返回 true

因此通过 getEnumConstants() 方法,同样可以轻而易举地获取所有枚举实例变量,下面通过代码来演示此功能,代码如下:

EnumDemo3.java本示例使用JAVA 8
package top.rem.rain;
import java.util.Arrays;
/**
 * @Author: LightRain
 * @Description:
 * @DateTime: 2023-12-07 20:45
 * @Version:1.0
 **/
public class EnumDemo3 {
  public static void main(String[] args) {
    // 向上转型 Enum
    Enum<DayType> e = DayType.MONDAY;
    // 获取 class 对象引用
    Class<?> clasz = e.getDeclaringClass();
    if (clasz.isEnum()) {
      DayType[] dsz = (DayType[]) clasz.getEnumConstants();
      System.out.println("DayType:" + Arrays.toString(dsz));
    }
        /*
         输出结果:
         DayType:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
         */
  }
}

我们通过 Enumclass 对象的 getEnumConstants() 方法仍能一次性获取所有的枚举实例常量。

# 枚举进阶用法

在前面的分析中,我们都是基于简单枚举类型的定义,也就是在定义枚举时只定义了枚举实例类型,并没有定义方法或者成员变量,实际上使用 enum 关键字定义的枚举类,除了不能使用继承 (因为编译器会自动为我们继承 Enum 抽象类,而 Java 只支持单继承,因此枚举类是无法手动实现继承的),可以把 enum 类当成常规类,就是可以说我们向 enum 类中添加方法和变量,甚至是 mian 方法。

# 枚举类添加方法与构造函数

重新定义一个日期枚举类,带有成员变量,描述该日期枚举类的作用,同时定义一个 getDescriptionChinese() 方法来返回描述内容,并将构造函数私有化,防止外部调用,在声明枚举实例时传入对应的描述,代码如下:

DayType2.java本示例使用JAVA 8
package top.rem.rain;
/**
 * 枚举进阶用法
 *
 * @author LightRain
 */
public enum DayType2 {
    /**
     * 星期一
     */
    MONDAY("星期一"),
    /**
     * 星期二
     */
    TUESDAY("星期二"),
    /**
     * 星期三
     */
    WEDNESDAY("星期三"),
    /**
     * 星期四
     */
    THURSDAY("星期四"),
    /**
     * 星期五
     */
    FRIDAY("星期五"),
    /**
     * 星期六
     */
    SATURDAY("星期六"),
    /**
     * 星期天,使用逗号分隔,使用分号结尾
     */
    SUNDAY("星期天");
    /**
     * 中文描述
     */
    private final String descriptionChinese;
    /**
     * 将构造私有化,防止外部调用
     *
     * @param descriptionChinese 中文描述
     */
    private DayType2(String descriptionChinese) {
        this.descriptionChinese = descriptionChinese;
    }
    /**
     * 获取中文描述
     *
     * @return String
     */
    public String getDescriptionChinese() {
        return descriptionChinese;
    }
    public static void main(String[] args) {
        for (DayType2 day : DayType2.values()) {
            System.out.println("name: " + day.name() + ",getDescriptionChinese: " + day.getDescriptionChinese());
        }
    }
    /*
        执行结果:
        name: MONDAY,getDescriptionChinese: 星期一
        name: TUESDAY,getDescriptionChinese: 星期二
        name: WEDNESDAY,getDescriptionChinese: 星期三
        name: THURSDAY,getDescriptionChinese: 星期四
        name: FRIDAY,getDescriptionChinese: 星期五
        name: SATURDAY,getDescriptionChinese: 星期六
        name: SUNDAY,getDescriptionChinese: 星期天
     */
}

enum 类中确实可以像定义常规类一样声明变量和成员方法,但是我们必须注意到,如果打算在 enum 类中定义方法,务必在声明完枚举实例后使用逗号分隔和使用分号来结束,倘若在枚举实例前定义任何方法,编译器都将会报错将无法通过编译,同时即使自定义了构造函数且 enum 的定义结束,我们也永远无法手动调用构造函数创建枚举实例,这件事只能由编译器来执行。

# 覆盖 enum 类方法

既然 enum 类跟常规类的定义没有什么区别 ( enum 类还是有些约束的),那么覆盖父类的方法也不会很难,可惜的是父类 Enum 中定义的方法只有 toString() 方法没有使用 final 来修饰,因此只能覆盖 toString() 方法,下面将通过代码覆盖 toString() 来省去编写 getDescriptionChinese() 方法,代码如下:

DayType3.java本示例使用JAVA 8
package top.rem.rain; 
/**
 * 枚举进阶用法 - 覆盖 toString 方法来省去 getDescriptionChinese () 方法
 *
 * @author LightRain
 */
public enum DayType3 {
    /**
     * 星期一
     */
    MONDAY("星期一"),
    /**
     * 星期二
     */
    TUESDAY("星期二"),
    /**
     * 星期三
     */
    WEDNESDAY("星期三"),
    /**
     * 星期四
     */
    THURSDAY("星期四"),
    /**
     * 星期五
     */
    FRIDAY("星期五"),
    /**
     * 星期六
     */
    SATURDAY("星期六"),
    /**
     * 星期天,使用逗号分隔,使用分号结尾
     */
    SUNDAY("星期天");
    /**
     * 中文描述
     */
    private final String descriptionChinese;
    /**
     * 将构造私有化,防止外部调用
     *
     * @param descriptionChinese 中文描述
     */
    private DayType3(String descriptionChinese) {
        this.descriptionChinese = descriptionChinese;
    }
    @Override
    public String toString() {
       return descriptionChinese;
    }
    public static void main(String[] args) {
        for (DayType3 day : DayType3.values()) {
            System.out.println("name: " + day.name() + ",getDescriptionChinese: " + day.toString());
        }
    }
    /*
        执行结果:
        name: MONDAY,getDescriptionChinese: 星期一
        name: TUESDAY,getDescriptionChinese: 星期二
        name: WEDNESDAY,getDescriptionChinese: 星期三
        name: THURSDAY,getDescriptionChinese: 星期四
        name: FRIDAY,getDescriptionChinese: 星期五
        name: SATURDAY,getDescriptionChinese: 星期六
        name: SUNDAY,getDescriptionChinese: 星期天
     */
}

# 自定义 enum 类抽象方法

与常规抽象类一样,在 enum 类中允许我们为其定义抽象方法,然后使每个枚举实例都实现该方法,以便产生不同的行为方式,注意: abstract 关键字对于枚举类来说并不是必须的,代码如下:

EnumDemo4.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 自定义 enum 类抽象方法
 * @DateTime: 2023-12-07 22:17
 * @Version:1.0
 **/
public enum EnumDemo4 {
  /**
   * 使用枚举静态块重写 getInfo 方法来为抽象方法添加具体实现
   */
  FIRST{
    @Override
    public String getInfo() {
      return "FIRST TIME";
    }
  },
  SECOND{
    @Override
    public String getInfo() {
      return "SECOND TIME";
    }
  };
  /**
   * 在枚举类中定义抽象方法
   * @return String
   */
  public abstract String getInfo();
  public static void main(String[] args) {
    System.out.println("F:"+EnumDemo4.FIRST.getInfo());
    System.out.println("S:"+EnumDemo4.SECOND.getInfo());
        /*
            执行结果:
            F:FIRST TIME
            S:SECOND TIME
         */
  }
}

通过这种方式就可以轻而易举地定义每个枚举实例的不同行为方式,我们可以注意到使用 enum 类实例似乎表现出了多态的特性,可惜的是枚举类型的实例终究不能作为类型传递使用,就像下面的使用方法,就无法通过编译器。

// 无法通过编译,毕竟 EnumDemo4.FIRST 是个实例对象
 public void text(EnumDemo4.FIRST instance){ }

以上就是在枚举实例常量中定义抽象方法。

# 枚举与接口

由于 Java 单继承的原因, enum 类不能继承其它类,但它不妨碍实现接口,因此 enum 类同样可以实现多接口,代码如下:

EnumDemo5.java本示例使用JAVA 8
package top.rem.rain;
/**
 * 食品接口
 */
interface Food {
  String eat();
}
/**
 * 运动接口
 */
interface Sport {
  String run();
}
/**
 * 使用 enum 类实现多接口
 * @author LightRain
 */
public enum EnumDemo5 implements Food,Sport{
  /**
   * 食品
   */
  FOOD,
  /**
   * 运动
   */
  SPORT;
  @Override
  public String eat() {
    return "xxx喜欢吃甜食";
  }
  @Override
  public String run() {
    return "xxx喜欢跑步";
  }
  public static void main(String[] args) {
    System.out.println(EnumDemo5.FOOD.eat());
    System.out.println(EnumDemo5.SPORT.run());
  }
    /*
        执行结果:
        xxx 喜欢吃甜食
        xxx 喜欢跑步
     */
}

有时候我们可能需要对一组数据进行分类,比如食品菜单分类而且希望这些菜单都属于 Foods 类型, SichuanCuisine(川菜)LuCuisine(鲁菜)Cantonese(粤菜) 等,每种分类下又有多种具体的菜式和食品,此时可以利用接口来组织,代码如下:

Foods.java本示例使用JAVA 8
package top.rem.rain;
/**
 * 食品菜单接口
 *
 * @author LightRain
 */
public interface Foods {
    /**
     * 川菜
     */
    enum SichuanCuisine implements Foods {
        /**
         * 蚂蚁上树
         */
        ANTS_ON_TREES("蚂蚁上树"),
        /**
         * 麻辣血旺
         */
        SPICY_BLOOD("麻辣血旺"),
        /**
         * 四川腊肉
         */
        SICHUAN_BACON("四川腊肉");
        /**
         * 菜品描述
         */
        private final String description;
        SichuanCuisine(String desc) {
            this.description = desc;
        }
        @Override
        public String toString() {
            return description;
        }
    }
    /**
     * 鲁菜
     */
    enum LuCuisine implements Foods {
        /**
         * 木须肉
         */
        MUSHU_MEAT("木须肉"),
        /**
         * 把子肉
         */
        PUT_THE_MEAT("把子肉"),
        /**
         * 德州扒鸡
         */
        TEXAS_GRILLED_CHICKEN("德州扒鸡");
        /**
         * 菜品描述
         */
        private final String description;
        LuCuisine(String desc) {
            this.description = desc;
        }
        @Override
        public String toString() {
            return description;
        }
    }
    /**
     * 粤菜
     */
    enum Cantonese implements Foods {
        /**
         * 碌鸭
         */
        DUCK("碌鸭"),
        /**
         * 虎皮凤爪
         */
        TIGER_SKIN_CHICKEN_FEET("虎皮凤爪"),
        /**
         * 炒河粉
         */
        FRIED_RIVER_NOODLES("炒河粉");
        /**
         * 菜品描述
         */
        private final String description;
        Cantonese(String desc) {
            this.description = desc;
        }
        @Override
        public String toString() {
            return description;
        }
    }
}

在接口中是可以定义枚举静态块的。

FoodMenu.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 食品菜单
 * @DateTime: 2023-12-07 22:58
 * @Version:1.0
 **/
public class FoodMenu {
  public static void main(String[] args) {
    Foods foods = Foods.SichuanCuisine.ANTS_ON_TREES;
    System.out.println("ANTS_ON_TREES:" + foods);
    foods = Foods.Cantonese.FRIED_RIVER_NOODLES;
    System.out.println("FRIED_RIVER_NOODLES:" + foods);
    foods = Foods.LuCuisine.TEXAS_GRILLED_CHICKEN;
    System.out.println("TEXAS_GRILLED_CHICKEN:" + foods);
        /*
            执行结果:
            ANTS_ON_TREES:蚂蚁上树
            FRIED_RIVER_NOODLES:炒河粉
            TEXAS_GRILLED_CHICKEN:德州扒鸡
         */
  }
}

通过这种方式就可以很方便组织上述场景需求,同时确保每种具体类型的事物都属于 Foods ,现在我们利用一个枚举嵌套枚举的方式,把前面定义的菜谱存放到一个 Meal 菜单中,通过这种方式就可以统一管理菜单数据了,代码如下:

Meal.java本示例使用JAVA 8
package top.rem.rain;
import java.util.Arrays;
/**
 * 食品菜单整合
 *
 * @author LightRain
 */
public enum Meal {
    /**
     * 川菜菜单
     */
    SICHUANCUISINE(Foods.SichuanCuisine.class),
    /**
     * 鲁菜菜单
     */
    LUCUISINE(Foods.LuCuisine.class),
    /**
     * 粤菜菜单
     */
    CANTONESE(Foods.Cantonese.class);
    private final Foods[] values;
    private Meal(Class<? extends Foods> kind) {
        // 通过 class 对象获取枚举实例
        this.values = kind.getEnumConstants();
    }
    public static void main(String[] args) {
        Foods[] foods = Meal.SICHUANCUISINE.values;
        Arrays.stream(foods).forEach(f -> {
            System.out.println("川菜菜单:" + f);
        });
        foods = Meal.LUCUISINE.values;
        Arrays.stream(foods).forEach(f -> {
            System.out.println("鲁菜菜单:" + f);
        });
        foods = Meal.CANTONESE.values;
        Arrays.stream(foods).forEach(f -> {
            System.out.println("粤菜菜单:" + f);
        });
        
        /*
            执行结果:
            川菜菜单:蚂蚁上树
            川菜菜单:麻辣血旺
            川菜菜单:四川腊肉
            鲁菜菜单:木须肉
            鲁菜菜单:把子肉
            鲁菜菜单:德州扒鸡
            粤菜菜单:碌鸭
            粤菜菜单:虎皮凤爪
            粤菜菜单:炒河粉
         */
    }
}

# 枚举与 switch

在使用 switch 进行条件判断时,条件参数一般只能是整形、字符型,而枚举型确实也被 switch 所支持,在 Java 1.7switch 也对字符串进行了支持, switch 与枚举类型的使用,代码如下:

EnumAndSwitch.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 枚举与 switch 饿汉式
 * @DateTime: 2023-12-08 20:45
 * @Version:1.0
 **/
public class EnumAndSwitch {
  public static void main(String[] args) {
    getColorName(Color.BLUE);
    getColorName(Color.RED);
        /*
         执行结果:
         蓝色
         红色
         */
  }
  private static void getColorName(Color color) {
    switch (color) {
      case BROWN -> System.out.println("棕色");
      case RED -> System.out.println("红色");
      case ORANGE -> System.out.println("橙色");
      case YELLOW -> System.out.println("黄色");
      case GREEN -> System.out.println("绿色");
      case BLUE -> System.out.println("蓝色");
      case PURPLE -> System.out.println("紫色");
      case GRAY -> System.out.println("灰色");
      case WHITE -> System.out.println("白色");
      case BLACK -> System.out.println("黑色");
    }
  }
  private enum Color {
    /**
     * 棕
     */
    BROWN,
    /**
     * 红
     */
    RED,
    /**
     * 橙
     */
    ORANGE,
    /**
     * 黄
     */
    YELLOW,
    /**
     * 绿
     */
    GREEN,
    /**
     * 蓝
     */
    BLUE,
    /**
     * 紫
     */
    PURPLE,
    /**
     * 灰
     */
    GRAY,
    /**
     * 白
     */
    WHITE,
    /**
     * 黑
     */
    BLACK;
  }
}

使用枚举进行 switch 条件判断时无需使用 Color 引用。

# 枚举与单例模式

单例模式它的作用是确保某个类只有一个实例,自行实例化并向整个系统提供整个实例,在实际应用中,线程池、缓存、日志对象、对话框对象常被设计成单例模式,总之选择单例模式就是为了避免状态不一致,下面是单例模式的几种主要编写方式,从而对比出使用枚举实现单例模式的优点。

# 饿汉式单例模式

HungryChineseStyle.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 饿汉式单例模式 (基于 classloder 机制避免了多线程的同步问题)
 * @DateTime: 2023-12-08 21:25
 * @Version:1.0
 **/
public class HungryChineseStyle {
    private static final HungryChineseStyle INSTANCE = new HungryChineseStyle();
    private HungryChineseStyle() {
    }
    public static HungryChineseStyle getInstance() {
        return INSTANCE;
    }
}

显然这种写法很简单,但问题是无法做到延迟创建对象,事实上如果该单例类涉及资源较多,创建比较耗费时间时,我们更希望它可以尽可能地延迟加载,从而减小初始化的负载,从而就有了懒汉式单例模式。

# 懒汉式单例模式

Lazy.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 懒汉式单例模式 (适合多线程安全)
 * @DateTime: 2023-12-08 23:05
 * @Version:1.0
 **/
public class Lazy {
    private static Lazy INSTANCE;
    private Lazy() {
    }
    public static synchronized Lazy getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Lazy();
        }
        return INSTANCE;
    }
}

这种写法能够在多线程中很好避免同步问题,同时也具备 lazy loading 机制,遗憾的是由于 synchronized 的存在,效率很低,在单线程的情境下完全可以去掉 synchronize 为了效率与性能问题,改进后的代码如下:

# 双重检查锁单例模式

DoubleCheckLock.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 懒汉式 - 双重检查锁
 * @DateTime: 2023-12-08 23:14
 * @Version:1.0
 **/
public class DoubleCheckLock {
  private static volatile DoubleCheckLock instance;
  private DoubleCheckLock(){
  }
  public static DoubleCheckLock getInstance(){
    if(instance==null){
      synchronized (DoubleCheckLock.class){
        if (instance==null){
          instance = new DoubleCheckLock();
        }
      }
    }
    return instance;
  }
}

这种方式被称为 双重检查锁 ,主要在 getInstance() 方法中,进行了两次 if(instance==null) 判空检查,可以极大提升并发度,进而提升性能,毕竟在单例模式中 new 的情况非常少,绝大多都是可以并行操作,因此在枷锁前多进行一次 null 检查可以减少绝大多数的枷锁操作,也就提高了执行效率。但是必须注意的是 volatile 关键字,该关键字有两层语义,第一层是可见性,可见性是指在一个线程中对该变量的修改会马上由工作内存 ( Work Memory ) 写回主内存 ( Main Memory ),所以其它线程会马上读取到已修改的值,关于工作内存和主内存可以简单理解为高速缓存 (直接与 CPU 打交道) 和主存 (代表内存条),注意工作内存是线程独享的,主存是线程共享的。 volatile 的第二次语义是禁止指令重排序优化,我们写的代码 (特别是多线代码),由于编译器优化,会在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源码相同,却不保证实际指令的顺序与源码相同,这在单线程中并没有什么问题,然而一旦引入多线程环境,这种乱序就可能导致严重问题。 volatile 关键字就可以从语义上解决这个问题,值得关注的是 volatile 的禁止指令重排序优化功能在 Java 1.5 后才得以实现,因此在 Java 1.5 之前的版本仍然是不安全的,即使使用 volatile 关键字,或许我们可以利用静态内部类来实现更安全的机制。

# 静态内部类单例模式

StaticInnerClass.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 静态内部类单例模式
 * @DateTime: 2023-12-08 23:34
 * @Version:1.0
 **/
public class StaticInnerClass {
  private static class Holder{
    private static final StaticInnerClass INSTANCE = new StaticInnerClass();
  }
  private StaticInnerClass(){
  }
  public static StaticInnerClass getInstance(){
    return Holder.INSTANCE;
  }
}

上述代码展示的就是静态内部类单例模式,我们把 INSTANCE 实例放到一个静态内部类中,这样就可以避免了静态实例在 INSTANCE 类的加载阶段就创建对象,毕竟静态变量初始化是在 StaticInnerClass 类初始化时触发的,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全。

# 防止被破坏单例模式

  • 从上述 4 种单例模式的写法中,似乎也解决了效率与懒加载问题,但是他们都有两个共同缺点:

    • 序列化可能会破坏单例模式,比较每次反序列化一个序列化的对象实例时都会创建一个新的实例,解决方案代码如下:
    Singleton.java本示例使用JAVA 8
    package top.rem.rain;
     
     import java.io.Serial;
     import java.io.Serializable;
     
     /**
      * @Author: LightRain
      * @Description: 避免反序列化破坏单例模式
      * @DateTime: 2023-12-08 23:48
      * @Version:1.0
      **/
     public class Singleton implements Serializable {
       public static Singleton INSTANCE = new Singleton();
     
       protected Singleton() {
     
       }
     
       /**
        * 反序列时直接返回当前 INSTANCE
        * @return Object;
        */
       @Serial
       protected Object readResolve() {
         return INSTANCE;
       }
     
     }
    • 使用反射强行调用私有构造器,解决方式可以修改构造器,让它在创建第二个实例的时候抛出异常,解决方案代码如下:

      Singleton2本示例使用JAVA 8
      package top.rem.rain;
      /**
       * @Author: LightRain
       * @Description: 避免使用反射强行破坏单例模式
       * @DateTime: 2023-12-08 23:56
       * @Version:1.0
       **/
      public class Singleton2 {
        public static final Singleton2 INSTANCE = new Singleton2();
        private static volatile boolean flag = true;
        private Singleton2() {
          if (flag) {
            flag = false;
          } else {
            throw new RuntimeException("The instance  already exists !");
          }
        }
        public static Singleton2 getInstance() {
          return INSTANCE;
        }
      }

如上所述,问题确实也得到了解决,但问题是我们为此付出了不少努力,即添加了不少的代码,还应该注意到如果单例类维持了其它对象的状态时还需要使它们成为 transient 对象,这种就更复杂了,那么有没有简单还高效的呢?当然那就是枚举单例了,来看看如何实现,如下:

EnumSingleton.java本示例使用JAVA 8
package top.rem.rain;
/**
 * 枚举单例模式
 *
 * @author LightRain
 */
public enum EnumSingleton {
    /**
     * 实例
     */
    INSTANCE;
    
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

代码相当简洁,我们也可以像常规类一样编写 enum 类,为其添加变量和方法,访问方式也更简单,使用 EnumSingleton.INSTANCE 进行访问,这样也就避免调用 getInstance() 方法,更重要的是使用枚举单例的写法,我们完全不用考虑序列化和反射问题,枚举序列化是由 JVM 保证的,每一个枚举类型和定义的枚举变量在 JVM 中都是唯一的,在枚举类型的序列化和反序列化上 Java 做了特殊的规定,在序列化是 Java 仅仅是将枚举对象的 name 属性输出到结果中,反序列化的时候则是通过 java.lang.EnumvalueOf() 方法来根据名字查找枚举对象,同时编译器是不允许任何对这种序列化机制的定制并禁用 writeObjectreadObjectreadObjectNoDatwriteReplacereadResolve 等方法,从而保证了枚举实例的唯一性,不妨来看看 Enum 类的 valueOf() 方法源码:

Enum.java版本请选择JAVA 8
package java.lang;
public abstract class Enum<E extends Enum<E>> implements Constable, Comparable<E>, Serializable {
  public static <T extends Enum<T>> T valueOf(Class<T> enumClass, String name) {
    T result = enumClass.enumConstantDirectory().get(name);
    if (result != null)
      return result;
    if (name == null)
      throw new NullPointerException("Name is null");
    throw new IllegalArgumentException(
            "No enum constant " + enumClass.getCanonicalName() + "." + name);
  }
    
}

实际上通过调用 enumClassenumConstantDirectory() 方法获取到的是一个 Map 集合,在该集合中存放了以枚举 namekey 和以枚举实例变量为 valuekey & value 数据,因此通过 name 的值就可以获取到枚举实例,下面是 enumConstantDirectory() 源码:

Class.java版本请选择JAVA 8
package java.lang;
public final class Class<T> implements java.io.Serializable,
        GenericDeclaration,
        Type,
        AnnotatedElement,
        TypeDescriptor.OfField<Class<?>>,
        Constable {
  Map<String, T> enumConstantDirectory() {
    Map<String, T> directory = enumConstantDirectory;
    if (directory == null) {
        //getEnumConstantsShared () 最终通过反射调用枚举类的 values 方法
      T[] universe = getEnumConstantsShared();
      if (universe == null)
        throw new IllegalArgumentException(getName() + " is not an enum class");
      directory = new HashMap<>((int)(universe.length / 0.75f) + 1);
      //map 存放了当前 enum 类的所有枚举实例变量,以 name 为 key
      for (T constant : universe) {
        directory.put(((Enum<?>)constant).name(), constant);
      }
      enumConstantDirectory = directory;
    }
    return directory;
  }
  private transient volatile Map<String, T> enumConstantDirectory;  
}

到这里我们也就可以看出枚举序列化确实不会重新创建新实例, JVM 保证了每个枚举实例变量的唯一性,下面就是来看使用反射能不能创建枚举,通过反射获取构造器并创建枚举,代码如下:

ReflectionCreateEnumeration.java本示例使用JAVA 8
package top.rem.rain;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
 * @Author: LightRain
 * @Description: 反射创建枚举实例测试
 * @DateTime: 2023-12-09 11:30
 * @Version:1.0
 **/
public class ReflectionCreateEnumeration {
  public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    // 获取枚举类的构造函数
    Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
    // 暴力破解
    declaredConstructor.setAccessible(true);
    // 创建枚举
    EnumSingleton otherInstance = declaredConstructor.newInstance("otherInstance", 8);
        
        /*
          执行结果:
          Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
            at java.base/java.lang.reflect.Constructor.newInstanceWithCaller (Constructor.java:492)
            at java.base/java.lang.reflect.Constructor.newInstance (Constructor.java:480)
            at top.rem.rain.ReflectionCreateEnumeration.main (ReflectionCreateEnumeration.java:19)
         */
  }
}
执行结果
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
	at top.rem.rain.ReflectionCreateEnumeration.main(ReflectionCreateEnumeration.java:19)

很显然是告诉我们不能使用反射创建枚举类,这是为什么呢?下面来继续探究 newInstance() 方法源码:

Constructor.java版本请选择JAVA 8
package java.lang.reflect;
public final class Constructor<T> extends Executable {
  @CallerSensitive
  @ForceInline // to ensure Reflection.getCallerClass optimization
  public T newInstance(Object ... initargs)
          throws InstantiationException, IllegalAccessException,
          IllegalArgumentException, InvocationTargetException
  {
    Class<?> caller = override ? null : Reflection.getCallerClass();
    return newInstanceWithCaller(initargs, !override, caller);
  }
  /* package-private */
  T newInstanceWithCaller(Object[] args, boolean checkAccess, Class<?> caller)
          throws InstantiationException, IllegalAccessException,
          InvocationTargetException
  {
    if (checkAccess)
      checkAccess(caller, clazz, clazz, modifiers);
    // 在这里判断 Modifier.ENUM 是不是枚举修饰的,如果是则抛出异常
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
      throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
      ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(args);
    return inst;
  }
  
}

从源码中来看很显然,确实无法使用反射创建枚举实例,也就说明了创建枚举实例只有编译器能够做到,显然枚举订单里模式确实是个不错的选择,因此我们推荐使用它。
但是这总不是万能的,对于 Android 平台这个可能未必是最好的选择,在 Android 开发中,内存优化是个大块头,而使用枚举时占用的内存尝尝是静态变量订单两倍,因此 Android 官方在内存优化方面给出的建议是尽量避免在 Android 中使用 enum 。不管如何,关于单例模式,我们总是应该记住:线程安全、延迟加载、序列化与反序列化安全,反射安全是很重要的。

# EnumMap

# EnumMap 基本用法

有这样一个问题,现在我们有一堆大小相同而颜色不同的数据,需要统计出每种颜色的数量是多少以便将数据录入仓库,定义如下枚举用于表示颜色,代码如下:

Colors.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @author LightRain
 */
public enum Colors {
    GREEN,RED,BLUE,YELLOW
}

解决方案如下,使用 Map 集合来统计, key 值作为颜色名称, value 代表运费数量。

EnumMapBasicUsage.java本示例使用JAVA 8
package top.rem.rain;
import java.util.*;
/**
 * @Author: LightRain
 * @Description: EnumMap 基本用法
 * @DateTime: 2023-12-09 13:01
 * @Version:1.0
 **/
public class EnumMapBasicUsage {
    public static void main(String[] args) {
        List<Clothes> list = new ArrayList<>();
        list.add(new Clothes("C001", Color.BLUE));
        list.add(new Clothes("C002", Color.YELLOW));
        list.add(new Clothes("C003", Color.RED));
        list.add(new Clothes("C004", Color.GREEN));
        list.add(new Clothes("C005", Color.BLUE));
        list.add(new Clothes("C006", Color.BLUE));
        list.add(new Clothes("C007", Color.RED));
        list.add(new Clothes("C008", Color.YELLOW));
        list.add(new Clothes("C009", Color.YELLOW));
        list.add(new Clothes("C010", Color.GREEN));
        // 第一种:使用 HashMap
        Map<String, Integer> map = new HashMap<>();
        for (Clothes clothes : list) {
            String colorName = clothes.getColor().name();
            Integer count = map.get(colorName);
            if (count != null) {
                map.put(colorName, count + 1);
            } else {
                map.put(colorName, 1);
            }
        }
        System.out.println(map.toString());
        System.out.println("---------------");
        // 第二种:使用 EnumMap
        Map<Color, Integer> enumMap = new EnumMap<>(Color.class);
        for (Clothes clothes : list) {
            Color color = clothes.getColor();
            Integer count = enumMap.get(color);
            if (count!=null){
                enumMap.put(color, count+1);
            }else {
                enumMap.put(color, 1);
            }
        }
        System.out.println(enumMap.toString());
        
        /*
          执行结果:
            {RED=2, BLUE=3, YELLOW=3, GREEN=2}
            ---------------
            {GREEN=2, RED=2, BLUE=3, YELLOW=3}
         */
    }
    
    private enum Color {
        /**
         * 绿色
         */
        GREEN,
        /**
         * 红色
         */
        RED,
        /**
         * 蓝色
         */
        BLUE,
        /**
         * 黄色
         */
        YELLOW
    }
    private static class Clothes {
        private String name;
        private Color colors;
        public Clothes(String name, Color colors) {
            this.name = name;
            this.colors = colors;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public Color getColors() {
            return colors;
        }
        public void setColors(Color colors) {
            this.colors = colors;
        }
        public Color getColor() {
            return Color.valueOf(colors.name());
        }
    }
}

我们使用了两种解决方案,第一种是 HashMap , 第二种是 EnumMap , 虽然都统计出了正确的结果,但是 EnumMap 作为枚举的专属集合,我们没有理由再去使用 HashMap ,毕竟 EnumMap 要求其 KEY 必须为 Enum 类型的,因此使用 Color 枚举实例作为 KEY 最恰当不过了,也避免了获取 name 的步骤,更重要的是 EnumMap 效率更高,因为其内部是通过数组实现的,注意 EnumMapKey 值不能为 null ,虽说是枚举专属集合,但其操作与一般的 Map 差不多,概括性来说 EnumMap 是专门为枚举类型量身定制的 Map 实现,虽然使用其它的 Map (如: HashMap ) 类型也能完成相同功能,但是使用 EnumMap 会更加高效,它只能接收同一枚枚举类型的实例作为键值且不能为 null ,由于枚举类型实例的数量相对固定并且有限,所以 EnumMap 使用数组来存放与枚举类型对应的值,毕竟数组是一段连续的内存空间,根据程序局部性原理,效率会相当高,下面来进一步了解 EnumMap 的用法。
EnumMap 构造函数如下:

EnumMap.java版本请选择JAVA 8
package java.util;
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable {
    
  // 创建一个具有指定键类型的空枚举映射
  public EnumMap(Class<K> keyType) {
    this.keyType = keyType;
    keyUniverse = getKeyUniverse(keyType);
    vals = new Object[keyUniverse.length];
  }
  
  // 创建一个其键类型与指定枚举映射相同的枚举映射,最初包含相同的映射关系 (如果有的话)
  public EnumMap(EnumMap<K, ? extends V> m) {
    keyType = m.keyType;
    keyUniverse = m.keyUniverse;
    vals = m.vals.clone();
    size = m.size;
  }
 
  // 创建一个枚举映射,从指定映射对其初始化
  public EnumMap(Map<K, ? extends V> m) {
    if (m instanceof EnumMap) {
      EnumMap<K, ? extends V> em = (EnumMap<K, ? extends V>) m;
      keyType = em.keyType;
      keyUniverse = em.keyUniverse;
      vals = em.vals.clone();
      size = em.size;
    } else {
      if (m.isEmpty())
        throw new IllegalArgumentException("Specified map is empty");
      keyType = m.keySet().iterator().next().getDeclaringClass();
      keyUniverse = getKeyUniverse(keyType);
      vals = new Object[keyUniverse.length];
      putAll(m);
    }
  }
  
}

HashMap 不同,它需要传递一个类型信息,即 Class 对象,通过这个参数 EnumMap 就可以根据类型信息初始化其内部数据结构,另外两个是初始化时传入一个 Map 集合,代码如下:

EnumMapConstructor.java本示例使用JAVA 8
package top.rem.rain;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
/**
 * @Author: LightRain
 * @Description: EnumMap 构造器示例
 * @DateTime: 2023-12-09 16:37
 * @Version:1.0
 **/
public class EnumMapConstructor {
    public static void main(String[] args) {
        // 第一种构造
        Map<Color, Integer> enumMap = new EnumMap<>(Color.class);
        
        // 第二种构造
        Map<Color, Integer> enumMap2 = new EnumMap<>(enumMap);
        
        // 第三种构造
        Map<Color, Integer> hashMap = new HashMap<>();
        hashMap.put(Color.GREEN, 2);
        hashMap.put(Color.BLUE, 3);
        Map<Color, Integer> enumMap3 = new EnumMap<>(hashMap);
    }
    private enum Color {
        /**
         * 绿色
         */
        GREEN,
        /**
         * 红色
         */
        RED,
        /**
         * 蓝色
         */
        BLUE,
        /**
         * 黄色
         */
        YELLOW
    }
}

至于 EnumMap 的方法,跟普通的 Map 几乎没有区别,注意与 HashMap 的主要不同在于构造器需要传递类型参数和 EnumMap 保证 Key 顺序与枚举中的顺序一致,但请记住 Key 不能为 null

# EnumMap 实现原理解析

这里我们主要分析其内部存储结构,添加查找的实现,了解了这几点后对 EnumMap 内部实现原理就比较清晰了,先来看数据结构和构造函数。

EnumMap.java版本请选择JAVA 8
package java.util;
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable {
  // Class 对象引用
  private final Class<K> keyType;
  // 存储 Key 值的数组
  private transient K[] keyUniverse;
  // 存储 Value 值的数组
  private transient Object[] vals;
  //map 的 size
  private transient int size = 0;
  // 构造函数
  public EnumMap(Class<K> keyType) {
    this.keyType = keyType;
    keyUniverse = getKeyUniverse(keyType);
    vals = new Object[keyUniverse.length];
  }
  
}

EnumMap 继承了 AbstractMap 类,因此 EnumMap 具备一般 Map 的使用方法, keyType 表示类型信息, keyUniverse 表示键数组,存储的是所有可能的枚举值, vals 数组表示键对应的值, size 表示键值对个数,在构造函数中通过 keyUniverse = getKeyUniverse(keyType); 初始化了 keyUniverse 数组的值,内部存储的是所有可能的枚举值,接着初始化了存在 Value 值的数组 vals ,其大小枚举实例的个数相同, getKeyUniverse 方法实现如下:

# getKeyUniverse 方法实现分析

EnumMap.java版本请选择JAVA 8
package java.util;
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable {
    
  /**
   * 意思就是返回枚举数组
   * Returns all of the values comprising K.
   * The result is uncloned, cached, and shared by all callers. -> 返回包含 K 的所有值。 结果是未克隆的、缓存的,并由所有调用方共享
   */
  private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) {
    // 最终调用到枚举类型的 values 方法,values 方法返回所有可能的枚举值
    return SharedSecrets.getJavaLangAccess().getEnumConstantsShared(keyType);
  }
}

将鼠标放入到 getEnumConstantsShared 上然后按 Ctrl+鼠标左边 进入方法内部,以此追查。

JavaLangAccess.java版本请选择JAVA 8
package jdk.internal.access;
public interface JavaLangAccess {
    
  <E extends Enum<E>> E[] getEnumConstantsShared(Class<E> klass);
  
}
System.java版本请选择JAVA 8
package java.lang;
public final class System {
  public <E extends Enum<E>> E[] getEnumConstantsShared(Class<E> klass) {
    return (Enum[])klass.getEnumConstantsShared();
  }
    
}
Class.java版本请选择JAVA 8
package java.lang;
public final class Class<T> implements java.io.Serializable, GenericDeclaration, Type, AnnotatedElement, TypeDescriptor.OfField<Class<?>>, Constable {
  T[] getEnumConstantsShared() {
    T[] constants = enumConstants;
    if (constants == null) {
      if (!isEnum()) return null;
      try {
         //values 的调用就在这
        final Method values = getMethod("values");
        java.security.AccessController.doPrivileged(
                new java.security.PrivilegedAction<>() {
                  public Void run() {
                    values.setAccessible(true);
                    return null;
                  }
                });
        @SuppressWarnings("unchecked")
        T[] temporaryConstants = (T[])values.invoke(null);
        enumConstants = constants = temporaryConstants;
      }
      // These can happen when users concoct enum-like classes
      // that don't comply with the enum spec.
      catch (InvocationTargetException | NoSuchMethodException |
             IllegalAccessException ex) { return null; }
    }
    return constants;
  }
  private transient volatile T[] enumConstants;
    
}

从方法的返回值来看,返回类型是枚举数组,事实也是如此,最终返回值正是枚举类型的 values 方法的返回值,因此 keyUniverse 数组存储就是枚举类型的所有可能的枚举值。

# put 方法实现分析

put 方法实现代码如下:

EnumMap.java版本请选择JAVA 8
package java.util;
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable {
  public V put(K key, V value) {
    // 检测 key 的类型
    typeCheck(key);
    // 获取存放 value 值的数组下标
    int index = key.ordinal();
    // 设置旧值
    Object oldValue = vals[index];
    // 设置 value 值
    vals[index] = maskNull(value);
    if (oldValue == null)
      size++;
    // 返回旧值
    return unmaskNull(oldValue);
  }
    
}

这里通过 typeCheck 方法进行了 key 类型检测,判断是否为枚举类型,如果类型不对则会抛出异常。

EnumMap.java版本请选择JAVA 8
package java.util;
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable {
  private void typeCheck(K key) {
    Class<?> keyClass = key.getClass();
    if (keyClass != keyType && keyClass.getSuperclass() != keyType)
      throw new ClassCastException(keyClass + " != " + keyType);
  }
    
}

接着通过 key.ordinal() 方式获取到该枚举实例的顺序值,利用此值作为下标,把值存储在 vals 数组对应下标的元素中 (即 vals[index] ),这也是为什么 EnumMap 能维持与枚举实例相同存储顺序的原因,我们发现在对 vals[] 中元素进行赋值和返回旧值时分别调用了 maskNull()unmaskNull() 方法。

EnumMap.java版本请选择JAVA 8
package java.util;
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable {
    
  // 代表 Null 值的空对象实例
  private static final Object NULL = new Object() {
    public int hashCode() {
      return 0;
    }
    public String toString() {
      return "java.util.EnumMap.NULL";
    }
  };
  private Object maskNull(Object value) {
    // 如果值为空,返回 NULL 对象否则返回 value 
    return (value == null ? NULL : value);
  }
  @SuppressWarnings("unchecked")
  private V unmaskNull(Object value) {
    // 将 NULL 对象转换为 null 值
    return (V)(value == NULL ? null : value);
  }
    
}

由此看来 EnumMap 还是允许存放 null 的,但 key 是绝对不能为 null ,对于 nullEnumMap 进行了特殊处理,将其包装为 NULL 对象,毕竟 vals[] 存的是 ObjectmaskNull()unmaskNull() 方法正是用于 null 的包装和解包装的,这就是 EnumMap 集合的添加过程。

# get 方法实现分析

get() 方法实现代码如下:

EnumMap.java版本请选择JAVA 8
package java.util;
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable {
  public V get(Object key) {
    return (isValidKey(key) ?
            unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
  }
   
  // 对 Key 值的有效性和类型信息进行判断
  private boolean isValidKey(Object key) {
    if (key == null)
      return false;
    // Cheaper than instanceof Enum followed by getDeclaringClass
    Class<?> keyClass = key.getClass();
    return keyClass == keyType || keyClass.getSuperclass() == keyType;
  }
    
}

相对应 put() 方法, get() 方法显示相当简洁, key 有效的话直接通过 ordinal 方法取索引,然后在值数组 vals 里通过索引获取值返回。

# remove 方法实现分析

remove() 方法实现代码如下:

EnumMap.java版本请选择JAVA 8
package java.util;
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable {
  public V remove(Object key) {
    // 判断 key 值是否有效
    if (!isValidKey(key))
      return null;
    // 直接获取索引
    int index = ((Enum<?>)key).ordinal();
    Object oldValue = vals[index];
    // 对应下标元素值设置为 null
    vals[index] = null;
    if (oldValue != null)
      size--;
    return unmaskNull(oldValue);
  }
    
}

如果 key 值有效就通过 key 获取下标索引值,把 vals[] 对应的下标值设置为 null 然后 size 减一。

# containsValue&containsKey 方法实现分析

containsValue()containsKey() 方法实现代码如下:

EnumMap.java版本请选择JAVA 8
package java.util;
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable {
  // 判断是否包含 value
  public boolean containsValue(Object value) {
    value = maskNull(value);
    // 遍历数组
    for (Object val : vals)
      if (value.equals(val))
        return true;
    return false;
  }
  
  // 判断是否包含 key
  public boolean containsKey(Object key) {
    return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null;
  }
    
}

判断 value 直接通过遍历数组实现,而判断 key 就很简单了,判断 key 是否有效和对应 vals[] 中是否存在该值。
以上这就是 EnumMap 的主要实现原理,即内部有两个数组,长度相同,一个表示所有可能的键 (枚举值),一个表示对应的值,不允许 keynull ,但允许 valuenull ,键都有一个对应的索引,根据索引直接访问和操作其键数组和值数组,由于操作的都是数组,因此效率很高。

# EnumSet

EnumSet 是与枚举类型一起使用的专属 EnumSet 集合, EnumSet 中所有元素都必须是枚举类型,与其它 Set 接口的实现类 HashSetTreeSet 其内部都是用对应的 HashSetTreeMap 实现的,不同的是 EnumSet 在内部实现是 位向量 ,它是一种极为高效的位运算操作,由于直接存储和操作都是 bit ,因此 EnumSet 空间和时间性能都十分可观,足以媲美传统上基于 int位标志 的运算,重要的是我们可以像操作 set 集合一般来操作 为运算 ,这样使用代码更简单易懂同时又具备类型安全的优势。
注意: EnumSet 不允许使用 null 元素,试图插入 null 元素将会抛出 NullPointerException ,但试图测试判断是否存在 null 元素或移除 null 元素则不会抛出异常,与大多数 collection 实现一样, EnumSet 不是线程安全的,因此在多线程环境下一个注意数据同步问题。

# EnumSet 基本用法

创建 EnumSet 并不能使用 new 关键字,因为它是一个抽象类,而应该使用其提供的静态工厂方法, EnumSet 的静态工厂方法比较多,代码如下:

EnumSet.java版本请选择JAVA 8
package java.util;
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E> implements Cloneable, java.io.Serializable {
  // 创建一个具有指定元素类型的空 EnumSet
  public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
    Enum<?>[] universe = getUniverse(elementType);
    if (universe == null)
      throw new ClassCastException(elementType + " not an enum");
    if (universe.length <= 64)
      return new RegularEnumSet<>(elementType, universe);
    else
      return new JumboEnumSet<>(elementType, universe);
  }
  // 创建一个指定元素类型并包含所有枚举值的 EnumSet
  public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) {
    EnumSet<E> result = noneOf(elementType);
    result.addAll();
    return result;
  }
  // 创建一个包括枚举值中指定范围元素的 EnumSet
  public static <E extends Enum<E>> EnumSet<E> range(E from, E to) {
    if (from.compareTo(to) > 0)
      throw new IllegalArgumentException(from + " > " + to);
    EnumSet<E> result = noneOf(from.getDeclaringClass());
    result.addRange(from, to);
    return result;
  }
  // 创建一个包括参数中所有元素的 EnumSet
  public static <E extends Enum<E>> EnumSet<E> of(E e) {
    EnumSet<E> result = noneOf(e.getDeclaringClass());
    result.add(e);
    return result;
  }
  public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2) {
    EnumSet<E> result = noneOf(e1.getDeclaringClass());
    result.add(e1);
    result.add(e2);
    return result;
  }
  public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3) {
    EnumSet<E> result = noneOf(e1.getDeclaringClass());
    result.add(e1);
    result.add(e2);
    result.add(e3);
    return result;
  }
  public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4) {
    EnumSet<E> result = noneOf(e1.getDeclaringClass());
    result.add(e1);
    result.add(e2);
    result.add(e3);
    result.add(e4);
    return result;
  }
  public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5) {
    EnumSet<E> result = noneOf(e1.getDeclaringClass());
    result.add(e1);
    result.add(e2);
    result.add(e3);
    result.add(e4);
    result.add(e5);
    return result;
  }
  @SafeVarargs
  public static <E extends Enum<E>> EnumSet<E> of(E first, E... rest) {
    EnumSet<E> result = noneOf(first.getDeclaringClass());
    result.add(first);
    for (E e : rest)
      result.add(e);
    return result;
  }
  // 创建一个包含参数容器中的所有元素的 EnumSet
  public static <E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s) {
    return s.clone();
  }
  public static <E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c) {
    if (c instanceof EnumSet) {
      return ((EnumSet<E>)c).clone();
    } else {
      if (c.isEmpty())
        throw new IllegalArgumentException("Collection is empty");
      Iterator<E> i = c.iterator();
      E first = i.next();
      EnumSet<E> result = EnumSet.of(first);
      while (i.hasNext())
        result.add(i.next());
      return result;
    }
  }
  
}

代码示例如下:

EnumSetDemo.java本示例使用JAVA 8
package top.rem.rain;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
/**
 * @Author: LightRain
 * @Description: EnumSet 用法示例
 * @DateTime: 2023-12-10 11:09
 * @Version:1.0
 **/
public class EnumSetDemo {
    public static void main(String[] args) {
        // 创建一个空 Set 集合
        EnumSet<Color> enumSet = EnumSet.noneOf(Color.class);
        System.out.println("添加前的EnumSet:" + enumSet);
        enumSet.add(Color.BLUE);
        enumSet.add(Color.BLACK);
        enumSet.add(Color.GREEN);
        enumSet.add(Color.YELLOW);
        enumSet.add(Color.RED);
        System.out.println("添加后的EnumSet:" + enumSet);
        System.out.println("------------------------------------------------");
        // 使用 allOf 创建包含所有枚举类型的 enumSet,其内部根据 Class 对象初始化了所有枚举实例
        EnumSet<Color> enumSet1 = EnumSet.allOf(Color.class);
        System.out.println("allOf直接填充:" + enumSet1);
        System.out.println("------------------------------------------------");
        // 初始集合包括枚举值中指定范围的元素
        EnumSet<Color> enumSet2 = EnumSet.range(Color.RED, Color.BLUE);
        System.out.println("指定初始化范围:" + enumSet2);
        System.out.println("------------------------------------------------");
        // 指定补集,也就是从全部枚举类型中去除参数集合中的元素,如下去掉上述 enumSet2 的元素
        EnumSet<Color> enumSet3 = EnumSet.complementOf(enumSet2);
        System.out.println("指定补集:" + enumSet3);
        System.out.println("------------------------------------------------");
        // 初始化时直接指定元素
        EnumSet<Color> enumSet4 = EnumSet.of(Color.YELLOW);
        System.out.println("指定Color.YELLOW元素:" + enumSet4);
        EnumSet<Color> enumSet5 = EnumSet.of(Color.YELLOW, Color.RED);
        System.out.println("指定Color.YELLOW和Color.RED元素:" + enumSet5);
        System.out.println("------------------------------------------------");
        // 复制 enumSet5 容器的数据作为初始化数据
        EnumSet<Color> enumSet6 = EnumSet.copyOf(enumSet5);
        System.out.println("enumSet6:" + enumSet6);
        System.out.println("------------------------------------------------");
        List<Color> list = new ArrayList<>();
        list.add(Color.RED);
        // 此处添加一个重复元素
        list.add(Color.RED);
        list.add(Color.GREEN);
        list.add(Color.YELLOW);
        System.out.println("list:" + list);
        // 使用 copyOf (Collection<E> c)
        EnumSet<Color> enumSet7 = EnumSet.copyOf(list);
        System.out.println("enumSet7:" + enumSet7);
        /*
          执行结果:
            添加前的 EnumSet:[]
            添加后的 EnumSet:[GREEN, RED, BLUE, BLACK, YELLOW]
            ------------------------------------------------
            allOf 直接填充:[GREEN, RED, BLUE, BLACK, YELLOW]
            ------------------------------------------------
            指定初始化范围:[RED, BLUE]
            ------------------------------------------------
            指定补集:[GREEN, BLACK, YELLOW]
            ------------------------------------------------
            指定 Color.YELLOW 元素:[YELLOW]
            指定 Color.YELLOW 和 Color.RED 元素:[RED, YELLOW]
            ------------------------------------------------
            enumSet6:[RED, YELLOW]
            ------------------------------------------------
            list:[RED, RED, GREEN, YELLOW]
            enumSet7:[GREEN, RED, YELLOW]
         */
        
    }
    private enum Color {
        /**
         * 绿色
         */
        GREEN,
        /**
         * 红色
         */
        RED,
        /**
         * 蓝色
         */
        BLUE,
        /**
         * 黑色
         */
        BLACK,
        /**
         * 黄色
         */
        YELLOW
    }
}

noneOf(Class<E> elementType) 静态方法,主要用于创建一个空的 EnumSet 集合,传递参数 elementType 代表的是枚举类型的类型信息,即 Class 对象。
EnumSet<E> allOf(Class<E> elementType) 静态方法则是创建一个填充了 elementType 类型所代表的所有枚举实例,奇怪的是 EnumSet 提供了多个重载形式的 of() 方法,最后一个接受的是可变参数,其它重载方法则是固定参数个数, EnumSet 之所以这样设计是因为可变参数的运行效率低一些,所有在参数数据不多的情况下,强烈不建议使用传递参数为可变参数的 of() 方法,即 EnumSet<E> of(E first, E... rest) 。 至于 EnumSet 的操作方法与 set 集合一样,什么时候使用 EnumSet 比较好呢?事实上当需要进行位域运算时就可以使用 EnumSet 了。

# 位域运算

位域运算代码示例如下:

BitFieldDemo.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 位域运算代码示例
 * @DateTime: 2023-12-10 13:12
 * @Version:1.0
 **/
public class BitFieldDemo {
    // 定义位域变量
    /**
     * 1
     */
    public static final int TYPE_ONE = 1 << 0;
    /**
     * 2
     */
    public static final int TYPE_TWO = 1 << 1;
    /**
     * 4
     */
    public static final int TYPE_THREE = 1 << 2;
    /**
     * 8
     */
    public static final int TYPE_FOUR = 1 << 3;
    public static void main(String[] args) {
        // 位域运算
        int type = TYPE_ONE | TYPE_TWO | TYPE_THREE | TYPE_FOUR;
        System.out.println("type = " + type);
        /*
          执行结果:
          type = 15
         */
    }
}

诸如上述情况,我们都可以将上述的类型定义成枚举然后采用 EnumSet 来装载,进行各种操作,这样不仅不用手动编写太多冗余代码,而且使用 EnumSet 集合进行操作也将使代码更加简洁。

EnumSetDemo2.java本示例使用JAVA 8
package top.rem.rain;
import java.util.EnumSet;
/**
 * @Author: LightRain
 * @Description: 枚举替代位域
 * @DateTime: 2023-12-10 13:23
 * @Version:1.0
 **/
public class EnumSetDemo2 {
    public static void main(String[] args) {
        EnumSet<Type> set = EnumSet.of(Type.TYPE_ONE,Type.TYPE_FOUR);
    }
    private enum Type {
        /**
         * 1
         */
        TYPE_ONE,
        /**
         * 2
         */
        TYPE_TWO,
        /**
         * 3
         */
        TYPE_THREE,
        /**
         * 4
         */
        TYPE_FOUR
    }
}

EnumSet 其中最有价值的是其内部实现原理,采用的是位向量,它体现出来的是一种高效的数据处理方式,这点很值得我们去学习它。

# EnumSet 实现原理分析

关于 EnumSet 实现原理会有点绕脑,其内部执行几乎都是位运算。

# 理解位向量

分析 EnumSet 前有必要先了解一下 位向量 ,顾名思义 位向量 就是用一个 bit 位 ( 01 ) 标记一个元素的状态,用一组 bit 位表示一个集合的状态,而每个位对应一个元素,每个 bit 位的状态只可能有两种,即 01位向量 能表示的元素个数与向量的 bit 位长度有关,如:一个 int 类型能表示 32 个元素,而应该 long 类型则可以表示 64 个元素,对于 EnumSet 而言采用的就是 long 类型或者 long 类型数组。比如现在有一个文件中的数据,该文件存储了 N = 1000000 个无序的整数,需要把这些整数读取到内存并排序再重新写回文件中,该如何解决?最简单的方式是用 int 类型来存储每个数,并把其存入到数组中,再进行排序,但是这种方式将会导致存储空间异常地大,对数据操作起来效率也是问题,那么有没有更高效的方式呢?那就是运用 位向量 ,我们知道一个 int 类型的数有 4 个字节,也就是 32 位,那么我们可以用 N/32int 类型数组来表示 N 个数。

a [0] 表示第 1~32 个数(0~31)
a [1] 表示第 33~64 个数(32~63)
a [2] 表示第 65~96 个数(64~95)
...... 以此类推

这样一来每当输入一个数字 m 后,我们应该先找到该数字在数组的第 ? 个元素,也就是 a[?] 然后再确定在这个元素的第几个 bit 位,找到后设置为 1 ,代表存在该数字。
比如:输入 40 那么 40/3218 ,则应该将 a[1] 元素值的第 9bit 位设置为 1 ( 1 的二进制左移 8 位后就是第 9 个位置),表示该数字存在, 40 数字的表示原理图过程如下:

piRYuHx.png

在大概明白了位向量表示方式后,上述过程的计算方式,通过以下方式可以计算该数存储在数组的第 ? 个元素和元素中第 ?bit 位置,为了方便演示,我们在这里假设第 ? 个元素中的 ?P 余数值为 S

//m 除以 2^n 则商 (P) 表示为 m >> n
// 等同于 m / 2^5 取整数 即:40 / 32 = 1
// 那么 P=1 就是数组中的第 2 个元素,即 a [1]
// 位操作过程如下,40 的二进制
00000000 00000000 00000000 00101000
// 右移 5 位即 n=5,m >> 5, 即结果转为 10 进制就是 P=1
00000000 00000000 00000000 00000001

在这里我们使用 int 类型,即 32 位,所以 2^5=32 ,因此 n=5 , 由此计算出 P 的值代表的是数组的第 P 个元素,接着利用下述方式计算出余数 ( S ), 以此设置该元素的第 ( S+1 ) 个 bit 位为 1

//m 除以 2^n 的余数 (S) 表示为 m & (2^n-1)
// 等同于: m % 2^5 取余数 即:40 % 32 = 8
//m=40 的二进制
00000000 00000000 00000000 00101000
// 2^n-1 (31) 的二进制
00000000 00000000 00000000 00011111
//m & (2^n-1) 即 40 与 31 进行与操作得出余数 即 S=8
00000000 00000000 00000000 00001000
// 下面是将 a [1] 元素值的第 (8+1) 个 bit 设置为 1,为什么是 (8+1) 而不是 8?因为 1 左移 8 位就在第 9 个 bit 位了,过程如下:
// 1 的二进制如下:
00000000 00000000 00000000 00000001
// 1 << 8 利用余数 8 对 1 进行左移动
00000000 00000000 00000001 0000000
// 然后再与 a [1] 执行或操作后就可以将对应的 bit 为设置为 1
//a [P] |= 1 << S 下面将使用 Java 代码来实现

通过上述二进制位运算就可以计算出整数部分 P 和余数部分 S 并成功设置 bit 位为 1 ,下面是用 Java 代码来实现运算过程,代码如下:

# 1️⃣定义位运算变量

BitOperationDemo.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 位运算示例:定义全局变量
 * @DateTime: 2023-12-10 16:15
 * @Version:1.0
 **/
public class BitOperationDemo {
    /**
     * 存储元素的数组
     */
    private int[] a;
    /**
     * 默认使用 int 类型
     */
    private static final int BIT_LENGTH = 32;
    /**
     * 整数部分
     */
    private static int P;
    /**
     * 余数
     */
    private static int S;
    /**
     * 2^5 - 1
     */
    private static final int MASK = 0x1F;
    /**
     * 2^n SHIFT=n=5 表示 2^5=32 即 bit 位长度 32
     */
    private static final int SHIFT = 5;
    
}

# 2️⃣位运算添加

BitOperationDemo.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 位运算示例
 * @DateTime: 2023-12-10 16:15
 * @Version:1.0
 **/
public class BitOperationDemo {
    /**
     * 置位和添加操作
     * @param i
     */
    public void set(int i){
        // 1. 结果等同  P = i / BIT_LENGTH; 取整数
        P = i >> SHIFT;
        // 2. 结果等同  S = i % BIT_LENGTH; 取余数
        S = i & MASK;
        // 3. 赋值设置该元素 bit 位为 1
        a[P] |= 1 << S;
        // 4. 将 int 型变量 j 的第 k 个比特位设置为 1, 即 j=j|(1<<k), 上述 3 句合并为一句
        //a[i >> SHIFT ] |= (1 << (i & MASK));
    }
}

在计算出 PS 后就可以进行赋值了,其中 a[P] 代表数组中第 P 个元素, a[P] |= 1 << S 整句意思就是把 a[P] 元素的第 S+1 位设置为 1 ,注意从低位到高位设置 (即:从右到左), 1. 2. 3. 合并为 4. ,代码将更佳简洁。
既然有添加操作就会有删除操作,删除操作的过程与添加类似,只不过删除是把相对应的 bit 位设置为 0 ,代表不存在该数值。

# 3️⃣位运算清除

BitOperationDemo.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 位运算示例
 * @DateTime: 2023-12-10 16:15
 * @Version:1.0
 **/
public class BitOperationDemo {
    /**
     * 置 0 操作,相当于清除元素
     *
     * @param i
     */
    public void clear(int i) {
        // 1. 计算位于数组中第?个元素 P = i / BIT_LENGTH;
        P = i >> SHIFT;
        // 2. 计算余数 S = i % BIT_LENGTH;
        S = i & MASK;
        // 3. 把 a [P] 元素的第 S+1 个 (从低位到高位) bit 位设置为 0
        a[P] &= ~(1 << S);
        // 4. 将 int 型变量 j 的第 k 个比特位设置为 0,即 j= j&~(1<<k)
        //a[i>>SHIFT] &= ~(1<<(i &MASK));
    }
}

与添加唯一不同的是,计算出余数 S 后利用 1 左移 S 位,再取反 ( ~ ) 操作,最后进行与 ( & ) 操作,即:将 a[P] 元素的第 S+1 个 (从低位到高位) bit 位设置为 0 ,表示删除该数字,这个计算过程可以自行推算一下,这就是 位向量 表示法的添加和清除方法。

# 4️⃣位运算读取

BitOperationDemo.java本示例使用JAVA 8
package top.rem.rain;
/**
 * @Author: LightRain
 * @Description: 位运算示例
 * @DateTime: 2023-12-10 16:15
 * @Version:1.0
 **/
public class BitOperationDemo {
    /**
     * 读取操作,返回 1 代表该 bit 位有值,返回 0 代表该 bit 位没值
     * @param i
     * @return int
     */
    public int get(int i){
        //a[i>>SHIFT] & (1<<(i&MASK));
        P = i >> SHIFT;
        S = i &  MASK;
        return Integer.bitCount(a[P] & (1 << S));
    }
}

其中 Integer.bitCount() 是返回指定 int 值的二进制补码 (计算机数字的二进制表示法都是使用补码表示) 表示形式的 1 位的数量。

# 5️⃣位运算完整代码

BitOperationDemo.java本示例使用JAVA 8
package top.rem.rain;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
 * @Author: LightRain
 * @Description: 位运算示例
 * @DateTime: 2023-12-10 16:15
 * @Version:1.0
 **/
public class BitOperationDemo {
  /**
   * 存储元素的数组
   */
  private int[] a;
  /**
   * 默认使用 int 类型
   */
  private static final int BIT_LENGTH = 32;
  /**
   * 整数部分
   */
  private static int P;
  /**
   * 余数
   */
  private static int S;
  /**
   * 2^5 - 1
   */
  private static final int MASK = 0x1F;
  /**
   * 2^n SHIFT=n=5 表示 2^5=32 即 bit 位长度 32
   */
  private static final int SHIFT = 5;
  private final int count;
  /**
   * 初始化位向量
   *
   * @param count
   */
  public BitOperationDemo(int count) {
    this.count = count;
    a = new int[(count - 1) / BIT_LENGTH + 1];
    init();
  }
  /**
   * 将数组中元素 bit 位设置为 0
   */
  public void init() {
    for (int i = 0; i < count; i++) {
      clear(i);
    }
  }
  /**
   * 置位和添加操作
   *
   * @param i
   */
  public void set(int i) {
    // 1. 结果等同  P = i / BIT_LENGTH; 取整数
    P = i >> SHIFT;
    // 2. 结果等同  S = i % BIT_LENGTH; 取余数
    S = i & MASK;
    // 3. 赋值设置该元素 bit 位为 1
    a[P] |= 1 << S;
    // 4. 将 int 型变量 j 的第 k 个比特位设置为 1, 即 j=j|(1<<k), 上述 3 句合并为一句
    //a[i >> SHIFT ] |= (1 << (i & MASK));
  }
  /**
   * 置 0 操作,相当于清除元素
   *
   * @param i
   */
  public void clear(int i) {
    // 1. 计算位于数组中第?个元素 P = i / BIT_LENGTH;
    P = i >> SHIFT;
    // 2. 计算余数 S = i % BIT_LENGTH;
    S = i & MASK;
    // 3. 把 a [P] 元素的第 S+1 个 (从低位到高位) bit 位设置为 0
    a[P] &= ~(1 << S);
    // 4. 将 int 型变量 j 的第 k 个比特位设置为 0,即 j= j&~(1<<k)
    //a[i>>SHIFT] &= ~(1<<(i &MASK));
  }
  /**
   * 读取操作,返回 1 代表该 bit 位有值,返回 0 代表该 bit 位没值
   *
   * @param i
   * @return int
   */
  public int get(int i) {
    //a[i>>SHIFT] & (1<<(i&MASK));
    P = i >> SHIFT;
    S = i & MASK;
    return Integer.bitCount(a[P] & (1 << S));
  }
  /**
   * 获取排序后的数组
   * @return
   */
  public List<Integer> getSortedArray(){
    List<Integer> sortedArray = new ArrayList<Integer>();
    for (int i = 0; i < count; i++) {
      // 判断 i 是否存在
      if (get(i) == 1) {
        sortedArray.add(i);
      }
    }
    return sortedArray;
  }
  public static void main(String[] args) {
    int count = 25;
    List<Integer> randoms = getRandomsList(count);
    System.out.println("排序前:");
    BitOperationDemo bitOperation = new BitOperationDemo(count);
    for (Integer e : randoms) {
      System.out.print(e+",");
      bitOperation.set(e);
    }
    System.out.println();
    System.out.println();
    System.out.println("------------------------------------------------");
    List<Integer> sortedArray = bitOperation.getSortedArray();
    System.out.println();
    System.out.println("排序后:");
    for (Integer e : sortedArray) {
      System.out.print(e+",");
    }
        /*
          执行结果:
            排序前:
            24,12,14,8,5,9,17,11,10,22,23,7,21,19,13,15,6,1,20,18,16,2,3,4,
            
            ------------------------------------------------
            
            排序后:
            1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,
         */
  }
  private static List<Integer> getRandomsList(int count) {
    Random random = new Random();
    List<Integer> randomsList = new ArrayList<Integer>();
    while(randomsList.size() < (count - 1)){
      // element ∈  [1,count)
      int element = random.nextInt(count - 1) + 1;
      if (!randomsList.contains(element)) {
        randomsList.add(element);
      }
    }
    return randomsList;
  }
}

# EnumSet 原理解析

有了前面 位向量 的分析,对于了解 EnumSet 的实现原理就会简单一些了, EnumSet 内部使用 位向量 来实现的,在前面说过 EnumSet 只是一个抽象类,事实上它存在两个子类, RegularEnumSetJumboEnumSetRegularEnumSet 使用一个 long 类型的变量作为 位向量long 类型的位长度是 64 bit ,因此可以存储 64 个枚举实例的标志位,一般情况下够用的了,而 JumboEnumSet 使用一个 long 类型的数组,当枚举个数超过 64 时,就会采用 long 数组的方式存储。

# 1️⃣ EnumSet 内部数据结构

EnumSet 内部数据结构源码如下:

EnumSet.java版本请选择JAVA 8
package java.util;
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E> implements Cloneable, java.io.Serializable {
  /**
   * 表示枚举类型
   * The class of all the elements of this set. -> 此集合的所有元素的类
   */
  final transient Class<E> elementType;
  /**
   * 存储该类型信息所表示的所有可能的枚举实例
   * All of the values comprising E.  (Cached for performance.) -> 包含 E 的所有值。(缓存以提高性能。)
   */
  final transient Enum<?>[] universe;
}

EnumSet 中有两个变量:一个是 elementType 用于表示枚举类型信息,另一个是 universe 是数组类型用于存储该类型信息所表示的所有可能的枚举实例, EnumSet 是抽象类,因此具体实现由子类来完成。

# 2️⃣ noneOf () 静态方法

noneOf(Class<E> elementType) 静态构造方法源码如下:

EnumSet.java版本请选择JAVA 8
package java.util;
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E> implements Cloneable, java.io.Serializable {
    
  // 根据 EnumMap 中的一样,获取可能的枚举实例
  public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
    Enum<?>[] universe = getUniverse(elementType);
    if (universe == null)
      throw new ClassCastException(elementType + " not an enum");
    if (universe.length <= 64)
      // 枚举个数小于 64 则创建 RegularEunmSet
      return new RegularEnumSet<>(elementType, universe);
    else
      // 否则创建 JumboEnumSet
      return new JumboEnumSet<>(elementType, universe);
  }
}

从源码可以看出如果枚举值个数小于等于 64 ,则静态工厂方法中创建的就是 RegularEnumSet ,否则大于 64 的话就创建 JumboEnumSet 。无论是 RegularEnumSet 还是 JumboEnumSet ,其构造函数内部都间接调用了 EnumSet 的构造函数,因此最终的 elementTypeuniverse 都传递给了父类 EnumSet 的内部变量。

# 3️⃣ RegularEnumSet&JumboEnumSet

RegularEnumSetJumboEnumSet 构造方法源码如下:

RegularEnumSet.java版本请选择JAVA 8
package java.util;
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
  // RegularEnumSet 构造
  RegularEnumSet(Class<E>elementType, Enum<?>[] universe) {
    super(elementType, universe);
  }
    
}
JumboEnumSet.java版本请选择JAVA 8
package java.util;
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {
  // JumboEnumSet 构造
  JumboEnumSet(Class<E>elementType, Enum<?>[] universe) {
    super(elementType, universe);
    elements = new long[(universe.length + 63) >>> 6];
  }
    
}

RegularEnumSetJumboEnumSet 类中都存在一个 elements 变量,用于记录 位向量 的操作。

RegularEnumSet.java版本请选择JAVA 8
package java.util;
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
  @java.io.Serial
  private static final long serialVersionUID = 3411599620347842686L;
  
  // 通过 long 类型的 elements 记录位向量的操作
  private long elements = 0L;
    
}
JumboEnumSet.java版本请选择JAVA 8
package java.util;
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {
  @java.io.Serial
  private static final long serialVersionUID = 334349849919042784L;
  // 通过 long 数组类型的 elements 记录位向量
  private long elements[];
  // 表示集合大小
  private int size = 0;
    
}

RegularEnumSetelements 是一个 long 类型的变量,共有 64bit 位,因此可以记录 64 个枚举常量,当枚举常量的数量超过 64 个时,将使用 JumboEnumSetelements 在该类中一个 long 型的数组,每个数组元素都可以存储 64 个枚举常量,这个过程其实与前面 位向量 的分析是同样的道理,只不过前面使用的是 32 位的 int 类型,这里使用的是 64 位的 long 类型罢了。

# 4️⃣ RegularEnumSet - Add () 方法

下面来看 EnumSet 是如何添加数据的 RegularEnumSet 中的 add 源码如下:

RegularEnumSet.java版本请选择JAVA 8
package java.util;
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
  public boolean add(E e) {
    // 检测是否为枚举类型
    typeCheck(e);
    // 记录旧 elements
    long oldElements = elements;
    // 执行位向量操作
    // 数组版:a [i>> SHIFT ] |= (1 << (i & MASK))
    elements |= (1L << ((Enum<?>)e).ordinal());
    return elements != oldElements;
  }
    
}

关于 elements |= (1L << ((Enum<?>)e).ordinal()); 这句跟我们所分析的 位向量 操作是相同的原理,只不过前面分析的是数组类型实现,这里用的 long 类型单一变量实现, ((Enum)e).ordinal() 通过该语句获取要添加的枚举实例的序号,然后通过 1 左移再与 long 类型的 elements 进行或操作,就可以把对应位置上的 bit 设置为 1 了,也就代表该枚举实例的存在。
详细过程请看下图:注意 universe 数组在 EnumSet 创建时就初始化并填充了所有可能的枚举实例,而 elements 值的第 nbit 位为 1 时代表枚举存在,而获取的则是从 universe 数组中的第 n 个元素值。

pifZexK.jpg

以上就是枚举实例的添加过程和获取原理,而对于 JumboEnumSetadd 接着看下面。

# 5️⃣ JumboEnumSet - Add () 方法

接着来看 JumboEnumSetadd 源码实现如下:

JumboEnumSet.java版本请选择JAVA 8
package java.util;
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {
  public boolean add(E e) {
    typeCheck(e);
    // 计算 ordinal 值
    int eOrdinal = e.ordinal();
    int eWordNum = eOrdinal >>> 6;
    long oldElements = elements[eWordNum];
    // 与前面分析的位向量相同:a [i>> SHIFT ] |= (1 << (i & MASK))
    elements[eWordNum] |= (1L << eOrdinal);
    boolean result = (elements[eWordNum] != oldElements);
    if (result)
      size++;
    return result;
  }
    
}

关于 JumboEnumSetadd 方法的实现与 RegularEuumSet 区别是一个 long 数组类型,一个 long 变量,运算原理相同,数组的 位向量 运算与前面的分析是相同的,。

# 6️⃣ RegularEnumSet&JumboEnumSet - remove () 方法

RegularEnumSetJumboEnumSetremove() 源码实现如下:

RegularEnumSet.java版本请选择JAVA 8
package java.util;
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
  public boolean remove(Object e) {
    if (e == null)
      return false;
    Class<?> eClass = e.getClass();
    if (eClass != elementType && eClass.getSuperclass() != elementType)
      return false;
    long oldElements = elements;
    // 将 int 类型变量 j 的第 k 个比特位设置为 0,即:j = j&~(1<<k)
    // 数组类型:a [i>>SHIFT] &= ~(1<<(i & MASK));
    elements &= ~(1L << ((Enum<?>)e).ordinal());
    return elements != oldElements;
  }
}
JumboEnumSet.java版本请选择JAVA 8
package java.util;
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {
  public boolean remove(Object e) {
    if (e == null)
      return false;
    Class<?> eClass = e.getClass();
    if (eClass != elementType && eClass.getSuperclass() != elementType)
      return false;
    int eOrdinal = ((Enum<?>)e).ordinal();
    int eWordNum = eOrdinal >>> 6;
    long oldElements = elements[eWordNum];
    // 与 a [i>>SHIFT] &= ~(1<<(i &MASK)); 相同
    elements[eWordNum] &= ~(1L << eOrdinal);
    boolean result = (elements[eWordNum] != oldElements);
    if (result)
      size--;
    return result;
  }
    
}

remove() 方法的实现,跟 位向量 清空操作是同样的实现原理,请看下图:

pifiaqA.png

至于 JumboEnumSet 的实现原理也是类似的,下面为了简洁起见,我们以 RegularEnumSet 类的实现作为源码分析,毕竟 JumboEnumSet 的内部实现原理可以说跟前面分析过的 位向量 几乎一样。

# 7️⃣ contains ()&containsAll () 方法

接着来看如何判读是否包含某个元素,源码如下:

RegularEnumSet.java版本请选择JAVA 8
package java.util;
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
  public boolean contains(Object e) {
    if (e == null)
      return false;
    Class<?> eClass = e.getClass();
    if (eClass != elementType && eClass.getSuperclass() != elementType)
      return false;
    // 先左移再进行 & amp; 操作
    return (elements & (1L << ((Enum<?>)e).ordinal())) != 0;
  }
  public boolean containsAll(Collection<?> c) {
    if (!(c instanceof RegularEnumSet<?> es))
      return super.containsAll(c);
    if (es.elementType != elementType)
      return es.isEmpty();
    // ~elements 取反相当于 elements 补集,再与 es.elements 进行 & amp; 操作,如果为 0 就说明 elements 补集与 es.elements 没有交集,也就是 es.elements 是 elements 的子集
    return (es.elements & ~elements) == 0;
  }
    
}

对于 contains(Object e) 方法,先左移再按位进行 & 操作,不为 0 则表示包含该元素,跟 位向量get 操作实现原理差不多。对于 containsAll(Collection<?> c) 则可能比较难懂,这里分析一下 elementslong 类型变量标记 EnumSet 集合中已存在元素的 bit 位,如果 bit 位为 1 则说明存在枚举实例,为 0 则不存在,现在执行 ~elements 操作后,则说明 ~elementselements 的补集,那么只要传递进来的 es.elements 与补集 ~elements 执行 & 操作为 0 ,就可以证明 es.elements 与补集 ~elements 没有交集的可能,也就说 es.elements 只能是 elements 的子集,这样就可以判断出当前 EnumSet 集合中包含传递进来的集合 c 了,详细请看下图:

pifAjds.png

上图中, elements 代表 A , es.elements 代表 B, ~elements 就是求 A 的补集, (es.elements & ~elements) == 0 就是在验证 A∩B 是不是空集,即 B 是否为 A 的子集。

# 8️⃣ retainAll () 方法

接着看 retainAll() 方法,求两个集合的交集,源码如下:

RegularEnumSet.java版本请选择JAVA 8
package java.util;
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
  public boolean retainAll(Collection<?> c) {
    if (!(c instanceof RegularEnumSet<?> es))
      return super.retainAll(c);
    if (es.elementType != elementType) {
      boolean changed = (elements != 0);
      elements = 0;
      return changed;
    }
    long oldElements = elements;
    // 执行 & 操作,求交集比较简单
    elements &= es.elements;
    return elements != oldElements;
  }
    
}

# 9️⃣ iterator () 方法取值

RegularEnumSet.java版本请选择JAVA 8
package java.util;
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
  public Iterator<E> iterator() {
    return new EnumSetIterator<>();
  }
  private class EnumSetIterator<E extends Enum<E>> implements Iterator<E> {
      
    // 记录 elements
    long unseen;
    // 记录最后一个返回值
    long lastReturned = 0;
    EnumSetIterator() {
      unseen = elements;
    }
    public boolean hasNext() {
      return unseen != 0;
    }
    @SuppressWarnings("unchecked")
    public E next() {
      if (unseen == 0)
        throw new NoSuchElementException();
      // 取值过程:先与本身负执行 & amp; 操作得出的就是二进制低位开始的第一个 1 的数值大小哦
      lastReturned = unseen & -unseen;
      // 取值后减去已取得 lastReturned
      unseen -= lastReturned;
      // 返回在指定 long 值的二进制补码表示形式中最低位 (最右边) 的 1 位之后的零位的数量
      return (E) universe[Long.numberOfTrailingZeros(lastReturned)];
    }
    public void remove() {
      if (lastReturned == 0)
        throw new IllegalStateException();
      elements &= ~lastReturned;
      lastReturned = 0;
    }
  }
    
}

比较难懂的应该是下面这些代码:

// 取值过程,先与本身负执行 & amp; 操作得出的就是二进制低位开始的第一个 1 的数值大小
lastReturned = unseen & -unseen; 
// 取值后减去已取得 lastReturned
unseen -= lastReturned;
// 返回在指定 long 值的二进制补码表示形式中最低位 (最右边) 的 1 位之后的零位的数量
return (E) universe[Long.numberOfTrailingZeros(lastReturned)];

我们通过原理图来帮助理解,现在假设集合中已保存所有可能的枚举实例变量,我们需要把它们遍历出来,下面的第一个枚举元素的获取过程,显然通过 unseen & -unseen 操作,我们可以获取到二进制低位开始的第一个 1 的数值,该计算的结果是要么全部都是 0 要么就只有一个 1 ,然后赋值给 lastReturned 通过 Long.numberOfTrailingZeros(lastReturned) 获取到该 bit164 位的 long 类型中的位置,即:从低位算起的第几个 bit
详细看下图:该 bit 的位置恰好是低位的第 1bit 位置,也就指明了 universe 数组的第一个元素就是要获取的枚举变量。执行 unseen -= lastReturned 后继续进行第 2 个元素的遍历,以此类推遍历出所有值,这就是 EnumSet 的取值过程,真正存储枚举变量的是 universe 数组,而通过 long 类型变量的 bit 位的 01 表示存储该枚举变量在 universe 数组的那个位置,这样做的好处是任何操作都是执行 long 类型变量的 bit 位操作,这样执行效率将会特别高,毕竟是二进制直接执行,只有最终获取值时才会操作到数组 universe

pifmjPA.png

以上这些就是关于 EnumSet 的实现原理的主要部分,其内部使用 位向量 来执行,存储结构简洁,节省空间,而大部分操作都是按位运算来执行,直接操作二进制数据效率极高,这就是本期的枚举全部内容。