java 枚举类

2021/02/08 Java初级进阶 共 20237 字,约 58 分钟
闷骚的程序员

1. 枚举类介绍

1.1 什么是枚举类?

  1. 枚举类,是一种特殊的类。
  2. java中使用enum定义枚举类;就像使用class定义普通类一样;就像使用interface定义接口一样;就像使用@interface定义注解一样。
  3. 枚举类是jdk 1.5之后引入的新的引用类型;和class、interface、array等一样,都是用来定义新类型的。
  4. 枚举类定以后,java编译器默认让该类继承了java.lang.Enum类;就像java编译器默认让一个普通的class继承Object类一样;基于此,意味着任何一个枚举类不能再继承其他类。
  5. 同时,java编译器默认对任何一个枚举类都修饰为final,意味着任何一个枚举类是不能被其他类继承的。
  6. 枚举类的构造方法默认是private的,即构造方法私有化;可以不显式编写,编译器自动为我们修饰为private的;如果要显式编写构造器的权限符,也只能编写为private的,否则编译器报错。
  7. 因为枚举类的构造方法必须是private的,所以它的作用就很明显了;即限制外部去构造枚举实例,而是交由枚举类自己去负责实例化自己的枚举对象;枚举对象也叫做枚举常量。
  8. 枚举类建议一定要实例化枚举对象;尽管枚举类可以什么都不编写,但是如果连枚举对象都没有,枚举将没有任何意义;如果声明枚举对象的话,则枚举对象的实例化声明必须在枚举类的首行,不能放在其他位置;如果是无参构造则只能通过无参构造创建实例;如果显式编写了有参构造,则可通过有参构造创建对应枚举实例;多个枚举对象通过逗号分隔,如果后续还有其他内容,则枚举对象结尾使用分号。
  9. 枚举实例对象不能显式添加任何修饰符,java编译器编译后为自动为任何一个枚举实例对象添加修饰符 public static final ,意味着任何一个枚举实例对象实际上就是一个枚举常量;因此建议枚举对象命名全部为大写,即符合常量的命名方式。
  10. 枚举类也可以定义自己的成员变量,定义方式和普通的class完全一致。
  11. 枚举类也允许像普通类一样定义自己的静态块和构造块;静态块随着枚举类的加载只执行一次,构造块的调用随着JVM自动调用枚举类的构造方法一起执行的。

1.2 通过案例验证上述规则

定义枚举类EnumType

package zeh.test.demo.com.enum1;

