Java枚举全解

1 篇文章 0 订阅
1 篇文章 0 订阅


1.枚举的概述


什么是枚举

枚举(enum),是指一个经过排序的、被打包成一个单一实体的项列表。一个枚举的实例可以使用枚举项列表中任意单一项的值。枚举在各个语言当中都有着广泛的应用,通常用来表示诸如颜色、方式、类别、状态等等数目有限、形式离散、表达又极为明确的量。JDK5 开始,引入了对枚举的支持。

Java 中的枚举是一种类型,顾名思义:就是一个一个列举出来。所以它一般都是表示一个有限的集合类型,它是一种类型,在维基百科中给出的定义是:

数学计算机科学 理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠.。枚举是一个被命名的整型常数的集合,枚举在日常生活中很常见,例如表示星期的 SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY 就是一个枚举。


枚举的优点

  • 代码较优雅,功能强大:枚举类型都继承了Enum类,且还经过编译器增强,因此枚举类可以写的很简洁优雅同时拥有很多功能。

  • 保证参数安全:枚举类中的常量都是 public static final 修饰的,因此它能保证参数的安全性,如方法声明传入的参数,必须是指定枚举中的常量,这样既能保证不会传入非法参数,且参数也不能被修改。就比如使用 int、String 类型 switch 时,当出现参数不确定的情况,偶尔会出现越界的现象,这样我们就需要做容错操作(if条件筛选等),使用枚举,编译期间限定类型,不允许发生越界。

  • 线程安全:枚举类型的实例化不是由程序员自己完成的,而是由虚拟机来做的,在枚举被加载到虚拟机之后就会对其进行实例化,这一步程序员是无感知的,通过可以看上面它反编译之后的代码,类的内部几乎全部都是static,这也就是说,当真正第一次使用到的时候,虚拟机会对其进行初始化、类加载操作,这个过程是线程安全的,这是 Java 虚拟机明确规定的,所以说这一步的线程安全有虚拟机来保障是绝对可靠的。

  • 枚举型可直接与数据库交互

  • 适用于单例:使用枚举单例的写法,我们完全不用考虑序列化和反射的问题。枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在 JVM 中都是唯一的,在枚举类型的序列化和反序列化上,Java 做了特殊的规定:在序列化时 Java 仅仅是将枚举对象的 name 属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf 方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的并禁用了 writeObject、readObject、readObjectNoData、writeReplace 和 readResolve等方法,从而保证了枚举实例的唯一性(也说明了只有Java中只有编译器能创建枚举实例)。

    如何确保反序列化时不会破坏单例:根据 valueOf(name) 得到反序列化后对象,valueOf 根据枚举常量名获取对应枚举常量

    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                  String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }
    
    
    Map<String, T> enumConstantDirectory() {
            if (enumConstantDirectory == null) {
                //getEnumConstantsShared最终通过反射调用枚举类的values方法
                T[] universe = getEnumConstantsShared();
                if (universe == null)
                    throw new IllegalArgumentException(
                        getName() + " is not an enum type");
                Map<String, T> m = new HashMap<>(2 * universe.length);
                //map存放了当前enum类的所有枚举实例变量,以name为key值
                for (T constant : universe)
                    m.put(((Enum<?>)constant).name(), constant);
                enumConstantDirectory = m;
            }
            return enumConstantDirectory;
        }
        private volatile transient Map<String, T> enumConstantDirectory = null;
    }
    

    如何确保反射不会破坏单例:反射源码里对于枚举类型反射直接抛异常所以反射生成不了枚举类型实例

    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        //获取枚举类的构造函数
        Constructor<SingletonEnum> constructor = 
            SingletonEnum.class.getDeclaredConstructor(String.class, int.class);
        //强制设置为可访问
        constructor.setAccessible(true);
        //通过反射创建枚举实例
        SingletonEnum singleton = constructor.newInstance("otherInstance",9);
    }
    
    //抛出的异常
    Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
        at zejian.SingletonEnum.main(SingletonEnum.java:38)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
    
    
    
    //通过反射调用的 newInstance 方法
    public T newInstance(Object ... initargs) throws InstantiationException,
    	IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        
         if (!override) {
             if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                 Class<?> caller = Reflection.getCallerClass();
                 checkAccess(caller, clazz, null, modifiers);
             }
         }
         //判断是否是枚举类,如果是就抛异常
         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(initargs);
         return inst;
    }
    

枚举的特点

  1. 枚举 enum 是一个特殊的 Java 类,它继承自 java.lang.Enum(抽象类),继承了非常多 Enum 的方法。
  2. 枚举类能够定义在 interface 或 class 中。枚举类中可以有构造函数、方法、数据域。
  3. 枚举类的构造器默认且必须是private,仅仅是在构造枚举值的时候被调用(这样能够保证外部代码无法新构建枚举类的实例,这通常在构建单例时很有用)。
  4. 枚举类可以实现接口,但不能继承类(因为底层默认隐式地继承了 java.lang.Enum 抽象类),但枚举类本身不能被继承(可以看成 final 类)。

我们在使用的时候没有必要过分在意它是 enum 还是 class,大多的使用方式都跟普通的 Java 类没有什么区别,唯独就是在写一些枚举常量的时候,写法有些迥异,但是除此之外,其他方面几乎完全一样:一样可以实现接口,一样需要实现接口中的方法,一样可以重写父类方法(如:toString),甚至一样可以有构造方法。所以仅仅就使用来说,跟传统开发上几乎没有什么区别,但是就底层实现和用途而言,还是有很大区别的。



2.枚举类的定义

