1. 什么是枚举?

枚举是一个被命名的整型常数的集合,用于声明一组带标识符的常数。
枚举在曰常生活中很常见,如:人的性别、四季、星期、月份、…。类似这种当一个变量有几种固定可能的取值时,就可以将它定义为枚举类型。

在 JDK 1.5 之前,没有枚举类型,那时候一般用接口常量来替代。如:

public interface SeasonConstant {
	Integer SPRING = 1;
	Integer SUMMER = 2;
	Integer AUTUMN = 3;
	Integer WINTER = 4;
}

这样的定义方式并没有什么错,但它存在许多不足,如:在类型安全和使用方便性上并没有多少好处。如果存在定义 int 值相同的变量,容易混淆,编译器也不会提出任何警告。因此,这种方式在枚举出现后并不提倡。

在 JDK 1.5 中,引入了一种新特性 ------ 枚举,并使用关键字 enum 来表示这种类型,可以更贴近地表示常量。当然,在实际使用中,枚举类型也可以作为一种规范,保障程序参数安全,进行编译检测。

在 Java 中,枚举跟接口、类级别相同,只是使用的关键字不同。并且,枚举也是一种类,只不过这种类是继承了 java.lang.Enum 类

好了,接下来咱们来看看如何定义枚举类型吧~~

2. 定义枚举类型

那么,我们如何定义枚举类型呢?

既然,枚举类型是一种特殊的类类型,那么,我们也可以像定义类那样定义枚举类型啊。定义枚举类型时,可以将其定义在一个单独的文件里面,也可以定义在其它类的内部。

现在,我们利用枚举类型来重新定义上述的常量,定义春天到冬天的常量。如:

public enum SeasonEnum {
	// 春天
    SPRING
    ,
    // 夏天
    SUMMER
    ,
    // 秋天
    AUTUMN
    ,
    // 冬天
    WINTER
    ;
}

枚举类(枚举可称为枚举类) SeasonEnum 中有四个枚举项:SPRING、SUMMER、AUTUMN、WINTER,它们是此枚举类的实例。即:枚举类 SeasonEnum 有 4 个实例对象。

注意:在定义枚举项时,多个枚举项之间使用逗号分隔,最后一个枚举项后需要给出分号!但如果枚举类中只有枚举项(没有构造器、方法、实例变量),那么可以省略分号!

定义了上述枚举类型后,我们又如何去使用它呢?

不能使用 new 来创建枚举类的对象,因为其构造方法是被 private 修饰的。

使用枚举类的写法:枚举类名.枚举项(枚举类中的枚举项就是枚举类中的实例)。即:

public enum SeasonEnum {
    SPRING, SUMMER, AUTUMN, WINTER;

    public static void main(String[] args) {
        SeasonEnum seasonEnum = SeasonEnum.SPRING;
    }
}



枚举类型与 switch 语句

将枚举类型使用在 switch 语句中,使之可读性更强。如:

定义一个四季的枚举类型:

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

测试:

public class TestEnum {
	
	// 判断是否是四季
    public void judgeSeason(SeasonEnum seasonEnum) {
        switch (seasonEnum) {
            case SPRING:
                System.out.println("这是春天");
                break;
            case SUMMER:
                System.out.println("这是夏天");
                break;
            case AUTUMN:
                System.out.println("这是秋天");
                break;
            case WINTER:
                System.out.println("这是冬天");
                break;
            default:
                System.out.println("这是错误的季节");
        }
    }

    public static void main(String[] args) {
        TestEnum testEnum = new TestEnum();
        SeasonEnum spring = SeasonEnum.SPRING;

        testEnum.judgeSeason(spring);
    }
}

TestEnum#judgeSeason(SeasonEnum) 方法的入参是一个 SeasonEnum 类型。在调用此方法时,如果传入的类型不是 SeasonEnum 类型,则在编译期就会报错,保障了程序参数的安全。

注意:在 switch-case 语句中,不能使用枚举类名。如:case SeasonEnum.SPRING:。否则,会在编译期报错(an enum switch case label must be the ...)。因为编译器会根据 switch 中 seasonEnum 的类型来判定每个枚举类型,在 case 中必须直接给出与 seasonEnum 相同类型的枚举选项,而不能再带有枚举类型。

回过头来,看看枚举的用法:枚举类名.枚举项
那么,为什么可以这样用呢?看看枚举的实现原理就知道了

3. 枚举实现原理

将上述的 SeasonEnum 枚举类通过 jad 反编译工具对上面生成的 .class 文件进行反编译一下。
反编译后的结果如下:

package com.tiandy.zzc.design.enums;

public final class SeasonEnum extends Enum    // 继承了 Enum 类
{
	// ---------- javac ----------
    public static SeasonEnum[] values()
    {
        return (SeasonEnum[])$VALUES.clone();
    }