// 2021-04-09
public enum EnumType {
    // 枚举对象通过指定构造方法去创建,只能声明在枚举类的首行
    // 如果编写在枚举类其他地方,编译器将报错
    // 实例化枚举对象时不能加入任何其他修饰符
    // 多个枚举对象通过逗号分隔,最后使用分号结尾
    ZHANGSAN("zhangsan", 22), LISI("lisi", 18);
    // 枚举类也可以定义自己的成员变量
    private String name;
    private int age;
    private static String school;
    private final String city = "西北工业大学";
    {
        // 定义枚举类自己的构造块
        System.out.println("枚举类自己的构造块");
    }
    static{
        System.out.println("枚举类自己的静态块");
    }
    // 显示编写空的枚举构造
    EnumType() {
    }
    // 枚举类的构造器默认是private的,可以不显式编写;如果执意要显式编写,也必须是private的,否则编译报错
    private EnumType(String name, int age) {
        this.setName(name);
        this.setAge(age);
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public void testMyEnum() {
        System.out.println("枚举类自己提供的业务方法");
    }
}

主程序

package zeh.test.demo.com.enum1;

// 2021-04-09
public class EnumMain {
    public static void main(String[] args) {
        // 反射获取枚举类 EnumType 的父类,结果是 Enum
        Class<?> clazz = EnumType.class.getSuperclass();
        System.out.println(clazz);
        // 反射获取 Enum 类的父类,结果是 Object
        clazz = Enum.class.getSuperclass();
        System.out.println(clazz);
        // 使用values()方法获取枚举类的所有枚举常量
        EnumType[] enumTypes = EnumType.values();
        for(EnumType enumType : enumTypes){
            enumType.testMyEnum();
            System.out.println(enumType);
        }
        EnumType enumType = EnumType.valueOf("ZHANGSAN");
        System.out.println(enumType);
    }
}

执行结果

class java.lang.Enum
class java.lang.Object
枚举类自己的构造块
枚举类自己的构造块
枚举类自己的静态块
枚举类自己提供的业务方法
ZHANGSAN
枚举类自己提供的业务方法
LISI
ZHANGSAN
Process finished with exit code 0

使用javap -v 反汇编EnumType.class

public final class zeh.test.demo.com.enum1.EnumType extends java.lang.Enum<zeh.test.demo.com.enum1.EnumType>
public static final zeh.test.demo.com.enum1.EnumType ZHANGSAN; //枚举常量
public static final zeh.test.demo.com.enum1.EnumType LISI; //枚举常量
private java.lang.String name;
private int age;
private static java.lang.String school;
private final java.lang.String city;
private static final zeh.test.demo.com.enum1.EnumType[] $VALUES; //枚举常量对象对应的属性数组
public static zeh.test.demo.com.enum1.EnumType[] values(); //枚举类的values方法
public static zeh.test.demo.com.enum1.EnumType valueOf(java.lang.String); //枚举类的valueOf方法
private zeh.test.demo.com.enum1.EnumType(); // 私有的空构造
private zeh.test.demo.com.enum1.EnumType(java.lang.String, int); // 私有的有参构造
public java.lang.String getName(); // 自己定义的业务方法
public void setName(java.lang.String); // 自己定义的业务方法
public int getAge(); // 自己定义的业务方法
public void setAge(int); // 自己定义的业务方法
public void testMyEnum(); // 自己定义的业务方法
static {}; // 静态块

使用第三方工具jad反编译EnumType.class
javap是jdk自带的一款反编译工具,但是它反编译后的文件不是很友好,我们一般使用它去反汇编(javap同时提供了反汇编即javap -c,或者javap -v),能够看到编译器传送到JVM的原始汇编指令,更好的了解JVM指令的底层。
因此,我们需要借助第三方工具jad去反编译字节码文件,jad反编译后的源文件是最接近原始指令的源文件,并且很友好(类似的jd反编译出来的源文件就不接近底层)。
使用jad反编译后的文件如下:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name:   EnumType.java
package zeh.test.demo.com.enum1;
import java.io.PrintStream;
// java编译器编译后的枚举类自动继承了Enum抽象类
public final class EnumType extends Enum
{
    // 编译器生成了values()方法,通过克隆枚举对象数组返回所有枚举常量
    public static EnumType[] values()
    {
        return (EnumType[])$VALUES.clone();
    }
    // 编译器生成了valueOf(String)方法,底层实际上调用的就是Enum类的valueOf,只不过第一个参数写死了。
    public static EnumType valueOf(String name)
    {
        return (EnumType)Enum.valueOf(zeh/test/demo/com/enum1/EnumType, name);
    }
    // 编译后的枚举的空构造方法实际上会接收两个参数,第一个参数表示该枚举常量的字面量,第二个参数表示该枚举常量的定义序号(序号从0开始)
    // 实际上当前枚举类的构造方法首先调用了父类Enum类的构造
    // 并且编译器优化了构造块,构造块的逻辑会合并到所有的构造方法中去。
    private EnumType(String s, int i)
    {
        super(s, i);
        System.out.println("\u679A\u4E3E\u7C7B\u81EA\u5DF1\u7684\u6784\u9020\u5757");
    }
    // 同理,对于有参构造,原理同上;在自己显式编写的构造参数前默认加上name和ordinal参数。
    private EnumType(String s, int i, String name, int age)
    {
        super(s, i);
        System.out.println("\u679A\u4E3E\u7C7B\u81EA\u5DF1\u7684\u6784\u9020\u5757");
        setName(name);
        setAge(age);
    }
    public String getName()
    {
        return name;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    public int getAge()
    {
        return age;
    }
    public void setAge(int age)
    {
        this.age = age;
    }
    public void testMyEnum()
    {
        System.out.println("\u679A\u4E3E\u7C7B\u81EA\u5DF1\u63D0\u4F9B\u7684\u4E1A\u52A1\u65B9\u6CD5");
    }
    public static final EnumType ZHANGSAN;
    public static final EnumType LISI;
    private String name;
    private int age;
    private static String school;
    private final String city = "\u897F\u5317\u5DE5\u4E1A\u5927\u5B66";
    // 编译器默认生成一个当前枚举类型的数组,用来缓存当前枚举类的所有枚举常量
    private static final EnumType $VALUES[];
    // 编译器生成了静态块,用来按照声明顺序(顺序从0开始)实例化当前枚举对象和枚举对象数组 $VALUES
    // 并且编译器对静态块做了优化,将所有的静态块逻辑合并到同一个静态块中执行
    static
    {
        ZHANGSAN = new EnumType("ZHANGSAN", 0, "zhangsan", 22);
        LISI = new EnumType("LISI", 1, "lisi", 18);
        $VALUES = (new EnumType[] {
            ZHANGSAN, LISI
        });
        System.out.println("\u679A\u4E3E\u7C7B\u81EA\u5DF1\u7684\u9759\u6001\u5757");
    }
}

结果分析