package enums;

/**
 * 枚举,enum关键字声明,类名最好以 "Enum" 且能够清晰表名用途。
 * 枚举类默认并隐式继承了 java.lang.Enum抽象类,
 * 枚举类经过编译之后被final修饰,即不可被继承。
 */
public enum RoleEnum {

    /**
     * enum constants 必须声明在开始的位置(任何其他成员的前面),
     * 多个 enum constants 用 ‘,’ 隔开,最后一个以 ‘;’ 结束。
     * 如果没有无参构造,则必须调用有参构造,并传入参数,
     * 如果有抽象方法,则必须实现抽象方法。
     * 不能给enum constants添加修饰,编译器隐式给定修饰符为 public static final。
     */
    ADMIN("Administrator") {
        @Override
        void introduce() {
            System.out.println("I'm a Administrator");
        }
    },  // enum constants 之间以逗号隔开。
    USER("User") {
        @Override
        void introduce() {
            System.out.println("I'm a User");
        }
    };

    //fields
    private String position;

    /**
     * 构造函数,默认且必须是private的。
     */
    RoleEnum(String position) {
        this.position = position;
    }

    // abstract method is allowed.
    abstract void introduce();

    
    // common method getter and setter.
    
    public String getPosition() {
        return position;
    }

    public void setPosition(String position) {
        this.position = position;
    }

    public static void main(String[] args) {
        Class<RoleEnum> declaringClass = RoleEnum.ADMIN.getDeclaringClass();
        RoleEnum admin = Enum.valueOf(RoleEnum.class, "ADMIN");
        RoleEnum user = RoleEnum.valueOf("USER");
        System.out.println(declaringClass);  //class enums.RoleEnum
        System.out.println(admin.position);  //Administrator
        System.out.println(user.position);  //User
        System.out.println(admin.equals(user)); //false
        System.out.println(admin.compareTo(user)); //-1
        System.out.println(admin.name() + "_" + admin.toString() + "_" 
                           + admin.ordinal());//ADMIN_ADMIN_0
        System.out.println(user.name() + "_" + user.toString() + "_" 
                           + user.ordinal());//USER_USER_1
    }
}



3.java.lang.Enum


Enum简介

枚举类隐式继承了 java.lang.Enum 抽象类,继承了 java.lang.Enum 的特性,又因为 java.lang.Enum 虽然是一个抽象类,但是却没有抽象方法,因此,枚举类不需要实现任何方法,之所以把 java.lang.Enum 设计成抽象类是因为设计者不想让开发者直接创建 java.lang.Enum 的实例,而是应该使用枚举类。


Enum源码

java.lang.Enum 的官方文档:

/**
 * 这是Java语言所有枚举类型的公共基类。
 * 
 * 更多关于枚举的信息,包括由编译器合成的隐式声明方法的描述请参考
 * <cite>The Java&trade; Language Specificatio</cite>.的第8.9节
 * 
 * 请注意,当使用枚举类型作为set的类型或map中的键的类型时,可以使用专门和高效的
 * {java.util.EnumSet set} 和 {java.util.EnumMap map}的实现。
 * 
 * @param <E> The enum type subclass
 * @see     Class#getEnumConstants() 如果一个类是枚举类,则返回它所有的元素。
 * @see     java.util.EnumSet
 * @see     java.util.EnumMap
 * @since   1.5
 */
