java 枚举源码解析

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 应用场景枚举通常用来列举一个类型的有限实例集合,我们可以使用常量集来实现,jdk1.5添加了枚举(enum)支持,解决了常量集的一些缺陷常量集中的变量不会必然在指定的范围内常量能够提供的功能很少,难于使用常量意义不明确,没有名字修改或增加枚举值后需要修改的代码多,不便于维护关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的组件使用。

应用场景

枚举通常用来列举一个类型的有限实例集合,我们可以使用常量集来实现,jdk1.5添加了枚举(enum)支持,解决了常量集的一些缺陷

  • 常量集中的变量不会必然在指定的范围内
  • 常量能够提供的功能很少,难于使用
  • 常量意义不明确,没有名字
  • 修改或增加枚举值后需要修改的代码多,不便于维护

关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的组件使用。

枚举源码

首先我们定义一个枚举类 Explore.java

public enum Explore {
    HERE, THERE
}  

然后编译 javac Explore.java,反编译javap Explore,得到反编译的结果:

Compiled from "Explore.java"
public final class Explore extends java.lang.Enum<Explore> {
    public static final Explore HERE;
    public static final Explore THERE;
    public static Explore[] values();
    public static Explore valueOf(java.lang.String);
    static {};
}    

我们看到当我们定义一个枚举,编译器其实是为我们创建了一个继承自Emum的类

  • 枚举实例对应新类中的static final 变量
  • 该类为 final类型,不可被继承
  • 增加了两个方法
    • valueOf(String) 从String构造枚举类型
    • values() 返回一个由枚举对象构成的数组
  • 添加了一个静态初始化器 static{},用来初始化枚举实例,和枚举实例数组,也就是 values()返回数组

Enum类

Enum作为枚举类的公共基类有以下的特点

  • 构造器私有(保护)
  • 关键域为ordinal来指示枚举对象被声明的顺序,name用来给出声明时的合理描述,序列化时只输出name属性,用valueOf(String)通过名字来进行反序列化
  • 该类中的valueOf()方法和编译器生成的valueOf方法签名不同
  • 禁止了基础的序列化方法,调用readObject()和writeObject()时抛出异常
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
    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;
    }
    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 self = this;
        if (self.getClass() != other.getClass() && // optimization
                self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }
    public final Class<E> getDeclaringClass() {
        Class clazz = getClass();
        Class zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? clazz : zuper;
    }
    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);
    }
    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");
    }
}  

序列化

序列化和反序列化保证了每一个枚举类型极其定义的枚举变量在JVM中都是唯一的

  • 在序列化的时候java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候通过 java.lang.Enum的valueOf方法来根据名字查找枚举对象
  • 通过私有化并且直接抛出异常来禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,保证了序列化机制不会被定制
  • 尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法enumConstants = (T[])values.invoke(null);,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。由于每次反序列化都是获取静态数组中的对象的引用,所以不会创建新的对象,从而保证了单例模式

NOTE
在了解了Java如何处理枚举的定义以及序列化和反序列化枚举类型之后,我们就需要在系统或者类库升级时,对其中定义的枚举类型多加注意,为了保持代码上的兼容性,如果我们定义的枚举类型有可能会被序列化保存(放到文件中、保存到数据库中,进入分布式内存缓存中),那么我们是不能够删除原来枚举类型中定义的任何枚举对象的,否则程序在运行过程中,JVM就会抱怨找不到与某个名字对应的枚举对象了。另外,在远程方法调用过程中,如果我们发布的客户端接口返回值中使用了枚举类型,那么服务端在升级过程中就需要特别注意。如果在接口的返回结果的枚举类型中添加了新的枚举值,那就会导致仍然在使用老的客户端的那些应用出现调用失败的情况。因此,针对以上两种情况,应该尽量避免使用枚举,如果实在要用,也需要仔细设计,因为一旦用了枚举,有可能会给后期维护带来隐患。

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

一个测试序列化一致性的例子

