Java枚举单例

1. 前言

注意enum不是Enum,有Java基础的同学们应该都不会把二者混淆了。简单来说,enum只是jdk1.5引入的语法糖,它不是java中的新增类型,编译器在编译阶段会自动将它转换成一个继承于Enum的子类,例如如下的代码:

    public enum GenderEnum {
        MALE,
        FEMALE
    }

编译成class文件后,通过javap GenderEnum.class得到的简单的编译后的结构为:

public final class com.yuanxz.example.GenderEnum extends java.lang.Enum {
    public static final com.yuanxz.example.GenderEnum MALE;
    public static final com.yuanxz.example.GenderEnum FEMALE;
    static {};
    public static com.yuanxz.example.GenderEnum[] values();
    public static com.yuanxz.example.GenderEnum valueOf(java.lang.String);
}

从这里可以看到编译器已经将我们的GenderEnum处理成了Enum的子类了。

2. Java Enum类

2.1 Enum基本结构

既然是Enum的子类,那么我们就先来了解一下这个Enum类。这个类的基本结构还是很简单的,如下:

Java枚举单例_第1张图片

Enum类的成员变量只有name和ordinal,实现了Serilizable和Comparable接口。看到这个结构的时候很多人可能会有疑惑,如果只是简单继承Enum,怎么能够实现enum的功能的呢?确实,我们之前用javap GenderEnum.class得到的结构只是一个很简单的结构,实际上编译器为我们做了更多的工作,下面是使用javap -verbose GenderEnum.class得到的字节码内容:

public final class com.yuanxz.example.GenderEnum extends java.lang.Enum
  SourceFile: "GenderEnum.java"
  Signature: #45                          // Ljava/lang/Enum;
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM
Constant pool:

   ....                         // 省略了常量池中的内容,不影响这里分析

{
  public static final com.yuanxz.example.GenderEnum MALE;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final com.yuanxz.example.GenderEnum FEMALEE;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  static {};
    flags: ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
         0: new           #1                  // class com/yuanxz/example/GenderEnum
         3: dup           
         4: ldc           #13                 // String MALE
         6: iconst_0                          // ordinal参数的值为0
         7: invokespecial #14                 // Method "":(Ljava/lang/String;I)  构造GenderEnum对象
        10: putstatic     #18                 // Field MALE:Lcom/yuanxz/example/GenderEnum;  赋值给MALE变量
        13: new           #1                  // class com/yuanxz/example/GenderEnum
        16: dup           
        17: ldc           #20                 // String FEMALEE
        19: iconst_1                          // ordinal参数的值为1
        20: invokespecial #14                 // Method "":(Ljava/lang/String;I)  构造GenderEnum对象
        23: putstatic     #21                 // Field FEMALEE:Lcom/yuanxz/example/GenderEnum;  赋值给FEMALEE变量
        26: iconst_2                          // 数组的长度为2
        27: anewarray     #1                  // class com/yuanxz/example/GenderEnum
        30: dup           
        31: iconst_0      
        32: getstatic     #18                 // Field MALE:Lcom/yuanxz/example/GenderEnum;
        35: aastore       
        36: dup           
        37: iconst_1      
        38: getstatic     #21                 // Field FEMALEE:Lcom/yuanxz/example/GenderEnum;
        41: aastore       
        42: putstatic     #23                 // Field ENUM$VALUES:[Lcom/yuanxz/example/GenderEnum;    // 将MALE和FEMALE赋值给VALUES数组
        45: return        
      LineNumberTable:
        line 12: 0
        line 13: 13
        line 11: 26
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

  public static com.yuanxz.example.GenderEnum[] values();   // 拷贝返回VALUES数组
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=5, locals=3, args_size=0
         0: getstatic     #23                 // Field ENUM$VALUES:[Lcom/yuanxz/example/GenderEnum;
         3: dup           
         4: astore_0      
         5: iconst_0      
         6: aload_0       
         7: arraylength   
         8: dup           
         9: istore_1      
        10: anewarray     #1                  // class com/yuanxz/example/GenderEnum
        13: dup           
        14: astore_2      
        15: iconst_0      
        16: iload_1       
        17: invokestatic  #31                 // Method java/lang/System.arraycopy:(Ljava/lang/Object;ILjava/lang/Object;II)V
        20: aload_2       
        21: areturn       
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

  public static com.yuanxz.example.GenderEnum valueOf(java.lang.String);        //调用Enum的静态方法valueOf,第一个参数传递的是当前类对象
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #1                  // class com/yuanxz/example/GenderEnum
         2: aload_0       
         3: invokestatic  #39                 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         6: checkcast     #1                  // class com/yuanxz/example/GenderEnum
         9: areturn       
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
}