public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
    /**
     * The name of this enum constant, as declared in the enum declaration.
     * 大多数程序猿应该使用toString方法而不是访问该字段。
     */
    private final String name;

    /**
     * Returns the name of this enum constant, exactly as declared in its
     * enum declaration.
     * 大多数程序员应该优先使用{toString}方法,因为toString方法可能会返回一个更加 
     * user-friendly name.
     * 这个方法主要用于 '正确性取决于获得确切的名称' 这一特定场景下,不会因为版本而异
     * 
     * @return the name of this enum constant
     */
    public final String name() {
        return name;
    }

    /**
     * The ordinal of this enumeration constant (在枚举中声明的位置,从0开始计)
     * 大多数程序员都用不到这个域,它设计用于基于枚举的复杂数据结构,比如 EnumSet 和 EnumMap.
     */
    private final int ordinal;

    /**
     * Returns the ordinal of this enumeration constant (在枚举中声明的位置,从0开始计).
     * 大多数程序员都用不到这个域,它设计用于基于枚举的复杂数据结构,比如 EnumSet 和 EnumMap.
     *
     * @return the ordinal of this enumeration constant
     */
    public final int ordinal() {
        return ordinal;
    }

    /**
     * 唯一的构造器,开发者不能自行调用此构造器.这是供通过编译器响应枚举类型声明的code emitted使用的。
     * It is for use by code emitted by the compiler in response to
     * enum type declarations.
     *
     * @param name - The name of this enum constant,用来声明它的标识符。
     * @param ordinal - The ordinal of this enumeration constant 
     *					(在枚举中声明的位置,从0开始计).
     */
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

    /**
     * 
     * Returns the name of this enum constant, as contained in the declaration.
     * 这个方法可能会被重写,尽管它通常不是必须和理想的。
     * 当存在更 "programmer-friendly" 的字符串形式时,enum类型应该覆盖此方法。
     *
     * @return the name of this enum constant
     */
    public String toString() {
        return name;
    }

    /**
     * Returns true if the specified object is equal to this
     * enum constant.
     *
     * @param other 要与此对象进行相等性比较的对象。
     * @return  true if the specified object is equal to this
     *          enum constant.
     */
    public final boolean equals(Object other) {
        return this==other;
    }

    /**
     * Returns a hash code for this enum constant.
     *
     * @return a hash code for this enum constant.
     */
    public final int hashCode() {
        return super.hashCode();
    }

    /**
     * 抛出 CloneNotSupportedException. 
     * 保证了枚举不会被克隆,这对于保持他们的"singleton"(单例)状态是必要的。
     *
     * @return (never returns)
     */
    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

    /**
     * 比较此枚举与指定对象的顺序。
     * 当此对象小于,等于或大于指定的对象时,返回负整数,零或正整数。
     * 枚举常量只能与相同枚举类型的其他枚举常量比较.  此方法使用的自然顺序是声明常量的顺序。
     */
    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();
        return self.ordinal - other.ordinal;
    }

    /**
     * 返回与此枚举常量的枚举类型相对应的Class对象。  
     * 当且仅当 e1.getDeclaringClass() == e2.getDeclaringClass() 时,两个枚举常量 e1 和 e2 
     * 属于相同的枚举类型。(对于具有constant-specific类主体的enum constants, 此方法返回的值可能
     * 不同于{Object#getClass})
     *
     * @return the Class object corresponding to this enum constant's
     *     enum type
     */
    @SuppressWarnings("unchecked")
    public final Class<E> getDeclaringClass() {
        Class<?> clazz = getClass();
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }

    /**
     * 返回具有指定名称和指定枚举类型的枚举常量。该名称必须与用于声明此类型的枚举常量的标识符完全匹配。
     * (不允许使用多余的空格字符)。
     *
     * 注意: 对于一个特定的枚举类型 T,可以使用对该枚举隐式声明的
     * { public static TvalueOf(String) }方法来代替此方法,以从名称映射到相应的枚举常量。
     * 可以通过调用该类型的隐式{ public static T [] values()}方法来获取枚举类型的所有常量。
     *
     * @param <T> The enum type whose constant is to be returned
     * @param enumType the {@code Class} object of the enum type from which
     *      to return a constant
     * @param name the name of the constant to return
     * @return the enum constant of the specified enum type with the
     *      specified name
     * @throws IllegalArgumentException if the specified enum type has
     *         no constant with the specified name, or the specified
     *         class object does not represent an enum type
     * @throws NullPointerException if {@code enumType} or {@code name}
     *         is null
     * @since 1.5
     */
    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        /**
         * enumConstantDirectory 是 class对象内部的方法,如果调用者是枚举类型,则
         * 返回一个包含此枚举类中 enum constants 映射的 map: 
         * 		key 就是name,value则是enum constant
         */
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }

    /**
     * 枚举类不能具有 finalize 方法.
     */
    protected final void finalize() { }

    /**
     * 防止默认的反序列化。
     */
    private void readObject(ObjectInputStream in) throws IOException,
        ClassNotFoundException {
        throw new InvalidObjectException("can't deserialize enum");
    }

    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("can't deserialize enum");
    }
}

常用方法

返回类型方法名称方法说明
intcompareTo(E o)比较此枚举与指定对象的顺序
booleanequals(Object other)当指定对象等于此枚举常量时,返回 true。
ClassgetDeclaringClass()返回与此枚举常量的枚举类型相对应的 Class 对象
Stringname()返回此枚举常量的名称,在其枚举声明中对其进行声明
intordinal()返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零)
StringtoString()返回枚举常量的名称,它包含在声明中
static <T extends Enum<T>> Tstatic <T extends Enum<T>> T valueOf(Class<T> enumType, String name)返回带指定名称的指定枚举类型的枚举常量。



4.枚举原理分析


构造原理

我们先看一个简洁的枚举类:

public enum RoleEnum {
    ADMIN, USER
}

我们为 RoleEnums 创建了两个 enum constants,分别是 ADMIN 和 USER。这段代码实际上调用了 2 次父类Enum 的构造方法Enum(String name, int ordinal) ,也就是:

new Enum<RoleEnum>("ADMIN", 0);
new Enum<RoleEnum>("USER", 1);

我们来遍历输出一下枚举:

for (RoleEnum r : RoleEnum.values()) {
    System.out.println(r);
}
//输出
//ADMIN
//USER

反编译枚举Class

//定义枚举类型
enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

//Day.class反编译后美化的样子
//注意,类被 final 修饰了
public final class Day extends java.lang.Enum<Day> {
    
    //枚举常量
    public static final Day MONDAY;
    public static final Day TUESDAY;
    public static final Day WEDNESDAY;
    public static final Day THURSDAY;
    public static final Day FRIDAY;
    public static final Day SATURDAY;
    public static final Day SUNDAY;
    private static final Day $VALUES[];
    
    static {    
        //实例化枚举常量
        MONDAY = new Day("MONDAY", 0);
        TUESDAY = new Day("TUESDAY", 1);
        WEDNESDAY = new Day("WEDNESDAY", 2);
        THURSDAY = new Day("THURSDAY", 3);
        FRIDAY = new Day("FRIDAY", 4);
        SATURDAY = new Day("SATURDAY", 5);
        SUNDAY = new Day("SUNDAY", 6);
        $VALUES = (new Day[] {
            MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
        });
    }
    
    //编译器添加的静态的values()方法
    public static Day[] values() {
        return (Day[])$VALUES.clone();
    }
    //编译器添加的静态的valueOf()方法
    public static Day valueOf(String s) {
        return (Day)Enum.valueOf(Day.class, s);
    }
    //私有构造函数
    private Day(String s, int i) {
        super(s, i);
    }
    
}

