Java枚举类之Enum源码分析

一、概述

Enum的全写是Enumeration,这个词的翻译是列举、逐条陈述、细目。在程序语言中,枚举类型是一种特殊的数据类型(常用的数据类型比如字符串、整型),这种数据类型的变量值限定在固定的范围,比如季节只有春夏秋冬,月份是12个。

枚举类型是JDK 5之后引进的一种非常重要的引用类型,可以用来定义一系列枚举常量。在没有引入enum关键字之前,要表示可枚举的变量,只能使用以下的方式。

public class SeasonDef {
    
    public static final int SPRING = 1;
    public static final int SUMMER = 2;
    public static final int AUTUMN = 3;
    public static final int WINTER = 4;

    public static String getSeason(int type) {
        String season = "";
        switch (type) {
            case SPRING:
                season = "春天";
                break;
            case SUMMER:
                season = "夏天";
                break;
            case  AUTUMN:
                season = "秋天";
                break;
            case WINTER:
                season = "冬天";
                break;
        }
        return season;
    }
}

这种实现方式有几个弊端。

  1. 类型不安全。试想一下,有一个方法期待接受一个季节作为参数,那么只能将参数类型声明为int,但是传入的值可能是99。显然只能在运行时进行参数合理性的判断,无法在编译期间完成检查。
  2. 指意性不强,含义不明确。我们使用枚举,很多场合会用到该枚举的字串符表达,而上述的实现中只能得到一个数字,不能直观地表达该枚举常量的含义。当然也可用String常量,但是又会带来性能问题,因为比较要依赖字符串的比较操作。

使用enum来表示枚举可以更好地保证程序的类型安全和可读性。

  1. enum是类型安全的。除了预先定义的枚举常量,不能将其它的值赋给枚举变量。这和用intString实现的枚举很不一样。
  2. enum有自己的名称空间,且可读性强。在创建enum时,编译器会自动添加一些有用的特性。每个enum实例都有一个名字(name)和一个序号(ordinal),可以通过toString()方法获取enum实例的字符串表示。还以通过values()方法获得一个由enum常量按顺序构成的数组。

enum还有一个特别实用的特性,可以在switch语句中使用,这也是enum最常用的使用方式了。

二、案例

2.1 例1:获取枚举 - values方法

import lombok.Getter;
import java.util.Arrays;

@Getter
public enum SeasonEnum {

    SPRING(1, "春天"),
    SUMMER(2, "夏天"),
    AUTUMN(3, "秋天"),
    WINTER(4, "冬天"),
    ;

    Integer code;
    String name;

    SeasonEnum(Integer code, String name) {
        this.code = code;
        this.name = name;
    }

    public static SeasonEnum getCode(Integer code) {
        for (SeasonEnum season : SeasonEnum.values()) {
            if(season.getCode().equals(code)) {
                return season;
            }
        }
        return null;
    }

    //java8写法
    public static SeasonEnum getSeasonEnum(Integer code) {
        return Arrays.stream(SeasonEnum.values())
                .filter(t -> t.getCode().equals(code)).findFirst().orElse(null);
    }
}

2.2 例2:获取枚举 - EnumMap类

@Getter
public enum SeasonEnum {
    SPRING(1, "春天"),
    SUMMER(2, "夏天"),
    AUTUMN(3, "秋天"),
    WINTER(4, "冬天"),
    ;
    String code;
    String name;

    SeasonEnum(String code, String name) {
        this.code = code;
        this.name = name;
    }

    // 将数据缓存到map中
    private static final Map<String, String> map = new HashMap<>();

    static {
        for (SeasonEnum season : SeasonEnum.values()) {
            map.put(season.getCode(), season.getName());
        }
    }

    // 根据name查询value值
    public static String getNameByCode(String code) {
        return map.get(code);
    }
}

2.3 例3:枚举实现抽象方法

与常规抽象类一样,enum类允许我们为其定义抽象方法,然后使每个枚举实例都实现该方法,以便产生不同的行为方式,注意abstract关键字对于枚举类来说并不是必须的。

枚举也可以定义多个抽象方法

/*
 * 枚举类里定义抽象方法时不能使用abstract关键字将枚举类定义成抽象类(由于系统自动会为它添加abstract关键字),
 * 但由于枚举类须要显式建立枚举值,而不是做为父类,因此定义每一个枚举值时必须为抽象方法提供实现,不然将出现编译错误。
 */
public enum Operation {
    PLUS {
        public double eval(double a, double b){
            return a + b;
        }
    },
    MINUS {
        public double eval(double a, double b){
            return a - b;
        }
    },
    MULTI {
        public double eval(double a, double b){
            return a * b;
        }
    }, 
    DIVIDE {
        public double eval(double a, double b){
            return a / b;
        }
    };
	
