java enum实现原理

一、分析自定义枚举类

普通的枚举类和抽象枚举类相似,故直接分析抽象枚举类。

1. 编写一个抽象枚举类

package org.example;

public enum Operator {
    ADD ("+"){
        @Override
        public int calculate(int a, int b) {
            return a + b;
        }
    },
    SUB ("-"){
        @Override
        public int calculate(int a, int b) {
            return a - b;
        }
    },
    MUL ("*"){
        @Override
        public int calculate(int a, int b) {
            return a * b;
        }
    },
    DIV ("/"){
        @Override
        public int calculate(int a, int b) {
            return a / b;
        }
    };

    private String identifier;

    Operator(String identifier){
        this.identifier = identifier;
    }

    public abstract int calculate(int a, int b);

    public String getIdentifier(){
        return identifier;
    }
}

2. 编译

使用命令javac Operator.java文件编译成.class文件。

编译生成如下五个文件:

Operator.class
Operator$1.class
Operator$2.class
Operator$3.class
Operator$4.class

Operator.classOperator的编译结果,但是其余四个.class文件目前不明确来源。

3. 反编译

普通的枚举类的反编译结果使用final进行修饰。

3.1 查看Operator简单内容

通过命令javap Operator反编译Operator.class

反编译结果:

public abstract class org.example.Operator extends java.lang.Enum<org.example.Operator> {
  public static final org.example.Operator ADD;
  public static final org.example.Operator SUB;
  public static final org.example.Operator MUL;
  public static final org.example.Operator DIV;
  public static org.example.Operator[] values();
  public static org.example.Operator valueOf(java.lang.String);
  public abstract int calculate(int, int);
  public java.lang.String getIdentifier();
  org.example.Operator(java.lang.String, int, java.lang.String, org.example.Operator$1);
  static {};
}

反编译之后的结果多了valuesvalueOf以及一个static代码块,而且构造函数的参数和我们写的参数也不一致。但是有用信息太少,需要查看详细的编译信息,用来确定这些新增代码的用途。

3.2 查看Operator详细信息

通过命令javap -c -v Operator查看Operator.class反编译的详细信息。

具体的信息由于篇幅原因不展示具体的编译信息,只展示如下分析结果:

public abstract class org.example.Operator extends java.lang.Enum<org.example.Operator> {
  public static final org.example.Operator ADD;
  public static final org.example.Operator SUB;
  public static final org.example.Operator MUL;
  public static final org.example.Operator DIV;
  private static final org.example.Operator[] $VALUES;
  private String identifier;
  
  // 抽象方法,在子类中实例化
  public abstract int calculate(int, int);
  
  public java.lang.String getIdentifier(){ 
      return identifier;
  }
  
  // 关于编译之后添加的name、ordinal可以在Enum中看出它们的用途
  private org.example.Operator(String name, int ordinal, String identifier){
      super(name, ordinal);
      this.identifier = identifier;
  }
  
  // 看不懂该方法的作用
  org.example.Operator(String name, int ordinal, String identifier, org.example.Operator$1 ){     
      this(name, ordinal, identifier); 
  }
  
  public static org.example.Operator[] values(){
      return (Operator[])$VALUES.clone();
  }
  
  // 根据名字返回对应的枚举常量,具体看Eunm类的分析
  public static org.example.Operator valueOf(String name){
      return (Operator)super.valueOf(Operator.class, name);
  }

  // 实例化枚举类型中定义的常量
  static {
      ADD = new Operator$1("ADD", 0, "+");
      SUB = new Operator$2("SUB", 1, "-");
      MUL = new Operator$3("MUL", 2, "*");
      DIV = new Operator$4("DIV", 3, "/");
      $VALUES = new Operator[4];
      $VALUES[0] = ADD;
      $VALUES[1] = SUB;
      $VALUES[2] = MUL;
      $VALUES[3] = DIV;
  };
}
InnerClasses:
     static #24; //class org/example/Operator$4
     static #19; //class org/example/Operator$3
     static #14; //class org/example/Operator$2
     static #9;  //class org/example/Operator$1

最后的InnerClasses部分解答了之前的疑惑:每一个枚举对象就是一个匿名内部类的实例。该实例在Operator类的static代码块中初始化。并且每个枚举类型内部都会维护一个$VALUES数组,用来保存内部所有的枚举实例。

3.3 查看Operator$1.class详细信息

使用javap -c -v Operator$1查看Operator$1.class反编译的详细信息。
其余的几个和Operator$1.class类似,不做多余解释。