如上,一个枚举类除了显式声明的 enum constants 之外,编译器还会隐式地增加一个private static final Type $VALUES[] ,其中包含了所有显式声明的 enum constants,由于$VALUES[] 被声明为 private,我们只能通过 public static Type[] values() 方法获取。


反编译枚举常量Class

值得注意的是,如果一个枚举类中含有抽象方法,除了对应枚举类的 class 文件之外,编译器还会为其中的每一个枚举常量生成一个 class 文件,文件名以枚举类名开头加上 “$” 符号 和 此枚举常量生成的顺序(从 1 开始计数),因此如果一个枚举类中含有很多个枚举常量,并不建议该枚举类声明抽象方法。

例如:

package enums;

public enum RoleEnum {

    ADMIN("Administrator") {
        @Override
        void introduce() {
            System.out.println("I'm a Administrator");
        }
    },
    USER("User") {
        @Override
        void introduce() {
            System.out.println("I'm a User");
        }
    };

    private String position;


    RoleEnum(String position) {
        this.position = position;
    }

    abstract void introduce();

    public String getPosition() {
        return position;
    }

    public void setPosition(String position) {
        this.position = position;
    }
}

编译之后会产生三个 class 文件:
编译后的class文件01

其中对应关系如下:

  • 枚举类 RoleEnum 对应 RoleEnum.class
  • 枚举常量 ADMIN 对应 RoleEnum$1.class
  • 枚举常量 USER 对应 RoleEnum$2.class

我们对 RoleEnum$1.class 进行反编译:javap -c RoleEnum$1.class

Compiled from "RoleEnum.java"
final class enums.RoleEnum$1 extends enums.RoleEnum {
  enums.RoleEnum$1(java.lang.String, int, java.lang.String);
    Code:
       0: aload_0
       1: aload_1
       2: iload_2
       3: aload_3
       4: aconst_null
       5: invokespecial #1                  // Method enums/RoleEnum."<init>":(Ljava/lang/String;ILjava/lang/String;Lenums/RoleEnum$1;)V
       8: return

  void introduce();
    Code:
       0: getstatic     #2     // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3     // String I'm a Administrator
       5: invokevirtual #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

我们可以看到 RoleEnum$1 继承了 RoleEnum枚举类(虽然 RoleEnum 是 final 的,不能被继承,但是仅限于不能人为继承,编译器却可以做到)。

通常是一个枚举常量对应一个 class 文件,如果 switch 使用了枚举则会多出一个 class 文件。

比如我们修改一下上面的代码,重写 toString 方法,用 switch 实现:

//in RoleEnum
@Override
public String toString() {
    switch (this) {
        case ADMIN:
            System.out.println("ADMIN: " + this.getPosition());
            break;
        case USER:
            System.out.println("USER: " + this.getPosition());
    }
    return super.toString();
}

编译后我们会发现多了一个 RoleEnum$3.class :
编译后的class文件02

我们反编译一下:

Compiled from "RoleEnum.java"
class enums.RoleEnum$3 {
  static final int[] $SwitchMap$enums$RoleEnum;

  static {};
    Code:
       0: invokestatic  #1         // Method enums/RoleEnum.values:()[Lenums/RoleEnum;
       3: arraylength
       4: newarray       int
       6: putstatic     #2         // Field $SwitchMap$enums$RoleEnum:[I
       9: getstatic     #2         // Field $SwitchMap$enums$RoleEnum:[I
      12: getstatic     #3         // Field enums/RoleEnum.ADMIN:Lenums/RoleEnum;
      15: invokevirtual #4         // Method enums/RoleEnum.ordinal:()I
      18: iconst_1
      19: iastore
      20: goto          24
      23: astore_0
      24: getstatic     #2         // Field $SwitchMap$enums$RoleEnum:[I
      27: getstatic     #6         // Field enums/RoleEnum.USER:Lenums/RoleEnum;
      30: invokevirtual #4         // Method enums/RoleEnum.ordinal:()I
      33: iconst_2
      34: iastore
      35: goto          39
      38: astore_0
      39: return
    Exception table:
       from    to  target type
           9    20    23   Class java/lang/NoSuchFieldError
          24    35    38   Class java/lang/NoSuchFieldError
}

可以看出 RoleEnum$3 是一个名为 SwitchMap 的映射,它是一个保存开关所使用的枚举字面索引引用的映射。



5.用例


表示离散常量

在枚举出现之前,如果想要表示一组特定的离散值,往往使用一些常量。例如:

class Entity {

    public static final int VIDEO = 1; //视频
    public static final int AUDIO = 2; //音频
    public static final int TEXT = 3; //文字
    public static final int IMAGE = 4; //图片

    private int id;
    private int type;

    // ommited getters and setters.

}

当然,常量也不仅仅局限于 int 型,诸如 char 和 String 等也是不在少数。然而,无论使用什么样的类型,这样做都有很多的坏处。这些常量(int、char 和 String 等类型)通常都是连续、有无穷多个值的量,而类似这种表示类别的量则是离散的,并且通常情况下只有有限个值。用连续的量去表示离散量,会产生很多问题。例如,针对上述的 Entity 类,如果要对 Entity 对象的 type 属性进行赋值,一般会采用如下方法:

Entity e = new Entity();
e.setId(10);
e.setType(2);

这样做的缺点有:

  1. 代码可读性差、易用性低:由于 setType() 方法的参数是 int 型的,在阅读代码的时候往往会让读者感到一头雾水,根本不明白这个 2 到底是什么意思,代表的是什么类型。当然,要保证可读性,还有这样一个办法:

