枚举类

枚举类

一. 简介

enum关键字在 java5 中引入,表示一种特殊类型的类,其总是继承 java.lang.Enum 类。枚举类本质还是类,所有的枚举类型都是Enum的子类,所以枚举类型也可以实现接口。

枚举的一些特性:

  • 枚举类型不能被abstract或final修饰。
  • 枚举类型隐式的声明为final,除非它包含了一个具有类体的枚举常量(下文枚举用法2)。
  • 嵌套的枚举类型隐式的声明为static,可以冗余static,所以内部类中不能定义枚举类型,因为内部类不能有静态成员。
  • 使用 == 比较枚举类型。

toString() 方法返回枚举常量名,其逆方法是 valueOf()

1
2
String a = Size.SMALL.toString();
Size s = Enum.valueOf(Size.class, "SMALL");

所有枚举类型都有一个静态方法 values() ,其返回一个包含所有枚举值的数组。

1
Size[] values = Size.values();

枚举常用量代替常量类,其优点是使代码更具可读性,允许进行编译时检查,预先记录可接受值的列表,并避免由于传入无效值而引起的意外行为

二. 简单使用

2.1 使用枚举

最简单的枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum Status {
WAITING,
RUNNING,
FINISHED;
}

System.out.println(PizzaStatus.WAITING.name());
// WAITING
System.out.println(PizzaStatus.WAITING);
// WAITING
System.out.println(PizzaStatus.WAITING.name().getClass());
// class java.lang.String
System.out.println(PizzaStatus.WAITING.getClass());
// class xxx.Status

定义常量和方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public enum Size {
SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");

private String abbreviation;

private Size(String abbreviation){
this.abbreviation = abbreviation;
}

public String getAbbreviation() {
return abbreviation;
}
}

带类体的枚举常量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Operation {
PLUS {
double val(double x, double y){ return x + y; }
},MINUS {
double val(double x, double y){ return x - y; }
},TIMES {
double val(double x, double y){ return x * y; }
},DIVIDED_BY {
double val(double x, double y){ return x / y; }
};

abstract double val(double x,double y);

public static void main(String[] args){
double x = 0.5;
double y = 0.2;
for (Operation op : Operation.values())
System.out.println(x + " " + op + " " + y + " = " + op.val(x,y));
}
}

运行结果:

1
2
3
4
0.5 PLUS 0.2 = 0.7
0.5 MINUS 0.2 = 0.3
0.5 TIMES 0.2 = 0.1
0.5 DIVIDED_BY 0.2 = 2.5

2.2 switch支持

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
public class EnumTest {
enum Type { A,B,C,D }
static String test(Type a){
switch (a){
case A :
return "is A";
case B :
return "is B";
case C :
return "is C";
case D :
return "is D";
default :
throw new AssertionError("unknown type" + a);

}
}

public static void main(String[] args){
System.out.println(test(Type.A));
System.out.println(test(Type.B));
System.out.println(test(Type.C));
System.out.println(test(Type.D));
}
}

运行结果:

1
2
3
4
is A
is B
is C
is D

2.3 使用 == 比较枚举

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Work {
private Status status;

public boolean isWorking() {
return getStatus() == Status.RUNNING;
}

public Work(Status status) {
this.status = status;
}

public Status getStatus() {
return status;
}

public void setStatus(Status status) {
this.status = status;
}
}

...
public static void main(String[] args) throws Exception {
Work work1 = new Work(null);
Work work2 = new Work(null);
if(work1.getStatus().equals(work2.getStatus())) {
System.out.println("true"); // java.lang.NullPointerException
}
if(work1.getStatus() == work2.getStatus()) {
System.out.println("true"); // true
}
}
...

// 增加一个enum
public class Work {
private Status status;
private Type type;
...
}

public static void main(String[] args) throws Exception {
if(work1.getStatus().equals(work2.getType())) {
System.out.println("true");
}
if(work1.getStatus() == work2.getType()) { // 直接会在编译器提示类型不兼容
System.out.println("true");
}
}

使用 == 的优点:

  • == 不会引发NullPointerException,而equals则会。
  • 不同枚举类型比较 == 会在编译前报错,equals则一直不会报错。

2.4 EnumSet

  • EnumSet 是一种专门为枚举类型所设计的 Set 类型。
  • HashSet相比,由于使用了内部位向量表示,因此它是特定 Enum 常量集的非常有效且紧凑的表示形式。
  • 它提供了类型安全的替代方法,以替代传统的基于int的“位标志”,使我们能够编写更易读和易于维护的简洁代码。
  • EnumSet 是抽象类,其有两个实现:RegularEnumSetJumboEnumSet,选择哪一个取决于实例化时枚举中常量的数量。
  • 在很多场景中的枚举常量集合操作(如:取子集、增加、删除、containsAllremoveAll批操作)使用EnumSet非常合适;如果需要迭代所有可能的常量则使用Enum.values()
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
33
34
35
36
publicclass Pizza {

privatestatic EnumSet<PizzaStatus> undeliveredPizzaStatuses =
EnumSet.of(PizzaStatus.ORDERED, PizzaStatus.READY);

private PizzaStatus status;

publicenum PizzaStatus {
...
}

public boolean isDeliverable() {
returnthis.status.isReady();
}

public void printTimeToDeliver() {
System.out.println("Time to delivery is " +
this.getStatus().getTimeToDelivery() + " days");
}

public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
return input.stream().filter(
(s) -> undeliveredPizzaStatuses.contains(s.getStatus()))
.collect(Collectors.toList());
}

