七个问题 初探语法糖enum

为什么会突然想要了解Enum呢?

因为先前遇到这样的一种情况,我有一个接口与该接口的众多实现类,这些实现类中有着不同的参数,而我希望能在接口定义一个通用的方法,来对每个实现类的参数进行收集

思考之余,我想到了Enum中的values()方法,能得到不同枚举类中的枚举值

也因为先前没有好好了解Enum的设计,所以趁着这个机会,来尝尝这个java的语法糖

以下不对Enum的用法进行说明,毕竟网上大多数博客已经解释得够详细了


问题

  1. Enum是如何实现的
  2. Enum的values()方法是怎么实现的
  3. Enum的valueOf()方法是怎么实现的
  4. Java 枚举类比较用 == 还是 equals,有啥区别?
  5. Enum可以继承吗
  6. Enum允许实现接口吗
  7. Enum怎么获得对应名称的字符串

从源码进行分析

为了探究Enum的实现,我写了这么一个季节的枚举类,枚举了4个季节,每一个季节还有一个code

public enum Season {
    SPRING(1),
    SUMMER(2),
    AUTUMN(3),
    WINTER(4);

    private final int code;

    Season(int code) {
        this.code = code;
    }

    public int getCode() {
        return code;
    }
}

反编译后的结果如下

public final class Season extends Enum
{

    public static Season[] values()
    {
        return (Season[])$VALUES.clone();
    }

    public static Season valueOf(String name)
    {
        return (Season)Enum.valueOf(Season, name);
    }

    private Season(String s, int i, int code)
    {
        super(s, i);
        this.code = code;
    }

    public int getCode()
    {
        return code;
    }

    public static final Season SPRING;
    public static final Season SUMMER;
    public static final Season AUTUMN;
    public static final Season WINTER;
    private final int code;
    private static final Season $VALUES[];