    e.setType(Entity.AUDIO);
    

    而这样的话,问题又来了。这样做,客户端必须对这些常量去建立理解,才能了解如何去使用这个东西。说白了,在调用的时候,如果用户不到 Entity 类中去看看,还真不知道这个参数应该怎么传、怎么调。像是setType(2) 这种用法也是在所难免,因为它完全合法,不是每个人都能够建立起用常量名代替数值,从而增加程序可读性、降低耦合性的意识。

  2. 类型不安全:在用户去调用的时候,必须保证类型完全一致,同时取值范围也要正确。像是 setType(-1) 这样的调用是合法的,但它并不合理,今后会为程序带来种种问题。也许你会说,加一个有效性验证嘛,但是,这样做的话,又会引出下面的第 3 个问题。

  3. 耦合性高,扩展性差:假如,因为某些原因,需要修改 Entity 类中常量的值,那么,所有用到这些常量的代码也就都需要修改—当然,要仔细地修改,万一漏了一个,那可不是开玩笑的。同时,这样做也不利于扩展。例如,假如针对类别做了一个有效性验证,如果类别增加了或者有所变动,则有效性验证也需要做对应的修改,不利于后期维护。

而枚举可以很好地解决上述的问题,上面的 Entity 类就可以改成这样:

enum TypeEnum {
	VIDEO, AUDIO, TEXT, IMAGE
}

public class Entity {
   
    private int id;
    private TypeEnum type;

    //omitted getters and setters.
}

在为 Entity 对象赋值的时候,就可以这样:

Entity e = new Entity();
e.setId(10);
e.setType(TypeEnum.AUDIO);

怎么看,都是好了很多。在调用 setType() 时,可选值只有对应枚举类型的枚举常量,否则会出现编译错误,因此可以看出,枚举是类型安全的,不会出现取值范围错误的问题。同时,客户端不需要建立对枚举中常量值的了解,使用起来很方便,并且可以容易地对枚举进行修改,而无需修改客户端。如果常量从枚举中被删除了,那么客户端将会失败并且将会收到一个错误消息。枚举中的常量名称可以被打印,因此除了仅仅得到列表中项的序号外还可以获取更多信息。这也意味着常量可用作集合的名称,例如 HashMap。


配合Swith

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



6.EnumMap


EnumMap简介


Ⅰ-EnumMap类继承结构

EnumMap类继承结构

EnumMap 继承了 AbstractMap 接口,持有以 Enum 作为上边界的参数化类型的 Key


Ⅱ-EnumMap官方介绍
/**
 * 一个专门以enum类型作为key的{Map}实现。
 * enum map中的所有key必须来自一个单一的enum类型,该类型在创建map时被显式或隐式地指定。
 * 这种表示非常紧凑和有效。
 *
 * 
 * EnumMap 按其 key 的{自然顺序}进行维护(枚举常量的声明顺序), 这反映在集合视图
 * (keySet()}、{entrySet()}和values()})返回的迭代器中。
 *
 * collection views 返回的迭代器是 weakly consistent(弱一致性的): 
 * 它们将不会抛出{ConcurrentModificationException}异常,并且它们可能会显示也可能不会显示在
 * 进行迭代时对map进行的任何修改的效果。
 * 
 * 不允许 null key。试图插入一个 null key 将会抛出{NullPointerException}异常。
 * 但是,尝试测试是否存在空键或删除空键将正常工作。
 * 允许 null value。
 * 
 * 像大多数的集合实现一样,EnumMap也不是synchronized, 如果多个线程同时访问EnumMap,
 * 并且有至少一个线程修改此map, 应该在外部synchronized, 这通常是通过对一些自然封装EnumMap
 * 的对象进行同步来实现的, 如果没有这样的对象存在,则此map应该使用{Collections#synchronizedMap}
 * 方法进行包装。且最好是在创建时完成包装,以防止意料之外的unsynchronized访问:
 * Map<EnumKey, V> m = Collections.synchronizedMap(new EnumMap<EnumKey, V>(...))
 * 
 * 实现备注:所有基础操作的执行只需要常量时间。
 * 它们可能(虽然不能保证)比对应的{HashMap}更快。
 *
 * @see EnumSet
 * @since 1.5
 */
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable
{

EnumMap特点

  • 专门服务于枚举:EnumMap中的所有 key 必须是来自通过同一个枚举类型的enum constants,在构建EnumMap对象的时候就需要指定 Key 的枚举类型。

  • 不允许 null key,允许 null value:因为 key 是枚举常量,而枚举常量不可能为空。

  • 紧凑而高效:底层用数组实现,数组大小即为枚举类常量数量,枚举常量映射的 value 为数组vals[key.ordinal] 存储的值。


EnumMap底层实现


Ⅰ-记录keyType

keyType 记录了 key 对应的枚举类型的 Class 对象,此 filed 将被隐式或显式地被指定,在构造方法中被初始化。

/**
 * The <tt>Class</tt> object for the enum type of all the keys of this map.
 *
 * @serial
 */
private final Class<K> keyType;

Ⅱ-缓存key(枚举常量)

在 EnumMap 中有着么一个字段 private transient K[] keyUniverse; ,他在构造 EnumMap 的时候被初始化,通过Enum 的 Class对象获取其中所有的枚举常量,存入keyUniverse 数组中。

/**
 * 包含所有K的值.(缓存以提高性能.)
 */
private transient K[] keyUniverse;

Ⅲ-使用数组保存映射值
/**
 * 此 map 映射的数组表示.  第 i 个元素是当前映射到的 universe[i] 的值, 
 * 如果它没有映射到任何东西,则为空, 如果映射到null 则为NULL(一个引用名为NULL的Object对象).
 */
private transient Object[] vals;

Ⅳ-EnumMap的构造
/**
 * Creates an empty enum map with the specified key type.
 *
 * @param keyType 此EnumMap的key类型的Class对象。
 * @throws NullxPointerException if <tt>keyType</tt> is null
 */
public EnumMap(Class<K> keyType) {
    this.keyType = keyType;
    keyUniverse = getKeyUniverse(keyType);
    vals = new Object[keyUniverse.length];
}

/**
 * 使用指定的EnumMap创建一个新的EnumMap对象,使用同样的key类型,初始化并包含相同的映射(如果有)
 *
 * @param m the enum map from which to initialize this enum map
 * @throws NullPointerException if <tt>m</tt> is null
 */
public EnumMap(EnumMap<K, ? extends V> m) {
    keyType = m.keyType;
    keyUniverse = m.keyUniverse;
    vals = m.vals.clone();
    size = m.size;
}

/**
 * 创建一个EnumMap并根据指定的Map进行初始化,如果指定的Map也是一个EnumMap则执行效果和 
 * EnumMap(EnumMap<K, ? extends V> m) 构造方法一样,如果指定的map不是一个EnumMap
 * 类型的实例(要求此map中的key为枚举类型,否则抛出异常), 则把指定map中的映射放入此实例中。
 *
 * @param m the map from which to initialize this enum map
 * @throws IllegalArgumentException if <tt>m</tt> is not an
 *     <tt>EnumMap</tt> instance and contains no mappings
 * @throws NullPointerException if <tt>m</tt> is null
 */
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);
    }
}