如果要翻译成java代码的话基本如下:

    public final class com.yuanxz.example.GenderEnum extends java.lang.Enum<com.yuanxz.example.GenderEnum> {


        public static final com.yuanxz.example.GenderEnum MALE;
        public static final com.yuanxz.example.GenderEnum FEMALE;

        private static final com.yuanxz.example.GenderEnum[] VALUES;

        static {

            MALE = new com.yuanxz.example.GenderEnum("MALE", 0);
            FEMALE = new com.yuanxz.example.GenderEnum("FEMALE", 1);

            VALUES = new GenderEnum[2];
            VALUES[0] = MALE;
            VALUES[1] = FEMALE;
        }

        public static com.yuanxz.example.GenderEnum[] values() {
            com.yuanxz.example.GenderEnum[] values = new com.yuanxz.example.GenderEnum[VALUES.length];
            System.arraycopy(VALUES, 0, values, 0, VALUES.length);
            return values;
        }


        public static com.yuanxz.example.GenderEnum valueOf(java.lang.String name) {
            return Enum.valueOf(com.yuanxz.example.GenderEnum, name);
        }

    }

从生成的字节码中可以看到,编译器为我们生成的类的static模块实际上为我们完成了构造MALE和FEMALE变量并且赋值给VALUES数组的工作,因此当GenderEnum被classloader加载进来的时候就完成了这些工作,之后我们就可以通过GenderEnum.MALE这种方式来直接使用枚举了。

2.2 enum的其他特性

不要仅仅只将enum当做一个枚举来使用,除了枚举的特性之外它还能做一些其他工作,包括可以使用成员变量和声明自己的方法等,例如:

public enum EnumInstance {

    INSTANCE;

    private EnumInstance() {
        mInstaceVar = INIT_VAR_VALUE;   // 给变量赋初值
    }

    private static final int INIT_VAR_VALUE = 10;

    private int mInstaceVar = 0;

    public void printSelf() {
        System.out.println("当前enum是: " + this);
    }

    public void addVar(int increment) {
        mInstaceVar += increment;
    }

    public int getVar() {
        return mInstaceVar;
    }


}

声明以上的枚举后,我们可以通过INSTANCE来调用它内部声明的addVar和getVar等方法,所有的这些都和普通类没有什么区别,除了它的构造函数。
enum不允许声明public的构造函数,目的是防止程序中随意地实例化枚举对象,但是它可是声明private的构造函数,比如上面那段代码中构造函数:

    private EnumInstance(int var) {
            mInstaceVar = var;   // 给变量赋初值
    }

我们可以通过如下的实现方式来指明enum调用这个构造函数:

    public enum EnumInstance {

        INSTANCE(100);  // 指明调用private EnumInstance(int var)

        private EnumInstance() {
            mInstaceVar = INIT_VAR_VALUE;   // 给变量赋初值
        }

        private EnumInstance(int var) {
            mInstaceVar = var;   // 给变量赋初值
        }

        ... // 省略其他代码


    }

需要注意的是,由于enum本身只是一个语法糖,因此声明的构造函数相关的代码实际上经过编译器的处理会变成Enum构造函数中的一部分,例如EnumInstance经过编译器处理后代码就基本如下:

    public final class com.yuanxz.example.EnumInstance extends java.lang.Enum {
          public static final com.yuanxz.example.EnumInstance INSTANCE;
          static {
                INSTANCE = new com.yuanxz.example.GenderEnum("MALE", 0);
                VALUES = new EnumInstance[1];
                VALUES[0] = INSTANCE;
          };

          private EnumInstance(String name, int ordinal) {  // Enum的构造函数
                super(name, ordinal);   // 先调用Enum的构造函数
                // 以下是我们声明的构造函数中的内容
                mInstaceVar = 100;
          }

          // 下面的函数省略描述
          public void printSelf();
          public void addVar(int);
          public int getVar();
          public static com.yuanxz.example.EnumInstance[] values();
          public static com.yuanxz.example.EnumInstance valueOf(java.lang.String);
    }

除此之外,由于编译器需要将enum最终翻译成Enum的子类,而Java中又不允许多继承,因此enum只支持实现接口,而不支持声明继承。

3. Java中的枚举单例

枚举类的基本使用这里就不再多说了,但是想在这里简单介绍一下枚举单例。单例模式要求在同一个进程当中,类对象始终只有一份实例,单例模式有很多种实现方案,例如如下的两种:

    /**
     * 方法一
     */
    public final class SingleInstance {

        private static final SingleInstance sInstance = new SingleInstance();

        private SingleInstance() {

        }

        public static SingleInstance getInstance() {
            return sInstance;
        }

    }

    /**
     * 方法二
     */
    public final class LazySingleInstance {

        private static SingleInstance sInstance;

        private SingleInstance() {

        }

        public synchronized static SingleInstance getInstance() {
            if (null == sInstance) {
                sInstance = new SingleInstance();
            }
            return sInstance;
        }

        /**
        // 或者使用double check方法
        public static SingleInstance getInstance() {
            if (null == sInstance) {
                synchronized(SingleInstance.class) {
                    if (null == sInstance) {
                        sInstance = new SingleInstance();
                    }
                }
            }
            return sInstance;
        }
        */

    }