    static 
    {
        SPRING = new Season("SPRING", 0, 1);
        SUMMER = new Season("SUMMER", 1, 2);
        AUTUMN = new Season("AUTUMN", 2, 3);
        WINTER = new Season("WINTER", 3, 4);
        $VALUES = (new Season[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

首先,我们一部分一部分来看(以下代码块中,我用enum来表示我们编写的代码,用Enum来表示反编译后的代码)

首先可以看到枚举类是一个"class",继承于Enum类,也就是我们用enum定义的枚举类是一个货真价实的类,只不过由于语法糖的缘故,我们编写代码的时候可以不用像一般的类一样

>>> enum
	public enum Season{}
<<< Enum
	public final class Season extends Enum{...}

再来看类中定义的变量,很惊奇,多了很多,而且我们也可以看到我们定义的四个季节也在其中,而且还多了一个"$VALUES[]"的变量

>>> enum
    private final int code;
<<< Enum
    public static final Season SPRING;
    public static final Season SUMMER;
    public static final Season AUTUMN;
    public static final Season WINTER;
    private final int code;
    private static final Season $VALUES[];

再看到类中定义的方法,当然除了本身的构造函数以及getCode(),还多了个values()和valueOf()

先来说构造函数,原先的构造函数中仅有code一个参数,反编译后出现了一个字符串s和整数i,这是什么呢?

>>> enum
    private Season(int code) {}
    public int getCode() {}
<<< Enum
    private Season(String s, int i, int code) {}
    public int getCode() {}
	public static Season[] values(){}
	public static Season valueOf(String name){}

非常明显的还有一个static代码块,看到这个代码块,我们基本就能够知道enum是怎么实现的了.

在对象初始化的时候,执行static代码块时,会将我们写的枚举类生成一个个对应的枚举类的对象,而且看到构造函数中,会将我们写的枚举类对象的具体名称转化为字符串放到对象中去

而且,对于每一个枚举类对象,按顺序从0开始递增产生编码,这个编码可以理解成是这个对象的id,这个编码对于数据表中映射存储特别方便

依次生成对象后,$VALUES存储了所有枚举类对象

<<< Enum
   static 
    {
        SPRING = new Season("SPRING", 0, 1);
        SUMMER = new Season("SUMMER", 1, 2);
        AUTUMN = new Season("AUTUMN", 2, 3);
        WINTER = new Season("WINTER", 3, 4);
        $VALUES = (new Season[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    } 

所以说到底,其实我们是利用语法糖enum去便捷地实现了这些功能而已,本质上还是在存储final对象

最后看到刚刚漏掉的两个方法

public static Season[] values()
{
    return (Season[])$VALUES.clone();
}

public static Season valueOf(String name)
{
    return (Season)Enum.valueOf(Season, name);
}

解答刚刚的第二个问题:Enum的values()方法是怎么实现的

如果我们不反编译的话,我们是找不到values()这个方法的,Enum类也不会存在这个方法,而这个方法的实现原理就更简单了,只是单纯将储存的枚举类数组$VALUES clone一份发出来而已


其实现在才真的开始看Enum.java源码

首先还是先看介绍,简单来说就是Enum是一切枚举类的基类,而enum是给编译器的隐式声明

如果需要在set或map中使用到Enum,推荐使用EnumSet与EnumMap

/**
* This is the common base class of all Java language enumeration types.
*
* More information about enums, including descriptions of the
* implicitly declared methods synthesized by the compiler, can be
* found in section 8.9 of
* <cite>The Java&trade; Language Specification</cite>.
*
* <p> Note that when using an enumeration type as the type of a set
* or as the type of the keys in a map, specialized and efficient
* {@linkplain java.util.EnumSet set} and {@linkplain
* java.util.EnumMap map} implementations are available.
 */

还记得刚刚Season的构造函数,多了两个参数(String s,int i),这两个参数正对应着Enum类的name和ordinal,如下

private final String name;
public final String name() {
    return name;
}

private final int ordinal;
public final int ordinal() {
     return ordinal;
}

name对应的是我们定义枚举类时写的常量名

ordinal对应的是我们定义枚举类时的书写顺序,由0开始逐个递增

所以我们能使用name()与ordinal()方法获得对应枚举类的常量名与对应顺序

而如果用过枚举类的toString(),应该也会发现,跟name一样的功能,而且在源码中,两个方法确实是一模一样,而且都只是返回了name,那这两个方法有什么不同吗?

public String toString() {
    return name;
}

当然,从方法名上看就已经有区别了哈哈

正经说,首先toString()在Object类中已经有默认的实现

>>> Object.java
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

我们都知道,一个对象.toString()能够获得这个对象的直观信息,这个信息是在定义这个类时我们写入的一些直观信息,例如在枚举类中,我们认为常量的name是最直观的,那就是默认的情况,返回name

但如果我们写的Season类中的code也是比较重要的情况下,我们会希望重写toString方法,那此时枚举类的toString方法就会被覆盖.

而Enum.java中的name()方法是用final修饰的,绝对不会被重写

简而言之,默认情况下,枚举类的toString()与name()没有任何区别,但如果是要获取枚举常量名,应使用name(),因为name()一定是对应的枚举常量名,而toString()不一定,只要toString被重写的话

再看到equals()方法

没错简单粗暴,就用了"==",所以对于枚举类来说使用equals和使用"=="一模一样

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

接下来看到比较常用的valueOf()

实际使用时,我们只需要传入对应的name即可,enumType在上层已经为我们做了封装

代码的意思就比较简单,从枚举类的enumConstantDirectory中拿到一个Map,这个Map的key为枚举常量名name,value为枚举常量

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);
}

这时候有意思的点就来了,我们定义的枚举类中什么时候还出现了enumConstantDirectory()这个方法了,这个Map是什么时候生成的?

这个enumConstantDirectory()是在Class.java中实现了

而看到代码的实现,我们也能看到,如果我们不调用这个方法的话,enumConstantDirectory将会一直为null

而enumConstantDirectory的赋值也仅仅只能赋值一次,也就是当枚举类第一次使用valueOf时,enumConstantDirectory也会进行赋值,之后再调用valueOf,就仅仅只是将存储的enumConstantDirectory返回而已

Map<String, T> enumConstantDirectory() {
    if (enumConstantDirectory == null) {
        T[] universe = getEnumConstantsShared();
        if (universe == null)
            throw new IllegalArgumentException(
                getName() + " is not an enum type");
        Map<String, T> m = new HashMap<>(2 * universe.length);
        for (T constant : universe)
            m.put(((Enum<?>)constant).name(), constant);
        enumConstantDirectory = m;
    }
    return enumConstantDirectory;
}
private volatile transient Map<String, T> enumConstantDirectory = null;

最后,再简单了解一下EnumMap(EnumSet基本同理就不多解释)

因为枚举类是一种特殊的类,且有着自己的序号ordinal,所以利用这个ordinal直接定位到内部数组的索引,这样就可以避免hashCode的计算,效率更高

而且因为索引是连续递增的,且枚举类的常量个数是确定的,所以也不会造成过多的空间浪费

所以,如果是用枚举类作Map的key时,优先采取EnumMap

>>> EnumMap.java
public V get(Object key) {
    return (isValidKey(key) ?
            unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
}

解答

  1. Enum是如何实现的

    enum是一种特殊形式的类,实际上和普通的类没有什么区别,只不过编译器给我们多做了一层处理,使得每一个enum修饰的类都继承于java.lang.Enum,并添加一些其他的方法来达到我们对静态常量类的使用效果.

  2. Enum的values()方法是怎么实现的

    编译器会将我们定义的枚举常量数组存放到类中$VALUES中,而当我们调用values()时,仅仅只是clone一份发出来而已

  3. Enum的valueOf()方法是怎么实现的

    在Class.java中定义了一个方法enumConstantDirectory(),仅允许Enum调用

    这个方法会为Enum生成一个Map,以枚举常量的name为key,以枚举常量为value

    当我们调用valueOf()时,实际上就是在这个Map中去获取对应的枚举值

  4. Java 枚举类比较用 == 还是 equals,有啥区别?

    使用上没有区别

    但在枚举类比较中,使用equals会使人们认为,某些情况下Enum可以有相等的多个对象,这个在Enum中是不允许的

    所以我觉得对于判断枚举类是否相等,优先使用 ==

  5. Enum可以继承吗

    不可以继承类,因为默认为我们继承了Enum

  6. Enum允许实现接口吗

    允许实现接口,因为本质上,枚举类也是类

  7. Enum怎么获得对应名称的字符串

    默认情况下,可以使用.name()或是.toString()来获得对应常量名的字符串

    但优先选择.name()

    因为一旦枚举类的toString被重写,toString获取的值不一定就是我们需要的了;而name()被final修饰,保证不被更改.

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值