枚举Enum

枚举是Java1.5引入的新特性,通过关键字enum来定义枚举类。枚举类是一种特殊类,它和普通类一样可以使用构造器、定义成员变量和方法,也能实现一个或多个接口,但枚举类不能继承其他类。

一、原理分析

枚举类型使用的最常用类型就是枚举常量.下面通过一个简单的Demo来说明枚举的原理:

public enum Color {
    BLACK, WHITE
}

这样只是能够知道枚举简单的使用方法,不能看出枚举的特点和枚举的具体实现。

下面我们通过JDK自带的javap工具来反编译Color类,通过javap Color.class查看编译后的源码内容:

public final class Color extends java.lang.Enum<Color> {
  public static final Color BLACK;
  public static final Color WHITE;
  public static Color[] values();
  public static Color valueOf(java.lang.String);
  static {};
}

从反编译的类中,可以看出, 我们使用enum关键字编写的类,在编译阶段编译器会自动帮我们生成一份真正在jvm中运行的代码。

该类继承自 Enum类,public abstract class Enum<E extends Enum<E>>implements Comparable<E>, Serializable。

Enum类接受一个继承自Enum的泛型(在反编译java文件中没有体现泛型是因为泛型在阶段就会被类型擦除,替换为具体的实现)。

从反编译的Color类中可以看出,在enum关键字的类中,都会生成一个 Color实例,且它是在静态域中进行初始化的, 而静态域在类加载阶段的cinit中进行初始化,所以枚举对象是线程安全的,由JVM来保证。

外部可以通过values()方法获取当前枚举类的所有实例对象。

二、Enum成员变量和方法分析

Enum类实现了 Comparable接口,表明它是支持排序的,可以通过 Collections.sort 进行自动排序.实现了public final int compareTo(E o)接口,方法定义为final且其实现依赖的ordinal字段也是final类型,说明他只能根据ordinal排序,排序规则不可变。

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

ordinal:表示枚举的顺序,从Color类中可以看出,它是从0开始按自然数顺序增长,且其值是final类型,外部无法更改。对于 ordinal()方法,官方建议尽量不要使用它,它主要是提供给EnumMap,EnumSet使用的。

/**
     * Returns the ordinal of this enumeration constant (its position
     * in its enum declaration, where the initial constant is assigned
     * an ordinal of zero).
     *
     * Most programmers will have no use for this method.  It is
     * designed for use by sophisticated enum-based data structures, such
     * as {@link java.util.EnumSet} and {@link java.util.EnumMap}.
     *
     * @return the ordinal of this enumeration constant
     */
    public final int ordinal() {
        return ordinal;
    }

name:表示枚举类的名字,从Color类的构造函数可以看出,它的值就是我们定义的实例的名称。
我们在例子中之所以能打印出实例名称,是因为 它的toString()方法直接返回了name属性。

/**
     * Returns the name of this enum constant, as contained in the
     * declaration.  This method may be overridden, though it typically
     * isn't necessary or desirable.  An enum type should override this
     * method when a more "programmer-friendly" string form exists.
     *
     * @return the name of this enum constant
     */
    public String toString() {
        return name;
    }

equals():从其实现来看, 我们程序中使用 == 或者 equals来判断两个枚举相等都是一样的。

 public final boolean equals(Object other) {
        return this==other;
 }

getDeclaringClass():方法返回枚举声明的Class对象。

每一个枚举类型极其定义的枚举变量在JVM中都是唯一的

这句话的意思是枚举类型它拥有的实例在编写的时候,就已经确定下,不能通过其他手段进行创建,且枚举变量在jvm有且只有一个对应的实例。

为了达到这个效果,它通过以下方法来确保。

1、类加载时创建,保证线程安全

从Color类中可以看出, Color对象是在静态域创建,由类加载时初始化,JVM保证线程安全,这样就能确保Color对象不会因为并发同时请求而错误的创建多个实例。

2、对序列化进行特殊处理,防止反序列化时创建新的对象

我们知道一旦实现了Serializable接口之后,反序列化时每次调用 readObject()方法返回的都是一个新创建出来的对象。

而枚举则不同,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过Enum的valueOf()方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化进行定制,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

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);
    }
    /**
     * prevent default deserialization
     */
    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");
    }

3、私有构造函数, 无法正常的 new出对象

    // 私有的构造函数
    private Color(String name, int ordinal) {
        super(name, ordinal);
    }

4、无法通过 clone()方法,克隆对象

    /**
     * Throws CloneNotSupportedException.  This guarantees that enums
     * are never cloned, which is necessary to preserve their "singleton"
     * status.
     */
    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

5、无法通过反射的方式创建枚举对象

枚举类型,在 JVM 层面禁止了通过反射构造枚举实例的行为,如果尝试通过反射创建,将会报Cannot reflectively create enum objects.

    static void reflectTest() throws Exception {
        // 获取类对象
        Class<?> cls = Class.forName("em.Color");
        // 获取 color 的构造函数
        Constructor<?> constructor = cls.getDeclaredConstructor(String.class, int.class);
        // 获取访问权限
        constructor.setAccessible(true);
        // 实例化
        Object reflectColor = constructor.newInstance("name", 0);
    }

    // 报错
    Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at Main.reflect(Main.java:24)
    at Main.main(Main.java:13)