这个三个构造方法要求分别传入 EnumType Class 对象、EnumMap、Map(要求此map的key为枚举类型),不管哪个构造方法都可以获得 keyType,即 EnumType。


Ⅴ-put&get

put方法:

/**
 * 在此map中用指定的key关联指定的value。
 * 如果此map目前包含了此key的映射,则旧的value将被替换。
 * 
 * @return 指定的key关联的oldValue, 如果没有包含此key的映射则返回{null}
 * 		   (返回{null}也能是因为指定的key关联的value为{null})
 * @throws NullPointerException if the specified key is null
 */
public V put(K key, V value) {
    typeCheck(key);

    int index = key.ordinal();
    Object oldValue = vals[index];
    vals[index] = maskNull(value);
    if (oldValue == null)
        size++;
    return unmaskNull(oldValue);
}

get方法:

/**
 * 返回指定key映射的value, 如果此map不包含指定key的映射,则返回null。
 * or {@code null} if this map contains no mapping for the key.
 *
 * 返回{null}并不能说明此map不包含指定key的映射,也能是因为指定的key关联的value为{null},
 * {containsKey}操作可以区别这中情况。
 */
public V get(Object key) {
    return (isValidKey(key) ?
            unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
}

Ⅵ-NULL对象精确size

我们已经了解到,EnumMap是通过 Object[] vals 来建立映射的,数组下标 i 对应 key.ordinal 值,vals[i] 对应映射的 value 值,那么我们怎么统计 EnumMap 中保存的映射数量 size 呢,如果我们仅仅是判断 vals 数组中不为 null 的元素个数,那么对于本身映射到 null 的 key又如何解释呢,为了区分 vals[i] 是因为不含有当前映射为null,还是因为key本身就映射到了null 这两种情况,EnumMap 专门新增了一个字段 Object NULL:

/**
 * Distinguished non-null value for representing null values.
 * (区分非空值来表示空值)
 */
private static final Object NULL = new Object() {
    public int hashCode() {
        return 0;
    }

    public String toString() {
        return "java.util.EnumMap.NULL";
    }
};

在 put 方法和 get 方法中使用到了两个方法 maskNull 和 unmaskNull:

private Object maskNull(Object value) {
    return (value == null ? NULL : value);
}

@SuppressWarnings("unchecked")
private V unmaskNull(Object value) {
    return (V)(value == NULL ? null : value);
}

整两个方法是用来标记 null 值的,在 put 的时候,如果 value 为 null,则用 NULL 对象代替,这样一来,如果oldValue 为 null 就真的说明了这个位置之前是真的不存在映射值,那么 size 就自增,从而保证了 size 的准确性

// put方法中的部分代码: 
vals[index] = maskNull(value);
if (oldValue == null)
    size++;

EnumMap简单使用

enum ColorEnum {
    RED, GREEN, BLUE
}

public class Test {
    public static void main(String[] args) {
        EnumMap<ColorEnum, String> enumMap = new EnumMap(ColorEnum.class);
        enumMap.put(ColorEnum.RED, "红");
        enumMap.put(ColorEnum.GREEN, "绿");
        enumMap.put(ColorEnum.BLUE, "蓝");
        System.out.println(enumMap); //{RED=红, GREEN=绿, BLUE=蓝}
        System.out.println(enumMap.size()); //3
        System.out.println(enumMap.get(ColorEnum.RED)); //红
    }
}

EnumMap方法

如果你对集合框架熟悉的话,看到这些方法你一定不会陌生,大部分都是集合的通用方法。



7.EnumSet


EnumSet简介


Ⅰ-EnumSet类继承结构
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E> implements Cloneable, java.io.Serializable

EnumSet继承结构图

EnumSet 继承了 AbstractSet 抽象类,参数化枚举类型 (Enum 作为泛型)。


Ⅱ-EnumSet子类实现

EnumSet继承结构

EnumSet 是一个抽象类,自身不能实例化对象,我们一般使用的都是它的子类实现,EnumSet 只有两个实现类 JumboEnumSet 和 RegularEnumSet,这两个实现都是 EnumSet 的私有实现类(类采用默认修饰符修饰访问权限),因此这两个类对于我们来说是不可见的(当然除非你是 java.util 包的开发者),因此我们无法通过 new 关键字来创建它们的实例,好在 EnumSet 类中提供了实例化子类对象并返回子类实例的方法。JumboEnumSet 和 RegularEnumSet 分别应用与不同的场景,JumboEnumSet 适用于 “巨型” 枚举类型(即具有64个以上元素的枚举类型);RegularEnumSet 则适用于“常规大小”枚举类型(即,具有64个或更少的枚举常量的枚举类型)。我们将在之后对它们进行详细的介绍。


Ⅲ-EnumMap官方介绍

官方介绍:

/**
 * 专门用于枚举类型的{Set}实现. EnumSet中的所有元素都必须来自创建Set时显式或隐式指定的单个枚举类型.  
 * EnumSet在内部表示为位向量. 这种表示非常紧凑和高效。 此类的空间和时间性能都足够好,可以作为一个
 * high-quality、类型安全选择以替换基于 int 的 "bit flags"(位标志)。
 * 如果它们的参数也是EnumSet,即使是批量操作(诸如containsAll、retainAll)也会运行地很快。
 * 
 * 迭代器是 weakly consistent(弱一致性的): 
 * 它们将不会抛出{ConcurrentModificationException}异常,并且它们可能会显示也可能不会显示在
 * 进行迭代时对map进行的任何修改的效果。
 * iterator方法返回的迭代器在迭代元素的时候采用自然顺序(enum constants声明的顺序)
 * 返回的迭代器是weakly consistent(弱一致性的):
 * 		在迭代器的迭代过程中,对set进行的任何修改都不会抛出{ConcurrentModificationException}异常,
 *		并且它们可能会显示也可能不会显示修改的效果。
 *
 * 不允许空元素,试图插入一个null元素将会抛出{NullPointerException}异常。
 * 但是试图测试是否存在或者删除null元素将正常工作,
 *
 * 像大多数的集合实现一样,EnumMap 也不是 synchronized, 如果多个线程同时访问 EnumMap,
 * 并且有至少一个线程修改此map, 应该在外部synchronized, 这通常是通过对一些自然封装EnumMap
 * 的对象进行同步来实现的, 如果没有这样的对象存在,则此map应该使用{Collections#synchronizedMap}
 * 方法进行包装。且最好是在创建时完成包装,以防止意料之外的unsynchronized访问:
 * 		Set<MyEnum> s = Collections.synchronizedSet(EnumSet.noneOf(MyEnum.class));
 *
 * 实现备注:所有基础操作的执行只需要常量时间。
 * 它们可能(虽然不能保证)比对应的{HashSet}更快, 如果参数也是EnumSe, 则批量操作的小号也是常量时间
 * 
 * @since 1.5
 */
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable

EnumSet特点

  • 专门服务于枚举

  • 不允许null元素: 因为持有的枚举常量,而枚举常量不可能为空。

  • 紧凑高效:具体实现采用位向量,紧凑而高效。


EnumSet底层实现


Ⅰ-基本实现和规范

EnumSet 抽象类并没有提供核心的代码实现,只是在存储结构和方法上做了一些列的规范:

基础存储结构:

/**
 * The class of all the elements of this set.
 */
final Class<E> elementType;

/**
 * All of the values comprising T.  (Cached for performance.)
 */
final Enum<?>[] universe;

elementType 保存了元素类型的 Class 对象,universe 数组则是缓存了对应元素类型(枚举类型)的所有枚举常量

构造方法:

EnumSet(Class<E>elementType, Enum<?>[] universe) {
    this.elementType = elementType;
    this.universe    = universe;
}

EnumSet 只有这一个构造方法,这是专门给本包中的子类调用的,这要求子类创建实例时明确指出对应枚举类型和枚举常量组成的数组。

静态创建实例方法:

我们先看一个相对比较重要的静态方法 oneOf,因为他决定了返回的 EnumSet 的具体实例类型:

/**
 * Creates an empty enum set with the specified element type.
 *
 * @param <E> The class of the elements in the set
 * @param elementType the class object of the element type for this enum
 *     set
 * @return An empty enum set of the specified type.
 * @throws NullPointerException if <tt>elementType</tt> is null
 */
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");

    //如果枚举类对应的枚举常量数 <= 64,则创建并返回 RegularEnumSet的实例。
    if (universe.length <= 64)
        return new RegularEnumSet<>(elementType, universe);
    else //否则创建并返回一个 JumboEnumSet 实例。
        return new JumboEnumSet<>(elementType, universe);
}

