为什么会突然想要了解Enum呢?
因为先前遇到这样的一种情况,我有一个接口与该接口的众多实现类,这些实现类中有着不同的参数,而我希望能在接口定义一个通用的方法,来对每个实现类的参数进行收集
思考之余,我想到了Enum中的values()方法,能得到不同枚举类中的枚举值
也因为先前没有好好了解Enum的设计,所以趁着这个机会,来尝尝这个java的语法糖
以下不对Enum的用法进行说明,毕竟网上大多数博客已经解释得够详细了
问题
- Enum是如何实现的
- Enum的values()方法是怎么实现的
- Enum的valueOf()方法是怎么实现的
- Java 枚举类比较用 == 还是 equals,有啥区别?
- Enum可以继承吗
- Enum允许实现接口吗
- 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™ 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);
}
解答
-
Enum是如何实现的
enum是一种特殊形式的类,实际上和普通的类没有什么区别,只不过编译器给我们多做了一层处理,使得每一个enum修饰的类都继承于java.lang.Enum,并添加一些其他的方法来达到我们对静态常量类的使用效果.
-
Enum的values()方法是怎么实现的
编译器会将我们定义的枚举常量数组存放到类中$VALUES中,而当我们调用values()时,仅仅只是clone一份发出来而已
-
Enum的valueOf()方法是怎么实现的
在Class.java中定义了一个方法enumConstantDirectory(),仅允许Enum调用
这个方法会为Enum生成一个Map,以枚举常量的name为key,以枚举常量为value
当我们调用valueOf()时,实际上就是在这个Map中去获取对应的枚举值
-
Java 枚举类比较用 == 还是 equals,有啥区别?
使用上没有区别
但在枚举类比较中,使用equals会使人们认为,某些情况下Enum可以有相等的多个对象,这个在Enum中是不允许的
所以我觉得对于判断枚举类是否相等,优先使用 ==
-
Enum可以继承吗
不可以继承类,因为默认为我们继承了Enum
-
Enum允许实现接口吗
允许实现接口,因为本质上,枚举类也是类
-
Enum怎么获得对应名称的字符串
默认情况下,可以使用.name()或是.toString()来获得对应常量名的字符串
但优先选择.name()
因为一旦枚举类的toString被重写,toString获取的值不一定就是我们需要的了;而name()被final修饰,保证不被更改.