本例中,我们通过socket将枚举类型写入到网络中,然后在另一端用readObject()方法读取,最后判断反序列化后的对象与本地枚举对象的相等性

  • 如果 == 返回true,说明并没有创建新的对象,确实保证了单例模式
  • 如果 equals()返回true,说明反序列化后只是语义相同,没有保证单例模式
// 枚举类型定义
public enum WeekDayEnum {
    Mon(1), Tue(2), Wed(3), Thu(4), Fri(5), Sat(6), Sun(7);
    private int index;
    WeekDayEnum(int idx) {
        this.index = idx;
    }
    public int getIndex() {
        return index;
    }
}  
// 客户端代码
public class EnumerationClient {
    public static void main(String... args) throws UnknownHostException, IOException {
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress("127.0.0.1", 8999));
        OutputStream os = socket.getOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(os);
        oos.writeObject(WeekDayEnum.Fri);
        oos.close();
        os.close();
        socket.close();
    }
}  
// 服务器端代码
public class EnumerationServer {
    public static void main(String... args) throws IOException, ClassNotFoundException {
        ServerSocket server = new ServerSocket(8999);
        Socket socket = server.accept();
        InputStream is = socket.getInputStream();
        ObjectInputStream ois = new ObjectInputStream(is);
        WeekDayEnum day = (WeekDayEnum) ois.readObject();
        if (day == WeekDayEnum.Fri) {
            System.out.println("client Friday enum value is same as server's");
        } else if (day.equals(WeekDayEnum.Fri)) {
            System.out.println("client Friday enum value is equal to server's");
        } else {
            System.out.println("client Friday enum value is not same as server's");
        }
        ois.close();
        is.close();
        socket.close();
    }
} 

线程安全

由于枚举类型的对象是static,并且在static块中初始化,所以由JVM的ClassLoader机制保证了线程安全性。

枚举的常见用法

主要API

public class TestEnum {
    public static void main(String[] args) {
        Fruit[] values = Fruit.values();
        for (Fruit fruit : values) {
            System.out.println(fruit);
            printEnum(fruit);
        }
    }
    public enum Fruit {
        APPLE, ORANGE, WATERMELON
    }
    public static void printEnum(Fruit fruit) {
        System.out.println(fruit + " ordinal:" + fruit.ordinal());
        System.out.println("compare to apple" + fruit.compareTo(Fruit.APPLE));
        System.out.println(fruit == Fruit.ORANGE);
        System.out.println(fruit.getDeclaringClass());
        System.out.println(fruit.name());
    }
}  

switch

java的switch语法,是通过jvm的tableswitch和lookupswitch两个指令实现。java编译器为switch语句编译成一个局部变量数组,每个case对应一个数组的索引,指令的执行是通过不同的数组索引找到不同的入口指令。所以原则上switch…case只能处理int型的变量。
enum能用在switch语句中,也是一个语法糖,我们知道所有枚举类的父类Enum中有一个private final int ordinal;,java编译器检测到switch语句中变量是一个枚举类,则会利用之前枚举类的ordinal属性,编译一个局部变量数组,后续在进行case分支比较的时候,就是简单通过tableswitch或lookupswitch指令来进行跳转,需要注意的一点:这个局部变量数组的构建过程是在编译器在编译阶段完成的。