of 等一些重载方法其实也是调用了 noneOf 方法,区别在于同时添加了把参数指定的元素:

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

重写方法:

AbstractSet 类和 EnumSet 中定义的抽象方法和其普通方法将在子类中根据需要被重写:
待重写的抽象方法


Ⅱ-RegularEnumSet

之前一直在说 EnumMap, EnumSet 是专门用于服务于枚举的,那我们我们不禁会想,枚举有什么特殊的呢,以至于需要专门为其设计相应的集合实现呢?其实仔细想来,为什么传统的集合如 HashMap,ArrayList等,为何他们不是完美的,比如说有的集合空间上性能好,时间上性能低,有的查找快增删慢,有的则反之。因为这些集合的实现虽有是应对于不同的适用场景,但是真正限制它们的其实是通用性,即不是为单个类型的对象实现的,它需要能够存储大部分类型的对象(因为限定为Object,理论上是所有对象)。因此他要保证通用,就无法根据某个特定对象进行设计,因此无法在各方面做到完美。而 EnumSet 却不同,它是Enum 类型定制的,他当然只需要考虑 Enum类型的特点而进行设计来在 time 和 space 的表现上都达到完美的境地。在开发中专门为一个或几个类型设计存储结构和实现来提高性能其实并不普遍,因为 JDK 提供的 API 已经相当成熟稳定和高效了,而且这对工程师对业务的理解和编程基础都具有较高的要求,但并不是说专业化设计没有必要。(有点题外话了,回归正题)。

