深入理解枚举类

深入理解枚举

 最近刚学习完JVM相关知识,想到枚举既然这么异类,那就从字节码角度来分析一下它。有关枚举的讲解,很多博客已经很详细了,这里我们就从字节码的角度重新来认识一下它。

 枚举类是一种特殊的类,它可以有自己的成员变量,方法,可以实现一个或多个接口,也可也定义自己的构造器。

1. 枚举类的继承结构:

2. 枚举类和普通类的区别:

(1)枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是继承Object类,因此枚举不能够显示继承其他父类(单继承局限性,但是可以实现接口)。其中“java.lang.Enum”实现了“Comparable”和“Serializable”接口。

(2)使用enum定义,非抽象的枚举类默认会使用final修饰,因此枚举类不能够派生子类。

(3)枚举类的构造器只能够使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰;如果强制指定访问控制符,则只能指定private修饰符。

(4)枚举类的所有实例必须要在枚举类的第一行显示列出,否则这个枚举类永远都能产生实例。列出这些实时,系统会自动添加public static final修饰符,无需程序员显示添加。

(5)枚举类默认提供了一个values方法,返回枚举实例数组,该方法可以很方便的遍历所有的枚举值。

为了能够更好的说明,上面的这些不同之处,下面我们定义了一个枚举类,使用“javap -v -p ”来反编译它

enum  Season {
   SPRING("春"),SUMMER("夏"),AUTUMN("秋"),WINTTER("冬");
   private String name;

   Season(String name) {
      this.name = name;
   }
}

反编译可以得到这些信息:

$ javap -v -p Season.class
Classfile /D:/Project/JvmDemo/target/classes/com/bigdata/java/Season.class
  Last modified 2020-3-21; size 1206 bytes
  MD5 checksum e78087beee7e634071bc6cd1e019c168
  Compiled from "Demo69.java"
final class com.bigdata.java.Season extends java.lang.Enum<com.bigdata.java.Season>
  minor version: 0
  major version: 52
  flags: ACC_FINAL, ACC_SUPER, ACC_ENUM