上面的两种方法基本上已经能够满足平时的使用场景的需求,即保证通过getInstance方法在同一个进程中只能获取到一个SingleInstance的实例,同时还使用了classloader加载Class时的线程安全性和synchronized来保证多线程竞争环境下也只能获取到一个单例。相比较于上面大段代码的实现,如果使用enum来实现单例只需要一句:

public enum SingleInstance {

    INSTANCE;

}

只需要这样就能保证在我们代码中使用SingleInstance.INSTANCE在同一进程下获取到的一定是同一个单例对象。是不是很方便很神奇!但是还不仅仅如此,enum还为单例的实现提供了更多的的天然特性,下面就来简单总结一下:

3.1 多线程环境下的安全性

这个特性上面已经介绍了,因为INSTANCE最后会被编译器处理成static final的,并且在static模块中进行的初始化,因此它的实例化是在class被加载阶段完成,是线程安全的。这个特性也决定了枚举单例不是lazy的,如果你的单例初始化比较费时且大多数情况下只会被引用但是不会被真正调用的话,你需要使用lazy的单例模式(上面传统单例的方法二实现),而不要选择枚举单例。

3.2 不可被反射实例化

在Java中,不仅通过限制enum只能声明private的构造方法来防止Enum被使用new进行实例化,而且还限制了使用反射的方法不能通过Constructor来newInstance一个枚举实例。在你尝试使用反射得到的Constructor来调用其newInstance方法来实例化enum时,回得到一个exception:

java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:521)

这一点,使用传统的单例模式是不好来保证的。注意,虽然Enum不能被反射实例化,但是它的其他方法是可以被反射调用的,例如Enum的valueof方法实际上就是通过Class类的getEnumConstantsShared方法反射调用Enum类的values方法来实现的。

3.3 序列化的唯一性

传统的单例模式要防止序列化/反序列化的攻击必须要手动来实现readObject或者readResolve方法,这一点Enum已经我们保证了,官方对于Enum序列化处理的表述如下:

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream.
The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.

这说明了在序列化和反序列化的时候Enum的处理是和其他类不同的,在序列化的时候实际上只写入了Enum的name成员,而没有保存ordinal成员;在反序列化的时候从ObjectInputStream中读取Enum的name成员,同时调用Enum的valueof方法传入读取的name得到对应的ordinal值。同时Enum是不支持自定义序列化和反序列化的,一些序列化和反序列化对应的函数例如readObject和writeObject及serialVersionUID等属性在Enum的序列化过程中都是被忽略的。
Enum在序列化和反序列化上的特性保证了使用Enum来实现单例是经受得起序列化的攻击的。

4. enum的其他使用方法

4.1 使用enum来设计模板类

由于enum既有枚举的特性,又包含基本类的特性,因此结合他的特点能够巧妙地设计一些代码。例如,如果你的代码中存在这样的结构:

    if ("soldier".equals(toy)) {
        System.out.println("I'm a soldier."); 
    } else if ("doll".equals(toy)) {
        System.out.println("I'm a doll."); 
    } else {
        ...
    }

那么不妨考虑使用enum来实现:

    public enum Toy {

        SOLDIER {

            @Override 
            public void execute() { 
                System.out.println("I'm a doll."); 
            }
        },
        DOLL {

            @Override 
            public void execute() { 
                System.out.println("I'm a doll."); 
            }
        };

        // template method
        public abstract void execute();

    }

这样,直接使用toy.execute就能优雅地完成上面的功能,这是使用enum实现模板方法的典型方法。

4.2 使用value值反向查找enum对象

在Enum中提供了使用静态方法valuesof来根据name查询enum对象的方法,如果在程序中还有根据value来反向查找enum的需求,可以参考下面的代码来实现:

    public enum Status {
         WAITING(0),
         READY(1),
         SKIPPED(-1),
         COMPLETED(5);

         private static final Map lookup 
              = new HashMap();

         static {
              for(Status s : EnumSet.allOf(Status.class))
                   lookup.put(s.getCode(), s);
         }

         private int code;

         private Status(int code) {
              this.code = code;
         }

         public int getCode() { return code; }

         public static Status get(int code) { 
              return lookup.get(code); 
         }
    }

这样通过调用get方法传入value值就可以反向查找对应的Status枚举对象。注意这里使用了EnumSet,它相对于普通的HashSet,针对enum的执行效率更高,类似结构的还有EnumMap,在这里就不逐一介绍了。

5. 参考:

Making the Most of Java 5.0: Enum Tricks
What are enums and why are they useful?
Java中Enum类型的序列化

你可能感兴趣的:(java)