同样只展示分析后的结果:

final class org.example.Operator$1 extends org.example.Operator{
  // 构造函数传入了一个匿名子类的实例(实际上是`null`),搞不懂
  org.example.Operator$1(String name, int ordinal, String identifier){
      super(name, ordinal, identifier, null);
  }

  public int calculate(int a, int b){ 
      return a + b; 
  }
}

很简单的匿名内部类的实现,单纯的实现了父类的抽象方法。

自定义枚举分析完毕,下来看一下Enum类。

二、分析Enum类源码

public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
    /**
     * 枚举常量的名字,在本案例中是“ADD”、“SUB”、“MUL”、“DIV”。
     * 大多数的程序应该在{@link #toString}中引用该字段,而不是直接访问该字段。
     * 因此{@link #toString}需要返回对用户友好的字符串。
     */
    private final String name;

    public final String name() { return name; }

    /**
     * 枚举常量的定义的顺序(第一个常量的ordinal从0开始)。
     * 大多数的程序中不应该使用该字段。该字段被一些基于枚举的数据结构所使用,例如
     * {@link java.util.EnumSet} and {@link java.util.EnumMap}.
     */
    private final int ordinal;

    public final int ordinal() { return ordinal; }

    /**
     * 唯一的构造方法。程序不能执行该构造方法。它只能被编译器编生成的代码
     * (ACC_SYNTHETIC,比如{@link org.example.Operator(String, int, String)})
     * 所使用。
     *
     * 如果要是通过反射调用该构造方法,也会抛出异常。因为在反射API中对Class的类型做了判断。
     */
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

    /**
     * 该方法应该被重写,并返回一些对用户友好的字符串。
     */
    public String toString() { return name; }

    public final boolean equals(Object other) { return this==other; }

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

    /** enum classes cannot have finalize methods. */
    protected final void finalize() { }

    /**
     * 该函数是通过ordinal字段来比较的,因此其返回结果和常量定义的顺序有关系。
     * 并且枚举常量只能和相同类型的枚举常量进行比较。
     */
    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对象。 
         * 在当前案例中,每一个枚举的Class对象应该是内部类,比如“ADD”对象实际上是Class<Operator$1>,
         * 但这些是虚拟机内部的实现,用户想要看到的是Class<Operator>。
     */
    @SuppressWarnings("unchecked")
    public final Class<E> getDeclaringClass() {
        Class<?> clazz = getClass();
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }

    /**
     * 返回指定枚举类型中指定名字的枚举常量。该name在每个枚举类型中唯一。
     * 
     * 该方法被每个枚举类型中隐式的{@code public static T valueOf(String)}
     * 所使用。程序应该使用每个枚举类型中的{@code public static T valueOf(String)}。
     */
    public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
        // 根据name从map中取该常量
        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);
    }


    /**
     * 该方法抛出一个异常,为了保证枚举常量“单例”的特性。
     */
    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

    /**
     * 防止反序列化攻击,为了保证枚举常量“单例”的特性。
     */
    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");
    }
}

三、收获

枚举如何保证单例性

  1. new关键字:
    • 枚举:直接在编译器层面进行了控制。
    • 替代:可以通过构造方法私有化来实现。
  2. 继承:
    • 枚举:在编译器层面做了控制,无法继承一个枚举类。
    • 替代:普通类可以通过final关键字实现。抽象类则无法实现,因为我们没有办法控制一个类只被特定的几个类继承,但是可以通过其他方法间接实现该效果。
  3. 反射API:
    • 枚举:在反射API中进行了控制。
    • 替代:可以在单例对象的构造方法中进行判断来实现。
  4. 反序列化:
    • 枚举:在反序列化用到的方法中进行了控制。
    • 替代:可以通过重写反序列化方法实现。

关于反射API

反射API一般通过Class.newInstance来获取实例对象:

// java.lang.Class
@CallerSensitive
public T newInstance() throws InstantiationException, IllegalAccessException {
    // ... 省略其他代码
    try {
        return tmpConstructor.newInstance((Object[])null);
    } catch (InvocationTargetException e) {
        Unsafe.getUnsafe().throwException(e.getTargetException());
        return null;
    }
}

该方法通过 tmpConstructor.newInstance((Object[])null);新建对象。继续跟踪:

// java.lang.reflect.Constructor
@CallerSensitive
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对Enum类型做了判断,不会实例化Enum类型。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343