public void deliver() {
if (isDeliverable()) {
PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
.deliver(this);
this.setStatus(PizzaStatus.DELIVERED);
}
}

// Methods that set and get the status variable.
}

下面的测试演示了展示了 EnumSet 在某些场景下的强大功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() {
List<Pizza> pzList = new ArrayList<>();
Pizza pz1 = new Pizza();
pz1.setStatus(Pizza.PizzaStatus.DELIVERED);

Pizza pz2 = new Pizza();
pz2.setStatus(Pizza.PizzaStatus.ORDERED);

Pizza pz3 = new Pizza();
pz3.setStatus(Pizza.PizzaStatus.ORDERED);

Pizza pz4 = new Pizza();
pz4.setStatus(Pizza.PizzaStatus.READY);

pzList.add(pz1);
pzList.add(pz2);
pzList.add(pz3);
pzList.add(pz4);

List<Pizza> undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList);
assertTrue(undeliveredPzs.size() == 3);
}

2.5 EnumMap

EnumMap是一个专门化的映射实现,用于将枚举常量用作键。与对应的 HashMap 相比,它是一个高效紧凑的实现,并且在内部表示为一个数组:

1
EnumMap<Pizza.PizzaStatus, Pizza> map;

让我们快速看一个真实的示例,该示例演示如何在实践中使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
publicstatic EnumMap<PizzaStatus, List<Pizza>>
groupPizzaByStatus(List<Pizza> pizzaList) {
EnumMap<PizzaStatus, List<Pizza>> pzByStatus =
new EnumMap<PizzaStatus, List<Pizza>>(PizzaStatus.class);

for (Pizza pz : pizzaList) {
PizzaStatus status = pz.getStatus();
if (pzByStatus.containsKey(status)) {
pzByStatus.get(status).add(pz);
} else {
List<Pizza> newPzList = new ArrayList<Pizza>();
newPzList.add(pz);
pzByStatus.put(status, newPzList);
}
}
return pzByStatus;
}

下面的测试演示了展示了 EnumMap 在某些场景下的强大功能:

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
@Test
public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() {
List<Pizza> pzList = new ArrayList<>();
Pizza pz1 = new Pizza();
pz1.setStatus(Pizza.PizzaStatus.DELIVERED);

Pizza pz2 = new Pizza();
pz2.setStatus(Pizza.PizzaStatus.ORDERED);

Pizza pz3 = new Pizza();
pz3.setStatus(Pizza.PizzaStatus.ORDERED);

Pizza pz4 = new Pizza();
pz4.setStatus(Pizza.PizzaStatus.READY);

pzList.add(pz1);
pzList.add(pz2);
pzList.add(pz3);
pzList.add(pz4);

EnumMap<Pizza.PizzaStatus,List<Pizza>> map = Pizza.groupPizzaByStatus(pzList);
assertTrue(map.get(Pizza.PizzaStatus.DELIVERED).size() == 1);
assertTrue(map.get(Pizza.PizzaStatus.ORDERED).size() == 2);
assertTrue(map.get(Pizza.PizzaStatus.READY).size() == 1);
}

三. 深入解析

3.1 Enum源码

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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<E> self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}

@SuppressWarnings("unchecked")
public final Class<E> getDeclaringClass() {
Class<?> clazz = getClass();
Class<?> zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)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);
}

@SuppressWarnings("deprecation")
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");
}
}

3.2 枚举的实现

  • 编译器在编译枚举类时会将其转换为abstract抽象类,并继承Enum类。
  • 枚举中定义的枚举常量变为了 public static final 成员属性。
  • 类型即枚举类型所转抽象类,属性名即常量名,通过内部类实现每个枚举常量,继承抽象类,枚举常量通过静态代码块来进行初始化,即在类的加载阶段进行初始化。
  • clone()readObject()writeObject() 三个方法声明为final,保证每个枚举类型和枚举常量都是不可变的,保证了其线程安全。

3.3 枚举的序列化

  • 枚举的方式实现单例模式是六种实现中最被人提倡的方式,因为其实现简单,以及保证了序列化和线程安全
  • 枚举通过序列化时返回的原对象的name属性,反序列化时通过 valueOf 查找枚举对象。禁用了一系列方法保证这种序列化机制不会被破坏。
  • 更多内容可以看此篇博客IO和NIO中序列化部分。