  1. 枚举类编译后继承了父类 Enum,则不能再继承其他类(因为java是单继承);Enum类继承Object类。
  2. 枚举类编译后使用final修饰,即枚举类不能被其他类继承。
  3. 枚举对象编译后使用了public static final修饰,即所有枚举对象都是常量。
  4. 枚举类编译后提供了static方法 values(),返回一个当前枚举类型数组,表示当前枚举类的所有枚举对象;这个方法是java编译器为当前枚举类自动生成的,不是继承Enum类的;该方法通过克隆枚举对象数据$VALUES而来。
  5. 枚举类编译后提供了static方法 valueOf(),返回一个当前枚举类的指定枚举对象;这个方法是java编译器为当前枚举类自动生成的,不是继承Enum类的,但实际上调用的依旧是Enum中的valueOf()方法。
  6. 枚举对象的产生实际上并不高深,是java编译器直接为我们生成了一个静态块,然后调用枚举类的私有构造器产生的枚举常量(java编译器会合并所有的静态块逻辑到一个静态块中)。
  7. 编译后构造块实际上是合并到所有的构造方法中去执行的,因此构造块就是一个语法糖而已。
  8. 编译器优化了当前枚举类的构造方法,每个构造方法都调用了父类Enum中的构造器,并且Enum中的构造器本身是protected的,它只能由编译器调用。
  9. 至此,应该明白,实际上枚举并不高深,只不过java编译器替我们做了很多工作而已。

1.3 枚举和单例

我们先来回顾下单例模式:

  1. 单例模式中,构造方法必须是显式编写为private的,这样才能阻止外部实例化该对象。
  2. 单例类自己负责实例化对象,因此需要定义一个当前类自己的成员变量,该成员必须是static的,只有这样才能随着目标类加载一次;一般建议定义为private的,防止外部直接访问成员;专门提供一个公共的方法返回该成员。
    上面就是单例模式的基本要求,俗称“双私一公”;
    是不是发现,枚举和单例的要求很像。
    枚举实际上比单例模式具有更丰富的功能,它是java本身提供给我们的更加简约的一种单例应用。
    所以,我们可以认为,枚举就是单例模式简单化。
    也就是说,在java的发展中,逐渐发现单例模式用的很多,因此jdk直接提供了枚举:
    构造方法私有化–》单例模式/多例模式;
    单例模式/多例模式–》枚举类。

2. 探究枚举类的底层

2.1 如何获取枚举对象?

前面通过javap -v反汇编class字节码后,知道java编译器默认为每个枚举类提供了两个方法:

public static zeh.test.demo.com.enum1.EnumType[] values(); 
public static zeh.test.demo.com.enum1.EnumType valueOf(java.lang.String);

这两个方法都是用来获取枚举类的枚举常量的。
并且枚举类中定义的枚举对象默认被修饰成了public static final;因此可以直接通过当前枚举类获取对应的枚举常量。
其次,我们知道任何一个枚举类编译后都默认继承Enum类,我们在Enum的API中同样找到了一个方法(该方法是static,因此可以被子类继承但是不能覆盖):

public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name);

最后,观察Class类提供了一个 public T[] getEnumConstants()方法,这个方法可以返回当前枚举类的所有枚举常量。
所以,现在知道了获取一个目标枚举对象共有5种常见方式:
(1) 枚举类.枚举常量 直接获取;
(2) 通过当前枚举类中的values()方法获取所有枚举常量,然后遍历;
(3) 通过当前枚举类中的valueOf()方法获取指定名称的枚举常量;
(4) 通过Enum.valueOf()方法来获取指定枚举类型的指定名称的枚举常量。
(5) 通过Class对象的getEnumConstants()来反射获取枚举常量。
下面我们通过案例来验证前4种方式,反射获取枚举常量的方式后面再学习(EnumType枚举类和上面案例中保持一样,只修改主程序):

package zeh.test.demo.com.enum1;

// 获取目标枚举对象的方式
// 2021-04-09
public class EnumMain {
    public static void main(String[] args) {
        // 方式1:枚举类.枚举常量 直接获取
        EnumType lisi = EnumType.LISI;
        // 通过获取后的枚举对象调用其方法
        lisi.testMyEnum();
        System.out.println("lisi is :" + lisi);
        // 方式2:通过当前枚举类自带的values方法返回所有枚举常量进行遍历
        EnumType[] enumTypes = EnumType.values();
        for(EnumType enumType : enumTypes){
            // 通过获取后的枚举对象调用其方法
            enumType.testMyEnum();
            System.out.println("enumType is :" + enumType);
        }
        // 方式3:通过当前枚举类自带的valueOf方法获取当前枚举类指定名称的枚举常量
        EnumType zhangsan = EnumType.valueOf("ZHANGSAN");
        // 通过获取后的枚举对象调用其方法
        zhangsan.testMyEnum();
        System.out.println("zhangsan is :" + zhangsan);
        // 方式4:通过Enum类提供的valueOf(Class,String)方法获取指定枚举类中的指定名称的枚举对象
        EnumType enumType = Enum.valueOf(EnumType.class,"LISI");
        enumType.testMyEnum();
        System.out.println("enumType is :" + enumType);
        // 因此valueOf(Class,String)方法是Enum类的且是public static的,因此子类EnumType也继承了它,所以可以直接使用(static方法可以被继承但是不能被覆盖)
        enumType = EnumType.valueOf(EnumType.class,"ZHANGSAN");
        enumType.testMyEnum();
        System.out.println("enumType is :" + enumType);
    }
}

2.2 枚举常量

我们知道,枚举对象实际上就是枚举常量,因为编译器默认将它们优化为public static final。
接下来我们深入了解下枚举常量:

