ZhouXiangの博客 后端工程师&机器学习爱好者

Java枚举简析

2018-10-24

Java枚举简析。

什么是枚举

枚举是JDK1.5版本新增的特性(泛型、For-each等如今被广泛应用的特性也是由JDK1.5时所新增的),另外到了JDK1.6后switch语句支持枚举类型。

枚举出现前的背景

使用一组int常量(称作int枚举模式),例如:

public class Season {
    public static final int SPRING = 1;
    public static final int SUMMER = 2;
    public static final int AUTUMN = 3;
    public static final int WINTER = 4;
}

上述写法在安全性、易用性和可读性上存在一定问题。

枚举的好处以及与常量类的区别

1) 枚举型可以直接与数据库打交道,我通常使用varchar类型存储,对应的是枚举的常量名。(数据库中好像也有枚举类型,不过也没用过)
2) switch语句支持枚举型,当switch使用int、String类型时,由于值的不稳定性往往会有越界的现象,对于这个的处理往往只能通过if条件筛选以及default模块来处理。而使用枚举型后,在编译期间限定类型,不允许发生越界的情况。
3) 当你使用常量类时,往往得通过equals去判断两者是否相等,使用枚举的话由于常量值地址唯一,可以用==直接对比,性能会有提高。
4) 常量类编译时,是直接把常量的值编译到类的二进制代码里,常量的值在升级中变化后,需要重新编译引用常量的类,因为里面存的是旧值。枚举类编译时,没有把常量值编译到代码里,即使常量的值发生变化,也不会影响引用常量的类。
5) 枚举类编译后默认为final class,不允许继承可防止被子类修改。常量类可被继承修改、增加字段等,容易导致父类的不兼容。

枚举的特点

1) 使用关键字enum 2) 类型名称 3) 一串允许的值 4) 枚举可以单独定义在一个文件中,也可以嵌在其它Java类中 5) 枚举可以实现一个或多个接口(Interface) 6) 可以定义新的变量 7) 可以定义新的方法 8) 可以定义根据具体枚举值而相异的类 9) 枚举不可被继承

枚举的本质

public enum t {
    SPRING,SUMMER,AUTUMN,WINTER;
}

反编译后如下:

public final class T extends Enum {
    private T(String s, int i) {
        super(s, i);
    }
    public static T[] values() {
        T at[];
        int i;
        T at1[];
        System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
        return at1;
    }
    public static T valueOf(String s) {
        return (T)Enum.valueOf(demo/T, s);
    }
    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T ENUM$VALUES[];
    static {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

通过反编译后代码我们可以看到,public final class T extends Enum,说明,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。

枚举创建与线程安全

当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。

枚举与单例模式

双重校验锁:

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  

枚举实现单例:

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}  

上面的双重锁校验的代码之所以很臃肿,是因为大部分代码都是在保证线程安全。为了在保证线程安全和锁粒度之间做权衡,代码难免会写的复杂些。但是,这段代码还是有问题的,因为他无法解决反序列化会破坏单例的问题。

枚举与序列化

单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。

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 const " + enumType +"." + name);  
}  

枚举的==与equals区别

== 和 equals 方法没啥区别,两个随便用都是一样的效果。因为枚举 Enum 类的 equals 方法默认实现就是通过 == 来比较的;类似的 Enum 的 compareTo 方法比较的是 Enum 的 ordinal 顺序大小;类似的还有 Enum 的 name 方法和 toString 方法一样都返回的是 Enum 的 name 值。

枚举类可以继承其他类(或实现其他接口)或者被其他类继承吗,为什么?

枚举类可以实现其他接口但不能继承其他类,因为所有枚举类在编译后的字节码中都继承自 java.lang.Enum,而 Java 不支持多继承,所以枚举类不可以继承其他类。 枚举类不可以被继承,因为所有枚举类在编译后的字节码中都是继承自 java.lang.Enum)的 final class 类,final 的类是不允许被派生继承的。

switch是如何对枚举进行支持的?

Java 1.7 之前 switch 参数可用类型为 short、byte、int、char,枚举类型之所以能使用其实是编译器层面实现的,编译器会将枚举 switch 转换为类似 switch(s.ordinal()) { case Status.START.ordinal() } 形式,所以实质还是 int 参数类型,感兴趣的可以自己写个使用枚举的 switch 代码然后通过 javap -v 去看下字节码就明白了。


Similar Posts

上一篇 Mahout简介

Comments