概要
本篇博客的内容如下
- 使用背景
- 枚举定义和使用
- 深度分析Java的枚举类型—-枚举的线程安全性及序列化问题
1.使用背景
在java语言还没有引入枚举类型之前,表示枚举类型的常用模式是声明一组int常量。通常利用public final static
方法定义。下面是一个小案例,分别用1表示春天、2表示夏天、3表示秋天、4表示冬天。
1 | public class Season { |
这种方式称作int枚举模式。但是这种模式有一些问题。通常我们写出的代码都会从以下三个方面来进行分析,安全性、易用性和可读性。首先我们考虑一下他的安全性。当然这种模式不是类型安全的。比如我们设计一个函数,要求传入春夏秋冬的某个值。但是使用int类型,我们无法保证传入的值为合法。下面代码就展示了一种可以传递没有定义的值:
1 | private String getChineseSeason(int season){ |
程序getChineseSeason(Season.SPRING)
是我们预期的使用方法。可getChineseSeason(5)
显然就不是了,而且编译很通过,在运行时会出现什么情况,我们就不得而知了。这显然就不符合Java程序的类型安全。
接下来我们考虑一下这种模式的可读性。使用枚举的大多数场合,我们都需要方便打印枚举类型的字符串表达式。如果将int枚举常量打印出来,我们所见到的就是一组数字,这是没什么太大的用处。我们可能会想到是一个String常量来代替int常量。虽然他为这些常量提供了可打印的字符串,但是他会导致性能问题,因为他依赖于字符串的比较操作,所以这种模式也是我们不期望的。从类型安全性和程序可读性俩方面考虑,int和String枚举模式的缺点就显露出来。幸运的是,从Java1.5
发行版本开始,就提出了另一种可以替代的解决方案,可以避免int
和String
枚举模式的缺点,并提供了许多额外的好处。那就是枚举类型(enum type
)。接下来的章节将介绍枚举类型的定义、特征、应用场景和优缺点。
2.枚举定义和使用
枚举类型(enum type)是指由一组固定的常量组成合法的类型。java中由关键字enum来定义一个枚举类型。下面就是java枚举类型的定义:
1 | publi enum Season{ |
枚举的用法主要由以下几种:
常量,就如上面定义那样使用
switch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19enum 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;
}
}
}向枚举中添加新的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public enum Color {
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.getIndex() == index) {
return c.name;
}
}
return null;
}
// get set 方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}覆盖枚举的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public enum Color {
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 String toString() {
return this.index+"_"+this.name;
}
}实现接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public interface Behaviour {
void print();
String getInfo();
}
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 String getInfo() {
return this.name;
}
//接口方法
public void print() {
System.out.println(this.index+":"+this.name);
}
}使用接口组织枚举
1
2
3
4
5
6
7
8public interface Food {
enum Coffee implements Food{
BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO
}
enum Dessert implements Food{
FRUIT, CAKE, GELATO
}
}常量相关的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14public enum Constantmethod{
DATE_TIME{
String getInfo(){
System.out.println("date_time");
}
}
CLASSPATH{
String getInfo(){
System.out.println("classpath");
}
}
//定义每个常量都要实现的方法
abstract String getInfo();
}
基本使用
1 | public enum Season { |
枚举使用总结
其实枚举的定义很像类的定义,只不过枚举有一些限制,你甚至可以在枚举里面写main方法,这也是可以运行通过的。不过枚举不能继承类,至于原因接下来的文章会讲到,但是枚举可以实现接口,就像上面展示的用法一样。
3.深度分析Java的枚举类型—-枚举的线程安全性及序列化问题
枚举是如何保证线程安全
在java中一切都是对象,每个类都是继承自Object对象,那么枚举类型到底是什么类呢?是enum吗?答案很明显不是,enum就像class一样,只是一个关键字,他并不是一个类,那么枚举是由什么类维护的呢,我们简单写一个枚举:
1 | public enum t { |
然后我们使用反编译,看看这段代码到底是怎么实现的,反编译(Java的反编译)后,代码内容如下:
1 | public final class T extends Enum |
通过反编译后代码我们可以看到,public final class T extends Enum
,说明,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。当我们使用enmu
来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承,我们看到这个类中有几个属性和方法:
1 | public static final T SPRING; |
都是static类型,因为static类型的属性会在类被加载之后就会被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。保证只会被一个类初始化,并且是final类型的,后面值也不会被改变。
为什么用枚举实现的单例是最好的方式
在单例模式的七种写法中,我们看到一共有七种实现单例的方式,其中,Effective Java作者Josh Bloch
提倡使用枚举的方式,既然大神说这种方式好,那我们就要知道它为什么好?
1. 枚举写法简单
写法简单这个大家看看单例模式的七种写法里面的实现就知道为什么枚写法简单。
1 | public enum EasySingleton{ |
你可以通过EasySingleton.INSTANCE
来访问。
2. 枚举自己处理序列化
我们知道,以前的所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。
原文如下:
1 | Enum constants are serialized differently than ordinary serializable or externalizable |
大概意思就是说,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我们看一下这个valueOf
方法:
1 | public static <T extends Enum<T T valueOf(Class<T enumType,String name) { |
从代码中可以看到,代码会尝试从调用enumType
这个Class
对象的enumConstantDirectory()
方法返回的map
中获取名字为name
的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()
方法,就会发现到最后会以反射的方式调用enumType
这个类型的values()
静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType
这个Class
对象中的enumConstantDirectory
属性。
所以,JVM对序列化有保证。
3.枚举实例创建是thread-safe(线程安全的)
当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。