  1. 枚举常量本质是当前枚举类的public、static、final的实例化对象;因为它是final的,因此编译器禁止重置它的引用值;因为它是static的,因此随着JVM加载当前枚举类,它会首先被初始化到方法区的静态区中(静静非构构初始化方法);
  2. 直接输出枚举常量,输出的是枚举常量的字面量字符串,而不是枚举对象的引用地址。因为枚举类继承了Enum类,而Enum覆盖了toString()方法,覆盖后的toString方法返回的就是对应枚举常量的字面量值;
  3. 在声明枚举常量时,实际上调用的是当前枚举类对应的构造方法。如果是有参构造,则枚举常量声明时需要传递实际参数。

2.3 Enum类提供的API

前面已经清楚,任何一个使用enum定义的枚举类,在编译后默认继承Enum类。
那么,任何一个枚举对象都可以使用Enum提供的方法。

  1. Enum类中提供的方法大多数都是final的,意味着这些方法不允许子类去覆盖。
  2. Enum提供的clone方法直接抛出异常并且是protected,这意味着哪怕使用反射去调用clone方法,也无法创建一个枚举对象的副本,保证了枚举对象在整个JVM中的单例特性。
  3. Enum提供的finalize方法是final的,意味着不允许覆盖;且默认是空实现,说明枚举类不能有finalize方法。
  4. Enum是抽象类。
  5. Enum本身有构造方法 protected Enum(String name, int ordinal);该方法是protected的,只能由java编译器调用。
  6. Enum类本身实现了Comparable 和 Serializable接口。

下面通过案例测试下Enum类的API:

package zeh.test.demo.com.enum1;

// 获取目标枚举对象的方式
// 2021-04-09
public class EnumMain {
    public static void main(String[] args) {
        EnumType enumType = EnumType.LISI;
        // 比较两个枚举对象;如果当前枚举对象大于目标枚举对象,则返回1;等于则返回0;小于则返回-1。比较的顺序就是枚举类中声明枚举常量的顺序。
        int result = enumType.compareTo(EnumType.ZHANGSAN);
        System.out.println(result);
        // 判断两个枚举对象是否是同一个对象,即引用地址是否相同
        boolean isEqual = enumType.equals(EnumType.LISI);
        System.out.println(isEqual);
        // 返回描述此枚举对象的class对象
        Class<EnumType> enumTypeClass = enumType.getDeclaringClass();
        System.out.println(enumTypeClass);
        // 返回此枚举常量的hashCode
        int hashCode = enumType.hashCode();
        System.out.println(hashCode);
        // 返回当前枚举常量的名称,就是枚举常量的字面量含义。其实Enum覆盖的toString方法也是返回的枚举中的name
        String name = enumType.name();
        System.out.println(name);
        name = enumType.toString();
        System.out.println(name);
        // 返回当前枚举常量的序数,即当前枚举常量在枚举中的声明位置,序数从0开始
        int ordinal = enumType.ordinal();
        System.out.println(ordinal);
        // Enum提供的静态方法,返回指定枚举类型中的指定名称的枚举常量
        EnumType zhangsan = Enum.valueOf(EnumType.class, "ZHANGSAN");
        System.out.println(zhangsan);
    }
}

3. 枚举类常见注意点

3.1 equals方法