注意在switch中不需要用类型名来指定枚举对象(Single.RED),而是直接用类型名RED,在此时case语句不需要类限定前缀,完全是java编译器的限制(编译器是不需要枚举类的前缀,只需要枚举类编译的static int[] $SWITCH_TABLE

enum Signal {
    GREEN, YELLOW, RED
}
public class TrafficLight {
    Signal color = Signal.RED;
    public void change() {
        switch (color) {
            case RED :
                color = Signal.GREEN;
                break;
            case YELLOW :
                color = Signal.RED;
                break;
            case GREEN :
                color = Signal.YELLOW;
                break;
        }
    }
}  

像正常类一样扩展枚举类型

  • 定义私有构造函数
  • 自定义普通方法
  • 覆盖Enum中的方法
  • 实现接口方法

由于java不支持多继承,所有枚举类型都继承自java.lang.Enum,所以enum类型只能实现接口,而不能继承类

public enum Color implements Behaviour {
    RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
    // 成员变量
    private String name;
    private int index;
    // 构造方法
    private Color(String name, int index) {
        this.name = name;
        this.index = index;
    }
    // 普通方法
    public static String getName(int index) {
        for (Color c : Color.values()) {
            if (c.index == index) {
                return c.name;
            }
        }
        return null;
    }
    // 接口方法
    @Override
    public void print() {
        System.out.println(this.index + ":" + this.name);
    }
    // 覆盖方法
    @Override
    public String toString() {
        return this.index + "_" + this.name;
    }
}
interface Behaviour {
    void print();
}  

接口组织枚举类型

用接口来组织多层枚举

public class FoodTest {
    public static void main(String[] args) {
        Food f = Food.Coffee.BLACK_COFFEE;
        System.out.println(f);
    }
}
interface Food {
    enum Coffee implements Food {
        BLACK_COFFEE, DECAF_COFFEE, LATTE, CAPPUCCINO
    }
    enum Dessert implements Food {
        FRUIT, CAKE, GELATO
    }
    int i = 1;
}  

枚举专用集合类

java.util.EnumSet和java.util.EnumMap是两个枚举集合。EnumSet保证集合中的元素不重复;EnumMap中的key是enum类型,而value则可以是任意类型。集合类利用枚举类型的ordinal域来进行组织,用法和普通Map,Set类似,只是类型限定为Enum类型。

public enum Weeks {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURADAY, SUNDAY
} 
public class EnumSetTest {
    public static void main(String[] args) {
        EnumSet<Weeks> week = EnumSet.noneOf(Weeks.class);
        week.add(Weeks.MONDAY);
        System.out.println("EnumSet中的元素:" + week);
        week.remove(Weeks.MONDAY);
        System.out.println("EnumSet中的元素:" + week);
        week.addAll(EnumSet.complementOf(week));
        System.out.println("EnumSet中的元素:" + week);
        week.removeAll(EnumSet.range(Weeks.MONDAY, Weeks.THURSDAY));
        System.out.println("EnumSet中的元素:" + week);
    }
} 
public enum Course {
    ONE, TWO, THREE
}  
public class EnumMapTest {
    public static void main(String[] args) {
        EnumMap<Course, String> map = new EnumMap<Course, String>(Course.class);
        map.put(Course.ONE, "语文");
        map.put(Course.ONE, "政治");
        map.put(Course.TWO, "数学");
        map.put(Course.THREE, "英语");
        for (Entry<Course, String> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}  
目录
相关文章
|
4天前
|
数据采集 人工智能 Java
Java产科专科电子病历系统源码
产科专科电子病历系统,全结构化设计,实现产科专科电子病历与院内HIS、LIS、PACS信息系统、区域妇幼信息平台的三级互联互通,系统由门诊系统、住院系统、数据统计模块三部分组成,它管理了孕妇从怀孕开始到生产结束42天一系列医院保健服务信息。
19 4
|
11天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
39 2
|
2天前
|
数据采集 存储 Web App开发
Java爬虫:深入解析商品详情的利器
在数字化时代,信息处理能力成为企业竞争的关键。本文探讨如何利用Java编写高效、准确的商品详情爬虫,涵盖爬虫技术概述、Java爬虫优势、开发步骤、法律法规遵守及数据处理分析等内容,助力电商领域市场趋势把握与决策支持。
|
6天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
7天前
|
Java 测试技术 API
Java 反射机制:深入解析与应用实践
《Java反射机制:深入解析与应用实践》全面解析Java反射API,探讨其内部运作原理、应用场景及最佳实践,帮助开发者掌握利用反射增强程序灵活性与可扩展性的技巧。
|
11天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
1月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
70 0
|
1月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
57 0
|
1月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
62 0
|
1月前
|
安全 Java 程序员
Collection-Stack&Queue源码解析
Collection-Stack&Queue源码解析
84 0
下一篇
无影云桌面