枚举类的特点总结

  • 枚举实例必须在 enum关键字声明的类中显式的指定(首行开始的以第一个分号结束)。
  • 除了上面这种方式, 没有任何方式(new,clone,反射,序列化)可以手动创建枚举实例。
  • 枚举类不可被继承。
  • 枚举类是线程安全的。
  • 枚举类型是类型安全的(typesafe)。
  • 无法继承其他类(已经默认继承Enum)

三、枚举的使用

枚举常量

可以在 switch语句中使用

    void enmuTest() {
        Color tag = Color.BLACK;
        switch (tag) {
            case WHITE:
                break;
            case BLACK:
                break;
        }
    }

枚举类型是类型安全的,可以对传入的值进行类型检查:

如有个 handleColor(Color color)方法,那么方法参数自动会对类型进行检查,只能传入 Color.WHITE和Color.BLACK,如果使用 static final定义的常量则不具备 类型安全的特点。

枚举与构造函数

枚举类可以编写自己的构造函数,但是不能声明public,protected,为了是不让外部创建实例对象,默认为private且只能为它。

枚举与类

除了枚举常量外, enum是一个完整的类,它也可以编写自己的构造方法以及方法,甚至实现接口。

这里需要注意,枚举类不能继承其他类,因为在编译时它已经继承了 Enum,java无法多继承。

// 实现Runnable接口,在这个类中没有意义,只是为了举例
public enum Color implements Runnable {
    WHITE("黑色"),
    BLACK("白色");
    private final String value;

    // 自定义构造,虽然没有写private,但是默认就是private
    Color(String v) {
        value = v;
    }

    // 自定义方法
    public void draw() {
        System.out.println("绘制 " + value);
    }

    // 重写方法
    @Override
    public String toString() {
        return value;
    }

    // 实现接口方法
    @Override
    public void run() {
        // todo ...
    }
}

枚举与单例模式

单例模式有很多种实现方式,除了枚举方式外,都有一些缺点,就是不能完全保证单例在jvm中的唯一性。

  • 反序列化对象时会破环单例

反序列化对象时不会调用getXX()方法,于是绕过了确保单例的逻辑,直接生成了一个新的对象,破环了单例。

解决办法是:重写类的反序列化方法,在反序列化方法中返回单例而不是创建一个新的对象。

public class Singleton implements java.io.Serializable {     
   public static Singleton INSTANCE = new Singleton();     
 
   protected Singleton() {     
   }  
 
   //反序列时直接返回当前INSTANCE
   private Object readResolve() {     
            return INSTANCE;     
      }    
}
  • 在代码中通过反射机制,直接调用类的私有构造函数创建新的对象,会破环单例

解决办法是:维护一个volatile的标志变量在第一次创建实例时置为false;重写构造函数,根据标志变量决定是否允许创建。

private static volatile  boolean  flag = true;
private Singleton(){
    if(flag){
        flag = false;   //第一次创建时,改变标志
    }else{
        throw new RuntimeException("The instance  already exists !");
    }
}

这两种缺点虽然都有方式解决,但是不免有些繁琐。

枚举模式实现单例原理:定义枚举类型,里面只维护一个实例,以此保证单例。每次取用时都从枚举中取,而不会取到其他实例。

public enum  SingletonEnum {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}

优点:

  • 使用SingletonEnum.INSTANCE进行访问,无需再定义getInstance方法和调用该方法。
  • JVM对枚举实例的唯一性,避免了上面提到的反序列化和反射机制破环单例的情况出现:每一个枚举类型和定义的枚举变量在JVM中都是唯一的。原因:枚举类型在序列化时仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器禁止重写枚举类型的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

EnumSet的使用

java.util.EnumSet是枚举类型的集合类,可以保证集合中的枚举元素不重复。

// EnumSet的使用
EnumSet<EnumTest> weekSet = EnumSet.allOf(aEnum.class); //将枚举类.class文件作为参数。至于为何使用.class文件,在下文中揭晓
        

EnumMap的使用

java.util.EnumMap中的 key是enum类型,而value则可以是任意类型。

 // EnumMap的使用
 EnumMap<自定义Enum类型, String> weekMap = new EnumMap(aEnum.class);

 weekMap.put(aEnum.MON, "星期一");
 weekMap.put(aEnumt.TUE, "星期二");

枚举如何比较

枚举实例可以使用 == 作比较,也可以 使用 equals() 作比较,二者结果一样,因为Enum类重写了 equals()方法,其实质还是使用 == 作比较的。

public final boolean equals(Object other) {
    return this==other;
}

四、总结

五、参考

https://www.jianshu.com/p/0d69c36a723b

https://www.cnblogs.com/ygj0930/p/10845530.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

codedot

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值