    public static SeasonEnum valueOf(String name)
    {
        return (SeasonEnum)Enum.valueOf(com/tiandy/zzc/design/enums/SeasonEnum, name);
    }

	// 私有的构造方法
    private SeasonEnum(String s, int i)
    {
        super(s, i);
    }

	// 静态常量
    public static final SeasonEnum SPRING;
    public static final SeasonEnum SUMMER;
    public static final SeasonEnum AUTUMN;
    public static final SeasonEnum WINTER;
    private static final SeasonEnum $VALUES[];
	
	// 静态代码块进行初始化
    static 
    {
        SPRING = new SeasonEnum("SPRING", 0);
        SUMMER = new SeasonEnum("SUMMER", 1);
        AUTUMN = new SeasonEnum("AUTUMN", 2);
        WINTER = new SeasonEnum("WINTER", 3);
        $VALUES = (new SeasonEnum[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

从反编译的代码来看,总结以下几点:

  1. 编译器确实帮我们生成了一个 SeasonEnum 类,且此类继承了 java.lang.Enum 类,并被 finally 关键字修饰;
  2. 编译期还帮我们生成了 4 个静态常量,分别对应枚举类中的 4 个枚举实例
  3. 编译期也帮我们生成了 2 个静态方法:values()valueOf()
  4. 编译器也帮我们生成了一个私有的、带参的构造方法:private SeasonEnum(String s, int i)。一个是枚举对象的名字 name;另一个是枚举对象的索引 index

【总结】:使用关键字 enum 定义的枚举类型,在编译期后,会转换成为一个实实在在的类,而在该类中,会存在每个在枚举类型中定义好常量的对应实例对象。如上述的 SPRING枚举常量对应类中的 public static final SeasonEnum SPRING 静态常量

values() 方法

通过上述的反编译代码,我们知道:通过编译后,编译器为我们生成了 values()valueof()方法。查看 java.lang.Enum类,发现里面是没有 values()方法的,但是是有 valueof()方法。

values()方法的作用:获取枚举类中的所有的实例,并作为数组返回。如:

public enum SeasonEnum {

    SPRING,
    SUMMER,
    AUTUMN,
    WINTER;

    public static void main(String[] args) {
        SeasonEnum[] values = SeasonEnum.values();
        // [SPRING, SUMMER, AUTUMN, WINTER]
        System.out.println(Arrays.toString(values));
    }

}

不知大家有没有发现一个问题:

java.lang.Enum 类是没有 values() 方法的,但作为子类的 SeasonEnum 确实有 values() 方法的。如果我将子类向上转型为 父类,那不就调用不了 values() 方法的吗?也就无法一次性获取所有枚举实例变量。

哈哈哈,这个问题确实不错!
但是,你遗漏了一个牛逼的类:java.lang.Class 类。在此类中存在如下两个方法:

  1. T[] getEnumConstants() 方法:返回该枚举类型的所有元素,如果Class对象不是枚举类型,则返回 null
  2. boolean isEnum() 方法:当且仅当该类声明为源代码中的枚举类型时返回 true

因此,通过getEnumConstants() 方法,同样可以轻而易举地获取所有枚举实例。如下:

public enum SeasonEnum {

    SPRING,
    SUMMER,
    AUTUMN,
    WINTER;

    public static void main(String[] args) {
        Class<SeasonEnum> aClass = SeasonEnum.class;

        // 如果是枚举类型
        if (aClass.isEnum()) {
            SeasonEnum[] enumConstants = aClass.getEnumConstants();
            // [SPRING, SUMMER, AUTUMN, WINTER]
            System.out.println(Arrays.toString(enumConstants));
        }
    }

}

正如上述代码所展示,通过此方法,我们仍能一次性获取所有的枚举实例!!

valueof() 方法

虽然 java.lang.Enum类中有 valueof()方法,但是,这两个方法的入参不一样。编译器生成的 valueof()方法只有一个参数,会调用 java.lang.Enum类中有两个参数的 valueof()方法。

valueof()方法的作用:根据名称获取枚举实例。如:

public enum SeasonEnum {

    SPRING,
    SUMMER,
    AUTUMN,
    WINTER;

    public static void main(String[] args) {
        SeasonEnum seasonEnum = SeasonEnum.valueOf("AUTUMN");
        // AUTUMN
        System.out.println(seasonEnum);
    }

}

上述例子为什么会打印出 “AUTUMN”?
因为 java.lang.Enum类中重写了 toString()方法,此方法只返回该枚举类中枚举实例的名称。

public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
    
    // 枚举常量名称
    private final String name;
    
    ...
    
	public String toString() {
	    return name;
	}
}