应用场景

枚举通常用来列举一个类型的有限实例集合,我们可以使用常量集来实现,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() && // optimizationself.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;}// 接口方法@Overridepublic void print() {System.out.println(this.index + ":" + this.name);}// 覆盖方法@Overridepublic 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());}}
}  

java 枚举源码解析相关推荐

  1. Java Executor源码解析(3)—ThreadPoolExecutor线程池execute核心方法源码【一万字】

    基于JDK1.8详细介绍了ThreadPoolExecutor线程池的execute方法源码! 上一篇文章中,我们介绍了:Java Executor源码解析(2)-ThreadPoolExecutor ...

  2. Java Executor源码解析(7)—Executors线程池工厂以及四大内置线程池

    详细介绍了Executors线程池工具类的使用,以及四大内置线程池. 系列文章: Java Executor源码解析(1)-Executor执行框架的概述 Java Executor源码解析(2)-T ...

  3. Java SPI 源码解析及 demo 讲解

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:Java实现QQ登录和微博登录个人原创+1博客:点击前往,查看更多 作者:JlDang 来源:https://s ...

  4. Java HashSet源码解析

    本解析源码来自JDK1.7,HashSet是基于HashMap实现的,方法实现大都直接调用HashMap的方法 另一篇HashMap的源码解析文章 概要 实现了Set接口,实际是靠HashMap实现的 ...

  5. Java String源码解析

    String类概要 所有的字符串字面量都属于String类,String对象创建后不可改变,因此可以缓存共享,StringBuilder,StringBuffer是可变的实现 String类提供了操作 ...

  6. Java Thread 源码解析

    Thread 源码解析 线程的方法大部分都是使用Native使用,不允许应用层修改,是CPU调度的最基本单元.线程的资源开销相对于进程的开销是相对较少的,所以我们一般创建线程执行,而不是进程执行. T ...

  7. java time sleep_如何优雅地让线程休眠?Java sleep源码解析

    欢迎大家搜索"小猴子的技术笔记"关注我的公众号,有问题可以及时和我交流. 在学习Java多线程的时候,经常会使用"sleep(long millis)"方法让线 ...

  8. Java TreeMap 源码解析

    继上篇文章介绍完了HashMap,这篇文章开始介绍Map系列另一个比较重要的类TreeMap. 大家也许能感觉到,网络上介绍HashMap的文章比较多,但是介绍TreeMap反而不那么多,这里面是有原 ...

  9. Java 正则表达式源码解析

    使用方法 Pattern p = Pattern.compile("a*");Matcher m = p.matcher("aaaa");if (m.find( ...

最新文章

  1. java_泛型 TreeSet 判断hashcode/length(升序排列)
  2. ssh suse 配置_SUSE+linux+配置节点间的SSH信任关系
  3. 机器人学习--Robotics: Estimation and Learning(宾夕法尼亚大学COURSERA课程)
  4. boost::core实现交换std::type_info
  5. 2 shell 锂基脂_壬二酸和癸二酸制备的复合锂基脂到底有那些差别!
  6. 解决网站请求速度慢的一些方法
  7. 将C#中DateTime类型转化为JavaScript中的Date类型
  8. 使用php,使用 PHP
  9. Redis-Bitmap介绍及使用
  10. 中国石化:五年要建充换电站5000座
  11. Mybatis_day4_Mybatis的注解开发
  12. Django 视图层
  13. 第一次作业:对于Linux2.6.0源码中进程模型的分析
  14. (转)linux口令相关文件(/etc/passwd和/etc/shadow)
  15. 2017年二级计算机c真题语言,2017全国计算机二级C考试真题
  16. 利用Python实现人脸识别,制作天网系统
  17. PHP 返回结果给前端/ajax后,在后台继续执行代码的方法
  18. win7/win10上安装谷歌官方无广告的安卓模拟器 - Android Studio - 下载安装AVD虚拟机
  19. 数字化转型微漫画丨商品、渠道、供应同质化严重,企业如何在竞争中取胜
  20. uniapp 微信小程序 api上传图片 binary (form Data)

热门文章

  1. Keepalived+Nginx实现高可用,反向代理---模拟实现线上环境
  2. 5行代码秀碾压,比Keras还好用的fastai来了,尝鲜PyTorch 1.0必备伴侣
  3. 摄像头训练的吃豆人,我还是没活几集 | TensorFlow.js
  4. 特斯拉Model 3产能跟不上,是因为用了太多机器人
  5. KikaGO:一条数据线的AI之旅
  6. 一个优雅地探索相关性的新可视化方法
  7. 为什么要用 SpringMVC 的 SessionStatus
  8. DHCP服务器如何检测穿过中继代理的IP地址冲突(gratuitous ARP肯定是不行的)
  9. Katta:基于Lucene可伸缩分布式实时搜索方案
  10. [ERROR] InnoDB: ibdata1 different size (rounded down to MB)