走进 JDK 之 Enum

news/2024/7/4 9:30:49

什么是枚举

什么是枚举?说实话,在我这些年的开发生涯中,用过枚举的次数大概两只手都可以数的过来。当然你不能说枚举一无是处,只能说是我对 Java 理解的还不够深刻,在可以使用枚举的时候并没有去使用。

假设我有两个孩子(其实不用假设),每到周末他们都不知道去上什么辅导班。于是我就写了一个简单的程序来告诉他们,伪代码如下:

public final static int DAVID = 0;
public final static int MARRY = 1;

public void order(int who){
    switch (who){
        case DAVID:
            System.out.println("大卫,去练武术!");
            break;
        case MARRY:
            System.out.println("玛丽,去练跳舞!");
            break;
         default:
            System.out.println("今天休息");
    }
}
复制代码

然后我告诉 David 你输入 0,告诉 Marry 你输入 1,就可以了。不久之后,辅导班老师就指点问候我了,您家的两个孩子呢?这个气的我呀,立马回家看了看日志,两个孩子除了 0 和 1,其他数字都输齐了。

由此可见,这样直接使用 int 常量无法限定用户的输入,你让它输 0 或 1,它偏偏输个 45678。从代码可读性来说,参数是个 int 值,并不是那么直观的就可以看出来应该输入什么。无奈之下,我只得掏出 《Java 编程思想》,来治治这两个熊孩子。下面是我优化的程序:


public enum Child { DAVID, MARY }

public  void order(Child who) {
    switch (who) {
        case DAVID:
            System.out.println("大卫,去练武术!");
            break;
        case MARY:
            System.out.println("玛丽,去练跳舞!");
            break;
        default:
            System.out.println("今天休息");
    }
}
复制代码

实际上已经可以删除 default 了,因为参数是枚举类 Child,输入范围已经被限定为我定义的枚举,输入其他值将无法通过编译。两个熊孩子终于可以愉快的去上课了。

枚举是 Java 1.5 中新增的引用类型,指由一组固定的常量组成合法值的类型,其实上面的例子并不那么适合枚举,例如一年四季,一周七天这样的,更加适合枚举。相比使用 int 常量来定义,枚举具有类型安全和可读性良好的优势。《Effective Java》中也鼓励 用 enum 代替 int 常量

除此之外,还可以给枚举添加字段和方法,例如,我想给每个孩子加上姓名,可以这么做:

public enum Child {

    DAVID("David"), MARY("Marry");

    private String name;

    Child(String name){
        this.name=name;
    }
    
    public static void main(String[] args){
        for (Child child:Child.values()){
            System.out.println(child.ordinal()+" "+child.name);
        }
    }
}
复制代码

注意,枚举的定义只能放在第一行,如果你把 DAVID("David"), MARY("Marry"); 放在其他位置,是无法通过编译的。另外别忘了,最后一个枚举常量后面要加分号。

values() 会枚举出 Child 中定义的所有枚举常量。打印结果如下:

0 David
1 Marry
复制代码

是不是比之前的 int 常量那种方式强大多了。

源码解析

Enum

走进 JDK 系列,那必然是少不了源码解析的。

类定义

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {}
复制代码

Enum 是一个抽象类,我们自己定义的枚举类都继承了 Enum。实现了 Comparable 接口,重写了 compare 的逻辑。实现了 Serializable 接口,可以序列化。但是枚举对序列化作了一定的限制,在序列化的时候仅仅是将枚举对象的 name 属性输出到结果中,反序列化的时候则是通过 Enum.valueOf() 方法来查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了 writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve 等方法。因此,用枚举来实现单例模式的话,是反序列化安全的,因为即使反序列化也不会生成新的对象。

字段

private final String name; // 枚举实例的名称,就是枚举声明中的名称
private final int ordinal; // 在枚举声明中的次序,从 0 开始
复制代码

枚举类就只有这两个字段。name 就是枚举声明的名字,比如 DAVIDname 就是 DAVIDordinal 就是声明中的次序,之所以在 switch 中可以使用枚举,就是因为编译器会自动调用枚举的 ordinal() 方法。

构造函数

protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}
复制代码

受保护的,你不能调用枚举类的构造函数。

方法

public final String name() {
    return name; // 返回 name
}

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

public String toString() {
    return name; // 可以重写使得枚举类返回一个用户友好的名字,默认返回 name
}

public final boolean equals(Object other) {
    return this==other; // 枚举的 equals 和 == 是等价的
}

protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException(); // 枚举不支持 clone,单例
}

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; // 实际上是比较 ordinal
}

// 根据指定的枚举类型和名称返回枚举实例,在反序列化中会使用
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);
}
复制代码

还记得之前使用过的 values() 方法吗,用来遍历枚举的。找遍了 Enum.java 也没有看到这个方法,既然父类中没有这个方法,那么一定是在子类中声明的了。下面我们来验证一下。

Enum 子类

我们是怎么定义枚举的,

public enum Child {
    DAVID("David"), MARY("Marry");
    private String name;
    Child(String name){
        this.name=name;
    }
}
复制代码

并没有显示的去继承 Enum,而是使用了 enum 关键字,虽然没有使用 class 关键字,但其实它还是一个类,只是编译器帮我们做了中间步骤。之前有说过,知其然不知其所以然的时候,javap 是你的好帮手。这次我们不 javap 了,毕竟字节码可读性不是那么的好。我们使用 jad 反编译 Child.class 文件,得到结果如下:

// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.geocities.com/kpdus/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   Child.java

package enums;

public final class Child extends Enum {

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

    public static Child valueOf(String s) {
        return (Child)Enum.valueOf(enums/Child, s);
    }

    private Child(String s, int i, String s1) {
        super(s, i);
        name = s1;
    }

    public static final Child DAVID;
    public static final Child MARY;
    private String name;
    private static final Child $VALUES[];

    static {
        DAVID = new Child("DAVID", 0, "David");
        MARY = new Child("MARY", 1, "Marry");
        $VALUES = (new Child[] {
            DAVID, MARY
        });
    }
}
复制代码

看到这个东西,你就应该全明白了。说起来叫枚举,其实就是普通的类,只是我们只需要使用 enum 关键字,编译器就会帮我们做好一切。枚举中声明的变量都是 static final 的,且在 static 代码块中进行初始化,并存入对象数组 $VALUES。所以枚举实例的创建默认是线程安全的。除此之外,编译器还自动生成了 values() 方法,返回 $VALUES 数组的克隆。

说到这里,你应该对枚举的原理很清楚了。枚举的种种特性都特别契合单例模式,天生的线程安全和反序列化安全,这都是其他单例模式所不具备的。但是在我所见过的代码中,真正使用枚举去做单例的好像少之又少。具体的原因有待考究。

真的要使用枚举吗?

站在 Android 开发者的角度,实际上官方是不建议我们使用枚举的。

枚举占用的空间通常是静态常量的两倍。你应该严格避免在 Android 中使用枚举。

其实我并不是完全赞同。MVP 多了那么多接口和类,我们应该使用吗?在如今的手机内存下,如果你的应用发生了 OOM,我想枚举应该不是罪魁祸首吧。只要不过度使用,站在增强代码可读性,保证类型安全的角度,用枚举代替静态常量肯定是个好选择。当然如果你说你要追求极致的性能,那倒不必使用枚举了。

文章首发微信公众号: 秉心说 , 专注 Java 、 Android 原创知识分享,LeetCode 题解,欢迎关注!


http://www.niftyadmin.cn/n/2762493.html

相关文章

精度与分辨率是一回事吗

编码器精度与分辨率的区别 对于传感器的分辨率与精度的理解&#xff0c;可以拿千分尺为例&#xff0c;分辨率代表千分尺最多可以读到小数点后几位&#xff0c;但精度还与尺子的加工精度&#xff0c;测量方法有关系。同样的&#xff0c;在旋转编码器的使用中&#xff0c;分辨率与…

ESX主机上虚拟机文件的组成

在这一系列的第一部分中&#xff0c;我们从硬件角度了解了什么是虚拟机。在本部分中&#xff0c;TechTarget中国的特约虚拟化专家Eric Siebert将介绍ESX主机上虚拟机文件的组成。与虚拟机相关的文件有很多&#xff0c;它们位于主机上虚拟机的目录里。 如果你使用像WinSCP或者Da…

SAP S4 会计科目主数据相关类型的解释

新项目上&#xff0c;使用S4系统&#xff0c;在创建会计科目表时&#xff0c;相关字段的解释&#xff1a; &#xff08;1&#xff09;总账科目类型&#xff1a; N&#xff1a;对于不需要创建成本要素的损益科目&#xff1b; P:需要创建成本要素的损益科目&#xff1b; S&#x…

企业如何选择阿里云服务器配置【小白新手攻略】

阿里云在国内云计算领域的地位是毋庸置疑的&#xff0c;市场占有率早已超过了半壁江山&#xff0c;目前无论个人站长还是企业&#xff0c;都将阿里云作为了上云的第一选择&#xff0c;那么作为企业用户应该如何选择阿里云服务器呢?购买什么配置的阿里云服务器是适合自己的呢&a…

假期学习【四】RDD编程实验一

1.今天把Spark编程第三个实验的Scala独立程序编程写完了。使用 sbt 打包 Scala 程序&#xff0c;然后提交到Spark运行。 2.完成了实验四的第一项 &#xff08;1&#xff09;该系总共有多少学生&#xff1b;map(t > t.split(",")(0))表示&#xff1a;lines这个RDD中…

浅谈datagrid详细操作单元格样式

http://www.easyui.info/archives/470.html 今天有朋友问到&#xff1a;“如果设置列标题居中而列内容居右显示&#xff1f;”&#xff0c;仔细查了一下api&#xff0c;目前版本提供了两个相关的列属性&#xff0c;align和styler。align属性设置后会让列标题和列内容的对齐方式…

SAP S4 统计型成本要素

SAP 会计科目体系中把会计科目和成本要素统一到FS00中&#xff0c;那么在某种情况下用到的 90型成本要素&#xff0c;如何设置&#xff1f; 答案&#xff1a; &#xff08;1&#xff09; SAP只能对资产科目和物料科目可供设置成90型成本要素&#xff1b; 物料科目设置为90型成…

假期学习【五】RDD编程实验四

今天完成了实验四的第二问和第三问 第二题 对于两个输入文件 A 和 B&#xff0c;编写 Spark 独立应用程序&#xff0c;对两个文件进行合并&#xff0c;并剔除其中重复的内容&#xff0c;得到一个新文件 C。下面是输入文件和输出文件的一个样例&#xff0c;供参考。输入文件 A 的…