  1. 任何一个枚举类都继承Enum类的equals(),Enum类对该方法的实现就是直接使用==比较两个枚举对象的引用是否相同;因此通过枚举常量调用equals()方法时一定要注意它比较的是地址,即使用Enum的equals()方法和直接使用==是完全相同的。
  2. 任何一个字符串覆盖的equals()方法,是比较两个字符串的内容是否相同的;因此如果使用字符串的equals()去和一个枚举常量进行比较结果返回的是false。
  3. 在枚举的使用中,我们很少直接使用枚举的equals去比较两个枚举对象是否相同;而是直接通过枚举对象获取其中的属性,再去比较属性是否相同(因为枚举的属性大多数都是基本数据类型和String)。
    备注:
    因为枚举常量本身是常量,而且直接输出的话返回的是该枚举常量的字面量字符串;所以很多时候我们会潜意识的认为枚举常量本质是个字符串,从而使用枚举常量的equals()方法区比较,这是错误的!
    枚举常量直接toString后返回的是其字面量字符串,但它本质仍旧是个java对象,并不是字符串。
    下面通过案例去测试:
    ```java package zeh.test.demo.com.enum1;

// 获取目标枚举对象的方式 // 2021-04-09 public class EnumMain { public static void main(String[] args) { EnumType enumType = EnumType.LISI; // 比较两个枚举对象,返回true boolean result = enumType.equals(EnumType.LISI); System.out.println(result); // 使用==和使用枚举的equals()方法作用完全相同,返回true result = enumType == EnumType.LISI; System.out.println(result); // 使用字符串的equals比较枚举对象,返回false result = “LISI”.equals(enumType); System.out.println(result); // 获取枚举对象的name属性值,然后再比较,返回true result = “lisi”.equals(enumType.getName()); System.out.println(result); } }


## 3.2 编译器为枚举类生成的valueOf方法和values方法
1.  valueOf(String)方法和values()方法是编译器为枚举类自动生成的方法;这两个方法是static的。  
2.  因为是static的,所以由当前枚举类名直接调用。  
3.  当前枚举类向上转型为Enum后,无法调用这两个方法,因为Enum类中没有这两个方法。  
下面通过案例验证:  
```java
package zeh.test.demo.com.enum1;

// 获取目标枚举对象的方式
// 2021-04-09
public class EnumMain {
    public static void main(String[] args) {
        EnumType enumType = EnumType.LISI;
        Enum e = enumType;
        // 向上转型后无法调用values()方法和valueOf()方法(valueOf方法是一个参数的那个方法);因为Enum类中并没有这两个方法
        e.values();
        e.valueOf();
    }
}

3.3 通过反射也可以获取枚举类的所有枚举对象

Class类中提供了两个枚举相关的方法:
public boolean isEnum():返回当前class对象是否表示的是枚举类型。
public T[] getEnumConstants():如果当前class表示枚举类型,则返回枚举类的所有枚举常量,否则返回null。
所以,我们也可以通过class对象来获取枚举类的所有枚举对象。
下面通过案例验证:

package zeh.test.demo.com.enum1;
import java.util.Arrays;

// 获取目标枚举对象的方式
// 2021-04-09
public class EnumMain {
    public static void main(String[] args) {
        Class>?< clazz = EnumType.class;
        if(clazz.isEnum()){
            EnumType[] enumTypes = (EnumType[])clazz.getEnumConstants();
            System.out.println("enumTypes is :" + Arrays.toString(enumTypes));
        }
    }
}

4. 枚举对象的单例特性

4.1 枚举对象不能被克隆

前面说明过,Enum类中的clone方法是final、protected的,而且实现是直接抛出了异常。这意味着一个枚举对象无法clone出另外一个枚举对象。
案例

package zeh.test.demo.com.enum1;

public enum EnumType {
    ZHOUJIELUN, WANGLIHONG, LINJUNJIE;
    public static void main(String[] args) throws CloneNotSupportedException {
        // Enum类中的clone()方法是protected的,因此只能由Enum的子类调用;其直接抛出异常,表示枚举对象不支持clone
        ZHOUJIELUN.clone();
    }
}

测试结果

Exception in thread "main" java.lang.CloneNotSupportedException
    at java.lang.Enum.clone(Enum.java:163)
    at zeh.test.demo.com.enum1.EnumType.main(EnumType.java:14)
Process finished with exit code 1

4.2 不能反射出一个枚举对象

通过反射创建枚举对象,直接抛出异常。即Class的newInstance()方法不允许反射枚举对象。
下面通过案例演示:
定义枚举类EnumType

package zeh.test.demo.com.enum1;

public enum EnumType {
    ZHANGSAN("zhangsan", 22), LISI("lisi", 18);
    private String name;
    private int age;
    // 枚举空构造,编译后编译器默认为编译为两个参数的构造即:EnumType(String name,int ordinal)
    EnumType() {
    }
    // 枚举两参构造,编译后将成为:EnumType(String name,int ordinal,String name, int age)
    EnumType(String name, int age) {
        this.setName(name);
        this.setAge(age);
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

通过Class的newInstance()方法反射创建枚举对象

package zeh.test.demo.com.enum1;

// 获取目标枚举对象的方式
public class EnumMain {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        Class<?> clazz = EnumType.class;
        // 方式1:直接使用Class类的newInstance()方法去创建当前类的对象;该方法要求当前类必须具备public的无参构造方法
        // 但是我们知道枚举类编译后,其实是没有无参构造的,哪怕有也是private的,因此该方法始终报错,即找不到指定的构造方法
        clazz.newInstance();
    }
}

测试结果:

Exception in thread "main" java.lang.InstantiationException: zeh.test.demo.com.enum1.EnumType
    at java.lang.Class.newInstance(Class.java:427)
    at zeh.test.demo.com.enum1.EnumMain.main(EnumMain.java:14)
Caused by: java.lang.NoSuchMethodException: zeh.test.demo.com.enum1.EnumType.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.newInstance(Class.java:412)
    ... 1 more
Process finished with exit code 1

原因分析:
枚举类编译后实际上并没有无参构造方法,因此Class类的newInstance()方法永远无法创建枚举对象。

通过Constructor反射指定参数的构造器去创建枚举对象

package zeh.test.demo.com.enum1;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

// 获取目标枚举对象的方式
public class EnumMain {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        Class<?> clazz = EnumType.class;
        // 方式2:只能使用Constructor的newInstance()方法去获取指定参数的构造器。现在获取EnumType中两参构造器,所以得传入4个参数。
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class, String.class, int.class);
        constructor.setAccessible(true);
        EnumType enumType = (EnumType) constructor.newInstance("zhangsan", 12);
        System.out.println(enumType);
    }
}