3.4 线程安全问题

首先枚举常量都是静态资源,也就是在类加载时初始化,且不可变,所以其是线程安全的。

3.5 枚举实现设计模式

(1)单例模式

《Effective Java 》和《Java与模式》都非常推荐使用枚举类来实现单例模式的方式

这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton的最佳方法。—-《Effective Java 中文版 第二版》

《Java与模式》中,作者这样写道,使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。

下面的代码段显示了如何使用枚举实现单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum PizzaDeliverySystemConfiguration {
INSTANCE;
PizzaDeliverySystemConfiguration() {
// Initialization configuration which involves
// overriding defaults like delivery strategy
}

private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL;

public static PizzaDeliverySystemConfiguration getInstance() {
return INSTANCE;
}

public PizzaDeliveryStrategy getDeliveryStrategy() {
return deliveryStrategy;
}
}

使用:

1
PizzaDeliveryStrategy deliveryStrategy = PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy();

通过 PizzaDeliverySystemConfiguration.getInstance() 获取的就是单例的 PizzaDeliverySystemConfiguration

(2)策略模式

通常,策略模式由不同类实现同一个接口来实现的。这也就意味着添加新策略意味着添加新的实现类。使用枚举,可以轻松完成此任务,添加新的实现意味着只定义具有某个实现的另一个实例。

枚举实现策略模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
publicenum PizzaDeliveryStrategy {
EXPRESS {
@Override
public void deliver(Pizza pz) {
System.out.println("Pizza will be delivered in express mode");
}
},
NORMAL {
@Override
public void deliver(Pizza pz) {
System.out.println("Pizza will be delivered in normal mode");
}
};

public abstract void deliver(Pizza pz);
}

Pizza增加下面的方法:

1
2
3
4
5
6
7
public void deliver() {
if (isDeliverable()) {
PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
.deliver(this);
this.setStatus(PizzaStatus.DELIVERED);
}
}

使用:

1
2
3
4
5
6
7
@Test
public void givenPizaOrder_whenDelivered_thenPizzaGetsDeliveredAndStatusChanges() {
Pizza pz = new Pizza();
pz.setStatus(Pizza.PizzaStatus.READY);
pz.deliver();
assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED);
}

3.6 枚举类与Json

使用Jackson库,可以将枚举类型的JSON表示为POJO。下面的代码段显示了可以用于同一目的的Jackson批注:

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
33
34
35
36
37
38
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum PizzaStatus {
ORDERED (5){
@Override
public boolean isOrdered() {
returntrue;
}
},
READY (2){
@Override
public boolean isReady() {
returntrue;
}
},
DELIVERED (0){
@Override
public boolean isDelivered() {
returntrue;
}
};

privateint timeToDelivery;

public boolean isOrdered() {returnfalse;}

public boolean isReady() {returnfalse;}

public boolean isDelivered(){returnfalse;}

@JsonProperty("timeToDelivery")
public int getTimeToDelivery() {
return timeToDelivery;
}

private PizzaStatus (int timeToDelivery) {
this.timeToDelivery = timeToDelivery;
}
}

我们可以按如下方式使用 PizzaPizzaStatus

1
2
3
Pizza pz = new Pizza();
pz.setStatus(Pizza.PizzaStatus.READY);
System.out.println(Pizza.getJsonString(pz));

生成 Pizza 状态以以下JSON展示:

1
2
3
4
5
6
7
8
9
{
"status" : {
"timeToDelivery" : 2,
"ready" : true,
"ordered" : false,
"delivered" : false
},
"deliverable" : true
}

3.7 案例

可以通过在枚举类型中定义属性,方法和构造函数让它变得更加强大。下面通过一个实际的例子展示一下,当调用短信验证码的时候可能有几种不同的用途,在下面这样定义:

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
public enum PinType {

REGISTER(100000, "注册使用"),
FORGET_PASSWORD(100001, "忘记密码使用"),
UPDATE_PHONE_NUMBER(100002, "更新手机号码使用");

privatefinalint code;
privatefinal String message;

PinType(int code, String message) {
this.code = code;
this.message = message;
}

public int getCode() {
return code;
}

public String getMessage() {
return message;
}

@Override
public String toString() {
return"PinType{" +
"code=" + code +
", message='" + message + '\'' +
'}';
}
}

实际使用:

1
2
3
System.out.println(PinType.FORGET_PASSWORD.getCode()); // 100001
System.out.println(PinType.FORGET_PASSWORD.getMessage()); // 忘记密码使用
System.out.println(PinType.FORGET_PASSWORD.toString()); // PinType{code=100001, message='忘记密码使用'}

参考:

🔗《Java语言规范》 8.9 枚举类型

🔗 用好 Java 中的枚举

🔗 Java枚举类型的实现原理

🔗 Java 枚举与单例

🔗 深度分析Java的枚举类型—-枚举的线程安全性及序列化问题