那我们从枚举的特点结构出发,来分析一下:一个枚举类有有限个枚举常量,每个枚举常量都有一个整型 ordinal字段,且其值是连续的,因为它代表了枚举常量在枚举类中声明的序号(从 0 开始,这在之前已经介绍过了)。而且值得注意的是,在同一个枚举类中,ordinal 可以认为是枚举常量的唯一标识,且连续。枚举类型和 ordinal 可以唯一缺一个枚举常量。那么我直接存储 枚举类型 和 ordinal 不就可以达到保存枚举常量的效果了吗?而且聪明的设计师并没有直接用数组保存 ordinal 值,而是用位(bit):

比如现在有一个 long 类型的 L,我们都知道它有 8 个字节,64位,对于值为 0 的 long 类型常量,它的二进制为:0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

想想看,二进制意味着什么,0 代表低电压,1代表高电压,同时 0 也可以代表没有,1 代表有,那么 64 位的二进制可以表示 64 个有或者没有,再结合我们之前提到的 ordinal 想想看,从右往左数,如果我把第 i 的二进制的状态( 0或1)来表示 ordinal 为 i 的枚举常量的存在与否,那么我可以轻易的存储 64个或 64 个一下的枚举常量的状态(体现在集合中即包含或不包含),这样既紧凑又高效,二进制仅仅通过位运算即可判断,还有比位运算更快的传统数学运算吗。

说道这里我们再来看一下 RegularEnumSet 类的实现:

/**
 * Private implementation class for EnumSet, for "regular sized" enum types
 * (i.e., those with 64 or fewer enum constants).
 * EnumSet的私有实现类,用于“常规大小”枚举类型(即,具有64个或更少的枚举常量的枚举类型)。
 */
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {

不知道你现在有没有理解为什么只能存储64个或更少的枚举常量。

这就是我们之前提到的用来存储元素状态的 long 类型的L

/**
 * Bit vector representation of this set.  The 2^k bit indicates the
 * presence of universe[k] in this set.
 * 此集合的位向量表示。 2^k位指示此集合中是否存在Universe [k]。
 */
private long elements = 0L;

部分核心实现:采用位运算

public boolean add(E e) {
    typeCheck(e);

    long oldElements = elements;
    elements |= (1L << ((Enum<?>)e).ordinal());
    return elements != oldElements;
}

public boolean contains(Object e) {
    if (e == null)
        return false;
    Class<?> eClass = e.getClass();
    if (eClass != elementType && eClass.getSuperclass() != elementType)
        return false;

    return (elements & (1L << ((Enum<?>)e).ordinal())) != 0;
}
...

为了便于理解,粗略地画了一个图(为了不至于让图片太大,这里采用8bit进行演示,原理和64bit相同):
add&contains操作简图

第一个add 操作,枚举常量的 ordinal 值为 5,则 element 二进制的从右往左第 6 被标记为 1

第二个 contains 操作,枚举常量的 ordinal 值为 3,通过 & 运算判断出 element 第 4 位为0,返回 false(不包含)


Ⅲ-JumboEnumSet

JumboEnumSet 的核心思想和 RegularEnumSet 相同,都是通过位向量实现,之前提到过基本类型中字节数最多的也就是long类型了,它可以保存64个枚举常量的状态,但是如果枚举常量个数超过了64个怎么办呢,这时候 JumboEnumSet 采用了 long 类型数组的形式,long elements[] 数组中有几个元素,就能保存 elements.length * 64 个枚举类型的个数。

/**
 * Bit vector representation of this set.  The ith bit of the jth
 * element of this array represents the  presence of universe[64*j +i]
 * in this set.
 * 此 set 的位向量表示。该数组第 j 个元素的第 i 位用该set中的universe[64*j +i]表示。
 */
private long elements[];

// Redundant - maintained for performance(冗余-保持性能)
private int size = 0;

JumboEnumSet(Class<E>elementType, Enum<?>[] universe) {
    super(elementType, universe);
    // 根据枚举常量的个数创建能容纳所有枚举常量的最小数组
    elements = new long[(universe.length + 63) >>> 6];
}

其他核心方法的实现就和 RegularEnumSet 大同小异了。


EnumSet简单使用

enum ColorEnum {
    RED, GREEN, BLUE
}


public class Test {

    public static void main(String[] args) {
        EnumSet<ColorEnum> colorEnums = EnumSet.noneOf(ColorEnum.class);
        colorEnums.add(ColorEnum.RED);
        colorEnums.add(ColorEnum.GREEN);
        colorEnums.add(ColorEnum.BLUE);

       //上面的代码等价于:
       // EnumSet<ColorEnum> colorEnums = EnumSet.allOf(ColorEnum.class);

        System.out.println(colorEnums); //[RED, GREEN, BLUE]
    }
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值