测试结果:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at zeh.test.demo.com.enum1.EnumMain.main(EnumMain.java:18)
Process finished with exit code 1

原因分析:
我们可以观察Class类提供的newInstance()方法的源代码:

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    throw new IllegalArgumentException("Cannot reflectively create enum objects");

也就是说,源码内部限制了去反射创建枚举对象。

4.3 枚举对象不允许序列化和反序列化

5. 像使用普通class一样使用枚举

通过前面知道:我们通过enum关键字定义的一个枚举类,java编译器优化后会继承Enum类、使构造方法私有并且默认调用Enum的构造器、通过静态块初始化枚举实例等。
因此,除了这些区别之外,枚举类实际上和普通类没有其他区别了。
因此我们可以把枚举类当做普通类,实现接口、添加内部类、添加成员和方法、甚至是添加main方法。

5.1 向枚举中添加普通成员和方法等

EnumType

package zeh.test.demo.com.enum1;

public enum EnumType {
    ZHANGSAN("zhangsan", 22), LISI("lisi", 18);
    private String name;
    private int age;
    public static String school;
    private final String city = "西北工业大学";
    {
        System.out.println("枚举类自己的构造块");
    }
    static {
        System.out.println("枚举类自己的静态块");
    }
    EnumType(String name, int age) {
        this.setName(name);
        this.setAge(age);
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public void testMyEnum() {
        System.out.println("枚举类自己提供的业务方法,city = " + this.city + ";school = " + EnumType.school);
    }
    public static void main(String[] args) {
        EnumType enumTypes[] = EnumType.values();
        System.out.println("enumTypes is :" + Arrays.toString(enumTypes));
        EnumType.school = "西南石油大学";
        EnumType enumType = EnumType.valueOf("ZHANGSAN");
        enumType.testMyEnum();
    }
}

执行结果

枚举类自己的构造块
枚举类自己的构造块
枚举类自己的静态块
enumTypes is :[ZHANGSAN, LISI]
枚举类自己提供的业务方法,city = 西北工业大学;school = 西南石油大学
Process finished with exit code 0

5.2 枚举类覆盖方法

既然枚举类继承Enum类,那么枚举类就可以覆盖Enum中的方法。
很遗憾,Enum类中的实例方法只有一个不是final的,那就是toString()方法。
言外之意,允许我们自定义枚举对象的输出格式。

5.3 枚举类中定义抽象方法