    //为枚举类定义一个抽象方法
    //这个抽象方法由不一样的枚举值提供实现
    public abstract double eval(double a, double b);
		
    public static void main(String[] args){
        for(Operation o : Operation.values()){
            System.out.println(o.eval(1, 2));
        }
    }
}

三、源码分析

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {

    //枚举常量的名称
    //大多数程序员应该使用toString方法而不是访问此字段。
    private final String name;

    //返回此枚举常量的名称,与其枚举声明中声明的完全相同
    public final String name() {
        return name;
    }

    //此枚举常量的序数(它在枚举声明中的位置,其中初始常量的序数为零)
    private final int ordinal;

    //返回序号
    public final int ordinal() {
        return ordinal;
    }

    // 构造器
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

    //返回声明中包含的此枚举常量的名称
    public String toString() {
        return name;
    }

    //果指定的对象等于此枚举常量,则返回true。
    public final boolean equals(Object other) {
        return this==other;
    }

    public final int hashCode() {
        return super.hashCode();
    }

    // 无法被克隆
    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

    //将此枚举与指定的枚举序号进行比较
    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;
    }

    //返回与此枚举常量的枚举类型相对应的Class对象
    @SuppressWarnings("unchecked")
    public final Class<E> getDeclaringClass() {
        Class<?> clazz = getClass();
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == java.lang.Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }

    /**
     * 返回具有指定名称的指定枚举类型的枚举常量。
     * 该名称必须与用于声明此类型的枚举常量的标识符完全一致。
     * 请注意,对于特定枚举类型T,
     *  有两个隐式声明方法可以直接使用:
     *      public static T valueOf(String)    根据名称获取单个枚举类型
     *      public static T[] values()   获取所有枚举类型数组
     */
    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 constant " + enumType.getCanonicalName() + "." + name);
    }

    //枚举类不能有 finalize 方法
    protected final void finalize() { }

    //防止反序列化
    private void readObject(ObjectInputStream in) throws IOException,
            ClassNotFoundException {
        throw new InvalidObjectException("can't deserialize enum");
    }
	
    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("can't deserialize enum");
    }
}

四、底层原理

javac命令将SeasonEnum这个枚举类编译生成SeasonEnum.class可执行文件,紧接着使用javap命令将SeasonEnum.class文件反汇编。

public final class com.test.SeasonEnum extends java.lang.Enum<com.test.SeasonEnum> {
  public static com.test.SeasonEnum[] values();
    Code:
      0: getstatic     #1  // Field $VALUES:[Lcom/test/SeasonEnum;
      3: invokevirtual #2  // Method "[Lcom/test/SeasonEnum;".clone:()Ljava/lang/Object;
      6: checkcast     #3  // class "[Lcom/test/SeasonEnum;"
      9: areturn

  public static com.test.SeasonEnum valueOf(java.lang.String);
    Code:
      0: ldc           #4  // class com/test/SeasonEnum
      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/test/SeasonEnum
      9: areturn

  static {};
    Code:
      0: iconst_0
      1: anewarray     #4  // class com/test/SeasonEnum
      4: putstatic     #1  // Field $VALUES:[Lcom/test/SeasonEnum;
      7: return
}
  1. 类:SeasonEnum在底层其实还是用class修饰的,说明枚举类本质上还是一个Class,而且SeasonEnumjvm隐式的用final关键字修饰起来了,那么这个类不能被任何类继承,这也在一定程度上保证了类的安全。
  2. values方法:默认生成了一个values方法,它返回了当前类型枚举的数组,这也解释了ordinal的来源。
  3. valueOf方法:在具体的每个自定义枚举类里面生成了一个根据枚举名返回枚举实例的方法,这其实是对java.lang.EnumvalueOf方法的具体重写,它只限于当前枚举类。
  4. 静态代码块:SeasonEnum中所有的enum实例在这里初始化完成。

五、拓展

5.1 枚举是如何保证线程安全的

当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以创建一个enum类型是线程安全的。

5.2 为什么用枚举实现的单例是最好的方式

  1. 枚举写法简单
  2. 枚举自己处理序列化
  3. 枚举实例创建是thread-safe(线程安全的)

5.3 EnumMap与EnumSet

为了更好地支持枚举类型,java.util中添加了两个新类:EnumMapEnumSet。使用它们可以更高效地操作枚举类型。具体请参考Java容器之EnumMap源码分析Java容器之EnumSet源码分析

参考文章

posted @ 2022-05-23 20:14  夏尔_717  阅读(368)  评论(0编辑  收藏  举报