Constant pool:
  ...
{
  //枚举类中定义的常量,在字节码层面上的反映     
  public static final com.bigdata.java.Season SPRING;
    descriptor: Lcom/bigdata/java/Season;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final com.bigdata.java.Season SUMMER;
    descriptor: Lcom/bigdata/java/Season;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final com.bigdata.java.Season AUTUMN;
    descriptor: Lcom/bigdata/java/Season;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final com.bigdata.java.Season WINTTER;
    descriptor: Lcom/bigdata/java/Season;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  private java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE
        
  //枚举数组,这个数组供values方法使用,用于返回枚对象数组
  private static final com.bigdata.java.Season[] $VALUES;
    descriptor: [Lcom/bigdata/java/Season;
    flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
  //自动生成了values方法,它所完成的功能就是$VALUES.clone
  public static com.bigdata.java.Season[] values();
    descriptor: ()[Lcom/bigdata/java/Season;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         //从类中获取静态字段$VALUES,然后压入到操作数栈的栈顶
         0: getstatic     #1                  // Field $VALUES:[Lcom/bigdata/java/Season;
         //调用实例方法clone,即出栈$VALUES,然后执行$VALUES.clone,由于clone是native方法并且有返回值,所以返回值会被压入到操作数的栈顶,此时操作数的栈顶是枚举对象数组       
         3: invokevirtual #2                  // Method "[Lcom/bigdata/java/Season;".clone:()Ljava/lang/Object;
         //弹出栈顶的枚举对象数组,检查它的类型是否符合给定的类型,检查完毕后再次压入到操作数栈的栈顶     
         6: checkcast     #3                  // class "[Lcom/bigdata/java/Season;"
         //将操作数栈顶的值弹出,并返回到给调用处,这样返回的就是一个clone后的枚举对象数组          
         9: areturn
      LineNumberTable:
        line 9: 0
  //它所完成的功能就是根据传入的字符串,调用父类的valueOf方法返回对应的枚举常量
  public static com.bigdata.java.Season valueOf(java.lang.String);
    descriptor: (Ljava/lang/String;)Lcom/bigdata/java/Season;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #4                  // class com/bigdata/java/Season
         2: aload_0
         3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         6: checkcast     #4                  // class com/bigdata/java/Season
         9: areturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  name   Ljava/lang/String;
  //枚举构造方法,接收String和int,String,第一个String是枚举常量字符串,第二个是枚举常量所定义的位置,第三个枚举中构造方法传入的值
  private com.bigdata.java.Season(java.lang.String);
    descriptor: (Ljava/lang/String;ILjava/lang/String;)V
    flags: ACC_PRIVATE
    Code:
      stack=3, locals=4, args_size=4
         //将本地变量表索引0上的引用类型元素压入到操作数的栈顶          
         0: aload_0
         //将本地变量表索引1上的引用类型元素压入到操作数的栈顶。尽管此时本地变量表对应索引上,元素为空,但运行时局部变量表被被填充为枚举常量字符串           
         1: aload_1
         //本地变量表索引2上的int型元素压入到操作数的栈顶,运行时本地变量表对应索引上会被填充为枚举常量定义的位置          
         2: iload_2
         //从操作数栈中依次弹出说压入的值,然后调用父类的构造方法,Enum."<init>":(Ljava/lang/String;I)V          
         3: invokespecial #6                  // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V
         //将本地变量表索引0上的引用类型元素压入到操作数的栈顶          
         6: aload_0
         //将本地变量表索引3上的引用类型元素压入到操作数的栈顶         
         7: aload_3
         //为对象中的字段设置值,从操作数栈中依次弹出枚举常量和构造方法所传入的值,为name字段赋值,如:SEASON.SPRING.name=“春”          
         8: putfield      #7                  // Field name:Ljava/lang/String;
        11: return
      LineNumberTable:
        line 13: 0
        line 14: 6
        line 15: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   Lcom/bigdata/java/Season;
            0      12     3  name   Ljava/lang/String;
    Signature: #42                          // (Ljava/lang/String;)V
  
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=5, locals=0, args_size=0
         0: new           #4                  // class com/bigdata/java/Season
         3: dup
         4: ldc           #8                  // String SPRING
         6: iconst_0
         7: ldc           #9                  // String 春
         9: invokespecial #10                 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
        12: putstatic     #11                 // Field SPRING:Lcom/bigdata/java/Season;
        15: new           #4                  // class com/bigdata/java/Season
        18: dup
        19: ldc           #12                 // String SUMMER
        21: iconst_1
        22: ldc           #13                 // String 夏
        24: invokespecial #10                 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
        27: putstatic     #14                 // Field SUMMER:Lcom/bigdata/java/Season;
        30: new           #4                  // class com/bigdata/java/Season
        33: dup
        34: ldc           #15                 // String AUTUMN
        36: iconst_2
        37: ldc           #16                 // String 秋
        39: invokespecial #10                 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
        42: putstatic     #17                 // Field AUTUMN:Lcom/bigdata/java/Season;
        45: new           #4                  // class com/bigdata/java/Season
        48: dup
        49: ldc           #18                 // String WINTTER
        51: iconst_3
        52: ldc           #19                 // String 冬
        54: invokespecial #10                 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
        57: putstatic     #20                 // Field WINTTER:Lcom/bigdata/java/Season;
        60: iconst_4
        61: anewarray     #4                  // class com/bigdata/java/Season
        64: dup
        65: iconst_0
        66: getstatic     #11                 // Field SPRING:Lcom/bigdata/java/Season;
        69: aastore
        70: dup
        71: iconst_1
        72: getstatic     #14                 // Field SUMMER:Lcom/bigdata/java/Season;
        75: aastore
        76: dup
        77: iconst_2
        78: getstatic     #17                 // Field AUTUMN:Lcom/bigdata/java/Season;
        81: aastore
        82: dup
        83: iconst_3
        84: getstatic     #20                 // Field WINTTER:Lcom/bigdata/java/Season;
        87: aastore
        88: putstatic     #1                  // Field $VALUES:[Lcom/bigdata/java/Season;
        91: return
      LineNumberTable:
        line 10: 0
        line 9: 60
}
Signature: #45                          // Ljava/lang/Enum<Lcom/bigdata/java/Season;>;
SourceFile: "Demo69.java"

可以发现它在静态块中总共完成了两件事情:

(1)实例化枚举类并赋值给枚举实例

(2)创建引用类型的数组,并为数组赋值,这也是values方法能够得到枚举数组的原因。

由于枚举类是继承自“java.lang.Enum”类的,所以它自动的就继承了该类的一些方法:

引用自“疯狂JAVA讲义 李刚”

3. 枚举类的成员变量,方法和构造器

枚举类也是一种类,只是它是一种比较特殊的类,因此它一样可定义成员变量,方法和构造器。还是上面的一段代码

public enum  Season {
    //枚举实例,必须要定义在第一行
   SPRING("春"),SUMMER("夏"),AUTUMN("秋"),WINTTER("冬");
    //定义了成员变量
   private String name;
   //定义了构造器,默认为private类型,无论是否显示修饰
   Season(String name) {
      this.name = name;
   }
}

实际上它底层自动完成了实例化工作,前面我们通过反编译也看到了这一点,它是在静态块中完成对象的实例化工作的。类似于这种:

public static final  SPRING=new SPRING("春");
public static final  SPRING=new SUMMER("夏");
public static final  SPRING=new AUTUMN("秋");
public static final  SPRING=new WINTTER("冬");

4. 枚举类实现接口

 枚举虽然无法继承其他类,但是还是可以实现其他接口的,这是因为接口没有单继承的局限性。枚举类在实现接口上和普通类的实现接口是一样的,没有什么本质区别。

定义接口:

public interface GenderDesc {
    void info();
}

枚举类实现该接口:

//实现接口,并且以内部类的形式
public enum Gender implements GenderDesc
{
    // public static final Gender MALE = new Gender("男");
    MALE("男") {
        public void info() {
            System.out.println("gender male information");
        }
    },
    FEMALE("女") {

        public void info() {
            System.out.println("gender female information");
        }
    };
    private String name;
    private Gender(String name) {
        this.name = name;
    }
}
public class enumTest {
	public static void main(String[] args) {
		// 通过valueof方法获取指定枚举类的值
		Gender gf = Enum.valueOf(Gender.class, "FEMALE");
		Gender gm = Enum.valueOf(Gender.class, "MALE");
		System.out.println(gf + " is stand for " + gf.getName());
		System.out.println(gm + " is stand for " + gm.getName());
		gf.info();
		gm.info();
	}
}
   Gender类在编译的时候,会生成三个类“Gender.class”,“Gender$1.class”和“Gender$2.class”,其中Gender$1和Gender$2都是表示匿名内部类。

注1:关于上面所讲的非抽象的枚举类,使用“final”修饰,抽象的枚举类,使用abstract修饰,这点可以通过“javap -c Gender.class”看到:

D:ProjectJUCDmoother	argetclassescomigdatajucenums>javap -c Gender.class
Compiled from "Gender.java"
//使用abstract修饰,因为尽管匿名内部实例MALE和FEMALE中实现了info方法,但是在Gender类中并没有实现info方法,所以它仍然会被认为是抽象的,除非显示的实现info方法
public abstract class com.bigdata.juc.enums.Gender extends java.lang.Enum<com.bigdata.juc.enums.Gender> implements com.bigdata.juc.enums.GenderDesc {
  public static final com.bigdata.juc.enums.Gender MALE;

  public static final com.bigdata.juc.enums.Gender FEMALE;

而“Gender$1.class”

D:ProjectJUCDmoother	argetclassescomigdatajucenums>javap -p Gender$1.class
Compiled from "Gender.java"
//使用final修饰   
final class com.bigdata.juc.enums.Gender$1 extends com.bigdata.juc.enums.Gender {
  com.bigdata.juc.enums.Gender$1(java.lang.String, int, java.lang.String);
  public void info();
}

注2:两个枚举值的方法表现出不同的行为,指的是它们在info的输出结果上不同,其他表现行为没有什么特殊的。

5. 包含抽象方法的枚举类

枚举类中定义抽象方法时,不能够使用abstract关键字将枚举定义为抽象类(因为系统会自动为它添加abstract关键字),但因为枚举类需要显示创建枚举值,所以定义每个枚举值时须为抽象方法提供实现,否则将出现编译异常。

enum Operation {
    PLUS {
        public double eval(double x, double y) {
            return x + y;
        }
    },
    MINS {
        public double eval(double x, double y) {
            return x - y;
        }
    },
    TIMES {
        public double eval(double x, double y) {
            return x * y;
        }
    },
    DIVIDE {
        public double eval(double x, double y) {
            if (y == 0) {
                return -1;
            }
            return x / y;
        }
    };
    //为枚举类定义抽象方法,具体由枚举值提供实现
    public abstract double eval(double x, double y);
}
public class OperationTest {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        System.out.println(Operation.PLUS.eval(1, 2));
        System.out.println(Operation.DIVIDE.eval(1, 0));
    }
}

反编译后的部分结果:

D:ProjectJUCDmoother	argetclassescomigdatajucenums>javap -c Operation.class
Compiled from "OperationTest.java"
//类名为abstract修饰    
abstract class com.bigdata.juc.enums.Operation extends java.lang.Enum<com.bigdata.juc.enums.Operation> {
  public static final com.bigdata.juc.enums.Operation PLUS;

  public static final com.bigdata.juc.enums.Operation MINS;

  public static final com.bigdata.juc.enums.Operation TIMES;

  public static final com.bigdata.juc.enums.Operation DIVIDE;

  public static com.bigdata.juc.enums.Operation[] values();
    Code:
       0: getstatic     #2                  // Field $VALUES:[Lcom/bigdata/juc/enums/Operation;
       3: invokevirtual #3                  // Method "[Lcom/bigdata/juc/enums/Operation;".clone:()Ljava/lang/Object;
       6: checkcast     #4                  // class "[Lcom/bigdata/juc/enums/Operation;"
       9: areturn

  public static com.bigdata.juc.enums.Operation valueOf(java.lang.String);
    Code:
       0: ldc           #5                  // class com/bigdata/juc/enums/Operation
       2: aload_0
       3: invokestatic  #6                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #5                  // class com/bigdata/juc/enums/Operation
       9: areturn

  public abstract double eval(double, double);

  com.bigdata.juc.enums.Operation(java.lang.String, int, com.bigdata.juc.enums.Operation$1);
    Code:
       0: aload_0
       1: aload_1
       2: iload_2
       3: invokespecial #1                  // Method "<init>":(Ljava/lang/String;I)V
       6: return

  static {};
    ……

6. 枚举的用途简介

在单例中,使用枚举

package com.bigdata.juc.singleton;
/*
 * 枚举类型:表示该类型的对象是有限的几个
 * 我们可以限定为一个,就成了单例
 */
enum EnumSingleton{
    //等价于 public static final INSTANCE=new EnumSingleton();
    INSTANCE
}
public class Singleton2 {
    public static void main(String[] args) {
        EnumSingleton s = EnumSingleton.INSTANCE;
        System.out.println(s);
    }
}

7. 关于枚举类中的values方法?

枚举的values(),既不是来自于在Java.lang.Enum,也不是来自于Enum所实现的接口,它是在编译过程中自己产生的,在前面的反编译过程中,我们也看到了这点,而且我们也看到它底层实际上就是生成了一个引用类型的数组(clone得到的),然后values方法返回了这个枚举数组。关于它的使用可以参考枚举类enum的values()方法

想要深入理解它的来源,可以参考该博客
http://blog.sina.com.cn/s/blog_6fd0fd4b01014x8l.html

8. 使用枚举实现单例设计

public class SingletonObject7 {
    private SingletonObject7() {

    }
    //定义枚举内部类
    private enum Singleton {
        INSTANCE;

        private final SingletonObject7 instance;

        Singleton() {
            instance = new SingletonObject7();
        }

        public SingletonObject7 getInstance() {
            return instance;
        }
    }

    public static SingletonObject7 getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    public static void main(String[] args) {
        IntStream.rangeClosed(1, 100)
                .forEach(i -> new Thread(String.valueOf(i)) {
                    @Override
                    public void run() {
                        System.out.println(SingletonObject7.getInstance());
                    }
                }.start());
    }
}

9. 枚举为什么能够防止反射攻击

观察如下实例:

public enum SingletonClass {
    INSTANCE;
    private String name;

    public void test() {
        System.out.println("The Test!");
    }

    public void setName(String name) {

        this.name = name;
    }

    public String getName() {

        return name;
    }

    public static void main(String[] args) {
        System.out.println(SingletonClass.INSTANCE);
        try {
            Class<SingletonClass> aClass = SingletonClass.class;
            Constructor<SingletonClass> constructor = aClass.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            constructor.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果:

INSTANCE
java.lang.NoSuchMethodException: com.bigdata.java.SingletonClass.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.bigdata.java.SingletonClass.main(SingletonClass.java:28)
  1. 为什么会报找不到“SingletonClass.<init>()”方法异常?
    为了解答这个问题,我们需要对该枚举类执行反编译,在反编译后能够看到这样一个构造方法“com.bigdata.java.SingletonClass(Ljava/lang/String;I)V”
 private com.bigdata.java.SingletonClass();
    descriptor: (Ljava/lang/String;I)V
    flags: ACC_PRIVATE
    Code:
      stack=3, locals=3, args_size=3
         0: aload_0
         1: aload_1
         2: iload_2
         3: invokespecial #6                  // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V
         6: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/bigdata/java/SingletonClass;
    Signature: #39                          // ()V

descriptor: (Ljava/lang/String;I)V,这说明SingletonClass构造方法的第一个参数为String类型,第二个参数为int类型,而它最终调用了父类中的构造方法:java/lang/Enum."<init>":(Ljava/lang/String;I)V

也即:java.lang.Enum#Enum

 /**单独的构造方法。程序员无法调用此构造方法。该构造方法用于由响应枚举类型声明的编译器发出的代码。
     * Sole constructor.  Programmers cannot invoke this constructor.
     * It is for use by code emitted by the compiler in response to
     * enum type declarations.
     *
     * @param name - The name of this enum constant, which is the identifier
     *               used to declare it.
                     此枚举常量的名称,它是用来声明该常量的标识符。

     * @param ordinal - The ordinal of this enumeration constant (its position
     *         in the enum declaration, where the initial constant is assigned
     *         an ordinal of zero).
                      枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零)。
     */
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

那么“com.bigdata.java.SingletonClass(Ljava/lang/String;I)V”何时被调用的呢?再看反编译后的static块:

static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
          
         0: new           #4                  // class com/bigdata/java/SingletonClass
             //创建SingletonClass实例并放置到操作数栈的栈顶
         3: dup
         //复制操作数栈顶的值,并将复制后的值压入到操作数栈的栈顶,此时栈中有两个SingletonClass类的对象
         4: ldc           #19                 // String INSTANCE
         //将“INSTANCE”从运行时常量池中取出,并放置到操作数栈的栈顶,此时栈中有三个元素
         6: iconst_0
         //将0压入到操作数栈的栈顶,此时栈中有四个元素
         7: invokespecial #20                 // Method "<init>":(Ljava/lang/String;I)V
         //调用该类的构造方法"<init>":(Ljava/lang/String;I)V,在调用构造方法的时候,依次从栈顶弹出0,INSTANCE,实例,然后调用该实例的构造方法,传入参数“INSTANCE”和“0”,此时操作数栈的栈顶只有一个元素
        10: putstatic     #11                 // Field INSTANCE:Lcom/bigdata/java/SingletonClass;
         //从操作数栈顶弹出该类的引用,然后赋值给类中的静态字段INSTANCE
        13: iconst_1
        //将1压入到操作数栈的栈顶,此时操作数栈中有1个元素
        14: anewarray     #4                  // class com/bigdata/java/SingletonClass
        //创建引用类型的数组,将栈顶元素弹出,作为创建的数组的长度,也即数组的长度为1,然后将数组的引用arrayref(为了叙述方便,使用arrayref代替)压入到操作数栈中
        17: dup
        //复制栈顶的值,并压入到操作数栈中,此时栈中有两个元素
        18: iconst_0
        //将0压入到操作数栈中,此时栈中有三个元素
        19: getstatic     #11                 // Field INSTANCE:Lcom/bigdata/java/SingletonClass;
        //获取静态字段INSTANCE的值,并压入到操作数栈中,此时栈中有四个元素
        22: aastore
        //从操作数栈读取一个reference类型数据存入到数组中,即依次弹出INSTANCE,0,arrayref,然后将INSTANCE放入到数组arrayref的0号位置。然后将arrayref压入到操作数栈的栈顶,此时栈中有两个元素,都是arrayref。
        23: putstatic     #1                  // Field $VALUES:[Lcom/bigdata/java/SingletonClass;
         //从操作数栈顶弹出该类的引用,然后赋值给类中的静态字段$VALUES,此时操作数栈中只有一个元素arrayref了
        26: return
         //方法返回void,此时操作数栈中的所有元素都会被丢弃   
      LineNumberTable:
        line 8: 0
        line 7: 13

通过以上的分析,可以看到在static块中,完成了以下任务

  • 创建SingletonClass的实例
  • 为类中的静态字段INSTANCE赋值
  • 创建数组并赋值给静态字段$VALUES

通过static块,能够看到在实例化类的时候,调用的是Method "<init>":(Ljava/lang/String;I)V 方法。再回到上面的问题中,由于没有生成无参的构造方法,只有这么一个构造方法,所以会给出“java.lang.NoSuchMethodException: com.bigdata.java.SingletonClass.()”异常信息。

既然没有无参构造方法,那么我们能否通过调用Method "<init>":(Ljava/lang/String;I)V 构造方法来进行实例化呢?

如果这样来修改main方法

    public static void main(String[] args) {
        System.out.println(SingletonClass.INSTANCE);
        try {
            Class<SingletonClass> aClass = SingletonClass.class;
            Constructor<SingletonClass> constructor = aClass.getDeclaredConstructor(java.lang.String.class,int.class);
            constructor.newInstance("instance2",1);
        } catch (Exception e) {
           e.printStackTrace();
        }
    }

运行结果:

INSTANCE
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.bigdata.java.SingletonClass.main(SingletonClass.java:30)

为什么会报这个异常,查看newInstance源码即可:

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
{
       ...
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
       ...
 } 

可以看到异常是上面代码抛出的,说明它不能够对于枚举类型进行反射创建枚举类的实例,也即API层面就限制住了不能通过反射实例化枚举实例。

10. 建议使用“Enum.getDeclaringClass”,而不是“Object.getClass”来获取枚举类的Class实例

枚举类实现了Comparable接口,实现了compareTo方法:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
/**将此枚举与指定的order对象进行比较。返回一个负整数、零或正整数,因为该对象小于、等于或大于指定的对象
     * Compares this enum with the specified object for order.  Returns a
     * negative integer, zero, or a positive integer as this object is less
     * than, equal to, or greater than the specified object.
     *枚举常数只能与同一枚举类型的其他枚举常数相比较。此方法实现的自然顺序与声明常量的顺序相同。
     * Enum constants are only comparable to other enum constants of the
     * same enum type.  The natural order implemented by this
     * method is the order in which the constants are declared.
     */
    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;
    }

注意这个self.getClass() != other.getClass() && self.getDeclaringClass() != other.getDeclaringClass()

在这里你一定会问,为什么还要进行getDeclaringClass()的比较,直接比较两个枚举常量的class对象类型是否相同不就行了吗?

观察如下的实例:

在使用枚举类的时候,建议用getDeclaringClass返回枚举类。但是为什么不用getClass呢?下面来看看代码:

public enum FruitEnum{
    BANANA,APPLE;

    public static void main(String[] args) {
        System.out.println(BANANA.getDeclaringClass());
        System.out.println(BANANA.getClass());
    }
}

# 运行结果
    class FruitEnum
    class FruitEnum
}

有人说结果不是一样吗?不急,看下面这种情况。

public enum FruitEnum{
    BANANA{
        String getName() {
            return "香蕉";
        }
    },APPLE{
        String getName() {
            return "苹果";
        }
    };

    abstract String getName();

    public static void main(String[] args) {
        System.out.println(BANANA.getDeclaringClass());
        System.out.println(BANANA.getClass());
    }
}

# 运行结果
class FruitEnum
class FruitEnum$1

这种情况下就不同了。因为此时BANANA和APPLE相当于FruitEnum的内部类。下面来看看Enum的源码:

public final Class<E> getDeclaringClass() {
    Class var1 = this.getClass();
    Class var2 = var1.getSuperclass(); // 获取上一级的父类
    return var2 == Enum.class?var1:var2;
}

当上一级的父类不是Enum,则返回上一级的class。因此对枚举类进行比较的时候,使用getDeclaringClass是万无一失的。
————————————————
版权声明:本文为CSDN博主「DaJian35」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/DaJian35/article/details/79705193

再来看getDeclaringClass方法:

   /**返回与该enum常量的enum类型对应的类对象。当且仅当e1.getingclass () == e2.getingclass()时,两个enum常量e1和e2具有相同的enum类型。(此方法返回的值可能与对象返回的值不同。使用特定于常量的类主体的枚举常量的getClass方法。)
     * Returns the Class object corresponding to this enum constant's
     * enum type.  Two enum constants e1 and  e2 are of the
     * same enum type if and only if
     *   e1.getDeclaringClass() == e2.getDeclaringClass().
     * (The value returned by this method may differ from the one returned
     * by the {@link Object#getClass} method for enum constants with
     * constant-specific class bodies.)
     *
     * @return the Class object corresponding to this enum constant's enum type 对应于enum常量的类对象枚举类型
     */
    @SuppressWarnings("unchecked")
    public final Class<E> getDeclaringClass() {
        Class<?> clazz = getClass();
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }

所以当枚举常量是普通的常量时,“clazz.getSuperclass()”获得得到的是“Enum.class”,此时返回的是就是这个枚举常量对应的Class实例。但是若该枚举常量是以匿名内部类的形式出现,则“clazz.getSuperclass()”返回的就是该枚举常量对应的Class实例,则返回的是“clazz.getSuperclass()”的计算结果。

参考链接:

java设计模式之单例模式(枚举、静态内部类)

深入理解Java枚举类型(enum)

原文地址:https://www.cnblogs.com/cosmos-wong/p/11919418.html