  1. 普通类定义了抽象方法后,则该类必须使用abstract进行修饰;并且需要子类去实现该抽象方法。
  2. 枚举和普通类类似,也可以定义抽象方法,只不过枚举对象只能由自己产生,因此枚举的抽象方法需要枚举类自己负责实现;既然必须枚举自己负责实现抽象方法,那么枚举类势必不能再使用abstract去修饰。
  3. 枚举内部定义抽象方法,表现出了一定的多态特性。
    ```java package zeh.test.demo.com.enum1;

public enum EnumType { TEST1 { @Override public void testEnum() { System.out.println(“TEST1的实现”); } }, TEST2 { @Override public void testEnum() { System.out.println(“TEST2的实现”); } }; public abstract void testEnum(); // 枚举类中直接定义main方法 public static void main(String[] args) { TEST1.testEnum(); TEST2.testEnum(); } }

## 5.4 枚举和接口
1.  枚举类只是不能继承其他类,但它可以实现多个接口。  
2.  一般通过接口来组织多种类型的枚举,使得枚举看起来优雅。  

枚举实现多个接口  
```java
package zeh.test.demo.com.enum1;

interface IEat {
    void eat();
}
interface IDrink {
    void drink();
}
public enum MyEnum implements IEat, IDrink {
    EAT, DRINK;
    @Override
    public void drink() {
        System.out.println("drink");
    }
    @Override
    public void eat() {
        System.out.println("eat");
    }
    public static void main(String[] args) {
        EAT.drink();
        DRINK.eat();
    }
}

通过接口组织枚举

package zeh.test.demo.com.enum1;

// 通过接口中定义内部枚举类并实现接口,来组织枚举分类并实现一定的多态性
public interface Food {
    enum Mianshi implements Food {
        YOUPO, DAOXIAO, SAOZOMIAN;
    }
    enum Mifan implements Food {
        HUANGMENJI, GAIJIAOFAN, ZHUTIFAN;
    }
    enum Roushi implements Food {
        JITUI, DAROU, HONGSHAOROU, PAIGU;
    }
    static void main(String[] args) {
        Food food = Mianshi.DAOXIAO;
        food = Mifan.GAIJIAOFAN;
        food = Roushi.HONGSHAOROU;
    }
}

5.5 枚举和switch case

