揭开枚举的面纱

Posted by W-M on December 18, 2017

由于枚举平时使用的不多,每次用到的时候都有一种陌生感,总是忘记其语法为什么是这样的,导致回头重新理解其实现原理,浪费时间。于是在这篇博客中记录一下,下次再忘记的时候好很快捡起来


揭开枚举语法糖

从一个枚举类Demo开始说起:

enum Signal {
    GREEN {
        @Override
        public void sayColor() {
            System.out.println("I am Green");
        }
    },
    YELLOW {
        @Override
        public void sayColor() {
            System.out.println("I am Yellow");
        }
    },
    RED {
        @Override
        public void sayColor() {
            System.out.println("I am Red");
        }
    };

    public void sayColor() {
        System.out.println("say your color");
    }
}

public class TrafficLight {
    private Signal currentColor = Signal.RED;

    public void changeColor(Signal signal) {
        if (signal == currentColor) {//传入当前color,则按照自动机进行转换
            switch (signal) {
                case RED: currentColor = Signal.GREEN;
                    break;
                case GREEN: currentColor = Signal.YELLOW;
                    break;
                case YELLOW: currentColor = Signal.RED;
                    break;
            }
        } else {
            currentColor = signal;//将当前color指定为其它颜色color
        }
    }

    public static void main(String[] args) {
        TrafficLight light = new TrafficLight();
        light.changeColor(light.currentColor);
        light.currentColor.sayColor();
        light.changeColor(Signal.RED);
        light.currentColor.sayColor();
    }
}

输出结果:
	I am Green
	I am Red

枚举实例只能在编译时由编译器创建,不能由程序员手动创建。设置枚举关键字的初衷是为了使得程序员写出的代码更加简单易懂。在程序员眼中,枚举类型只需要加一个关键字enum就可以实现,每个枚举实例还可以有自己的方法等等一系列特性,那么为什么枚举类型会有上述的这些特性呢?实际上枚举类型在经过编译器编译后会生成一个对应的类,下面就通过反编译这个枚举对应的class来揭开枚举语法糖。在上例枚举类Signal经过编译器编译后会生成Signal.class、Signal$1.class、Signal$2.class、Signal$3.class四个类,使用jad工具反编译Signal.class结果如下:

class Signal extends Enum
{
    public static Signal[] values()
    {
        return (Signal[])$VALUES.clone();
    }

    public static Signal valueOf(String name)
    {
        return (Signal)Enum.valueOf(com/enumTest/Signal, name);
    }

    private Signal(String s, int i)
    {
        super(s, i);
    }

    public void sayColor()
    {
        System.out.println("say your color");
    }


    public static final Signal GREEN;
    public static final Signal YELLOW;
    public static final Signal RED;
    private static final Signal $VALUES[];

    static 
    {
        GREEN = new Signal("GREEN", 0) {

            public void sayColor()
            {
                System.out.println("I am Green");
            }

        };
        YELLOW = new Signal("YELLOW", 1) {

            public void sayColor()
            {
                System.out.println("I am Yellow");
            }

        };
        RED = new Signal("RED", 2) {

            public void sayColor()
            {
                System.out.println("I am Red");
            }

        };
        $VALUES = (new Signal[] {
            GREEN, YELLOW, RED
        });
    }
}

由反编译后的结果可以看出,枚举类只不过是继承了java.lang.Enum类的普通类型而已,java.lang.Enum类是所有枚举类型的基类,禁止程序员手动继承。我们在枚举类中定义的一个个常量:GREEN、YELLOW、RED编译后实际上不过是类中的一个个的静态变量而已,理解了这一点,枚举类型的一些语法我们就很容易理解了。


应该在什么时候使用枚举

使用枚举的好处有:

  • 语义清晰。比如上例中使用Signal.RED, Signal.Green, Signal.Blue代表红绿蓝相比于使用int型肯定更清晰
  • 可以限制方法传入的参数。比如上例中的changeColor方法,限制传入的参数只能为Signal.RED, Signal.Green, Signal.Blue三种
  • 枚举类型相比于普通类型静态常量可以表示更多的信息
  • 使用枚举方法实现单例模式可以防止反序列化时重新创建新的对象

所以当我们想要让程序更清晰易读或者限制方法传入的参数为有限的几种时可以选择使用枚举,这两种情况很容易理解,那么为什么枚举可以实现单例模式呢?为什么枚举实现的单例模式可以防止反序列化时重新创建新的对象呢?

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

上面就是一个使用枚举类型实现单例的例子,满足单例模式的要求:由于枚举类型INSTANCE实际上是static final类型的常量所以生成时线程安全、枚举类型的所有变量都在编译时确定,程序员无法再创建新的枚举类型对象。

通过枚举方式实现的单例模式是饿汉式的,肯定不如懒汉式的实现方式优雅(比如通过静态内部类方式实现单例模式),但这种实现的优势在于程序编写起来十分简单,并且无需程序员编写多余代码来防止反序列化时重新创建新的对象使得单例模式不再单例(使用其他方式实现的单例模式在反序列化时需要程序员进行控制来防止创建新的对象,比如在readResolve方法中替换将要返回给客户端的对象)。下面解释枚举通过什么方式来解决单例模式的序列化问题。

根据Java规范中介绍,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。java.lang.Enum的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 const " + enumType +"." + name);  
}  

从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的反编译后的class文件中编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。所以可以保证枚举类型及其定义的枚举变量在JVM中都是唯一的。


枚举存在的缺陷

使用枚举的坏处有:

(完)
参考文章:深度分析Java的枚举类型—-枚举的线程安全性及序列化问题