  1. switch case语句用于选择结构中的一种,当匹配条件很多时使用switch case比较方便。
  2. switch后面的条件参数一般是int和char,jdk的后续版本支持了枚举和String。
  3. case后面的匹配值使用枚举时,不能使用枚举类型去引用枚举对象,直接使用枚举对象即可。

枚举结合switch case使用

package zeh.test.demo.com.enum1;

enum Color {
    RED, GREEN, YELLOW;
}
public class Switch {
    public static void printColor(Color color) {
        switch (color) {
            case RED: // 无须使用Color引用
                System.out.println("红色");
                break;
            case YELLOW:
                System.out.println("黄色");
                break;
            case GREEN:
                System.out.println("绿色");
                break;
            default:
                System.out.println("没有匹配的颜色");
        }
    }
    public static void main(String[] args) {
        printColor(Color.GREEN);
    }
}

6. 枚举实现单例

6.1 枚举类本身作为单例

通过上面的学习,我们已经知道,jdk底层对于枚举有诸多的限制。
总之一句话,枚举对象只能是有限的,且不能通过克隆、序列化、反射等方式创建。
枚举对象是static的,其只随着目标类的装载而实例化一次。
JVM保证了枚举对象在整个JVM生命周期中都是唯一的。
因此,通过枚举实现的单例模式是最经典的。
最简单的枚举实现单例,我们需要的单例就是枚举本身。

枚举实现单例1

package zeh.test.demo.com.enum1;
public enum SingletonEnum {
    // 需要使用时,直接通过SingletonEnum.INSTANCE获取该单例对象。
    // 该对象只有在第一次引用时才被装载,因此是懒加载的。
    INSTANCE;
}

有时候我们想加上一个static方法专门用来返回枚举单例,这更符合我们的编程习惯。
枚举实现单例2

package zeh.test.demo.com.enum1;

public enum SingletonEnum {
    INSTANCE;
    public static SingletonEnum getInstance() {
        return INSTANCE;
    }
}

往往我们需要的单例对象是比较复杂的,不会是个空对象,意味着需要的单例对象是属性和方法的。
枚举实现单例3

package zeh.test.demo.com.enum1;

public enum SingletonEnum {
    INSTANCE("单例枚举对象");
    public static SingletonEnum getInstance() {
        return INSTANCE;
    }
    // 定义单例枚举自己的属性
    private String name;
    // 编写带参构造
    SingletonEnum(String name) {
        this.name = name;
    }
    // 枚举单例提供的业务方法
    public void invoke() {
        System.out.println("name = " + name);
    }
}

6.2 借助枚举改造已有类

实际上很多时候,我们需要的单例对象是独立的一个Class,可能这个类已经编写好久了,我们不想把这个类修改成枚举,这时我们可以通过枚举来改造这个类。
我们将现有的MySingleton类改造为枚举单例:

package zeh.test.demo.com.enum1;

public class MySingleton {
    private MySingleton() {
    }
    public enum SingletonEnum {
        SINGLETON_ENUM;
        private MySingleton instance;
        SingletonEnum() {
            instance = new MySingleton();
        }
        public MySingleton getInstance() {
            return instance;
        }
    }
}

这种借助枚举改造已有类为单例的方式存在严重问题:

package zeh.test.demo.com.enum1;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class EnumMain {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        MySingleton mySingleton1 = MySingleton.SingletonEnum.SINGLETON_ENUM.getInstance();
        MySingleton mySingleton2 = MySingleton.SingletonEnum.SINGLETON_ENUM.getInstance();
        // 返回true
        System.out.println("mySingleton1 == mySingleton2 ?" + (mySingleton1 == mySingleton2));
        Constructor<?> constructor = MySingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        MySingleton mySingleton3 = (MySingleton) constructor.newInstance();
        // 返回false
        System.out.println("mySingleton1 == mySingleton3 ? " + (mySingleton1 == mySingleton3));
    }
}

分析:
类似这种,借助枚举类的外壳,包一个普通类的对象。
然后利用枚举类的各种特性去保证该枚举类的对象是唯一的,既然枚举对象都是唯一的,那么枚举中包装的那个普通类的对象也是唯一的了?
这块实际上和借助静态内部类去创建单例的思想是相同的,但实际上这种并没有真正保证单例特性,因为毕竟枚举包装的类是个普通类,尽管它的构造已经私有,然而我们依旧无法避免通过反射去创建它的一个新对象。

6.3 枚举实现单例的总结

使用枚举实现单例,就直接将单例对象所在的类改造成枚举。 只有枚举对象本身才是真正的单例,其他借助枚举特性去将普通类改造成单例的方式,是存在问题的。

7. 枚举和单例的关系

  1. 枚举常量本身被编译器修饰为:public static final的;而单例模式中的单例一般是 private static 的即可。
  2. 枚举常量直接输出是枚举常量的字面量含义,因为Enum实际上覆盖了toString方法,返回的就是枚举常量的字面量名称;而单例模式中直接输出单例,输出的是单例对象的引用地址。
  3. 枚举类可以创建多个有限的枚举实例(枚举枚举,就是有限个数的意思,可以枚举出来的实例);而单例模式中只有一个单实例。
  4. 枚举本身可以实现单例模式。

8. 枚举规范

  1. 枚举类通常当做常量类使用,因为枚举对象直接toString后就表示其字面量,即常量字符串。
  2. 枚举常量命名规范:全部大写,单词之间用_分隔。

9. 枚举集合

文档信息

Search

    Table of Contents