之前在 单例模式 这一篇中有提到可以使用枚举实现单例,并简单介绍了一下原理,这里讲讲平时在开发中使用枚举的一些常见姿势,学以致用!

关于枚举的一些知识点

枚举 enum 关键字是在 Java 5 中引入,是隐式继承自 java.lang.Enum 的一种特殊的类,通常被用于常量定义,相对于 public static final 这种方式,使用枚举常量可以使代码更易读、支持编译时类型检测避免非法参数等好处,此外枚举类型还自带一些有用的方法可直接使用。下面是枚举类型的一些特点:

  • 所有的枚举类都隐式的继承了 Enum 抽象类,因此无法再继承其它类,但是可以实现接口;但是你自己却不能显示的定义一个继承了 Enum 的类,Javac 编译器禁止这种行为

  • 枚举类是类型安全的,你不能给一个指向枚举的引用赋值其它任何类型的变量

  • 可以通过 MyEnum.values() 获取到所有该枚举类型的实例数组,其顺序与定义的顺序保持一致;ordinal() 方法则返回枚举在该数组中的下标

  • Enum 常量隐式的是 static 和 final 的,这个可以从之前的字节码解析文件中看到

  • Enum 可以直接使用 == 比较,这时因为 enum types ensure that only one instance of the constants exist in the JVM

  • 可以用于 switch 语句

  • Enum 实例只能从内部创建而不能通过 new 创建,因为其构造函数是私有的

  • 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
public enum GenderEnum {
MALE(1, "男"), FEMALE(2, "女");

private String name;
private int code;

private static final Map<Integer, GenderEnum> GENDER_ENUM_MAP = new HashMap<>();

static {
Arrays.stream(GenderEnum.values()).forEach(genderEnum -> GENDER_ENUM_MAP.put(genderEnum.code, genderEnum));
}

GenderEnum(int code, String name) {
this.name = name;
this.code = code;
}

public static GenderEnum getByCode(int code) {
return GENDER_ENUM_MAP.get(code);
}

public static String getNameByCode(int code) {
return GENDER_ENUM_MAP.containsKey(code) ? GENDER_ENUM_MAP.get(code).name : null;
}
}

支持定义方法,同时支持方法的重写

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
public enum ColorEnum {
RED {
@Override
public void paint() {
System.out.println("red");
}
},
BLUE {
@Override
public void paint() {
System.out.println("blue");
}
},
DEFAULT;

public void paint() {
System.out.println("default");
}

public static void main(String[] args) {
DEFAULT.paint();
RED.paint();
BLUE.paint();

}
}

通过 javac 编译该枚举可以得到三个 class 文件:ColorEnum$1.class ColorEnum$2.class ColorEnum.class。再次执行 javap -v ColorEnum$1.class 可以看到一行 public class ColorEnum extends java.lang.Enum<ColorEnum>,可知 RED 和 BLUE 其实是 ColorEnum 类的匿名内部类,他们都继承了 ColorEnum 类,因此是子类对父类方法的重写。但是注意 Enum 类都是 final 的,按理说不能够被继承,从现象来看可能是编译器对内部类继承 final 类的限制有所不同。

通过实现接口对枚举进行分组

我们知道枚举类不能被继承,因为它是 final 的,那么枚举类能继承其它类吗?答案也是不能,因为其已经隐式的继承了 Enum 抽象类,但是这对枚举类实现接口没有任何限制,因此可能通过实现接口对同一类枚举进行分组,同时用接口去规范枚举类里面的方法。

1
2
3
4
5
6
7
8
9
10
11
public interface Food {
enum Appetizer implements Food{
SALAD, SOUP, SPRING_ROLLS
}
enum Coffee implements Food{
BLACK_COFFEE, TEA, LATTE
}
enum Dessert implements Food{
FRUIT, GELATO, LASAGNE
}
}

EnumSet

The EnumSet is a specialized Set implementation meant to be used with Enum types.

EnumSet 是专门为枚举常量设计的一种集合实现,针对枚举类 EnumSet 要比 HashSet 的效率更高,因为它底层使用了 Bit Vector Representation 的结构。EnumSet 是一个抽象类,有 2 个默认实现 RegularEnumSetJumboEnumSet,内部会根据常量的数量在两者之间自动转换。

EnumMap

如果 Map 的 Key 类型是枚举类型的话, 相比于 HashMap,更推荐使用 EnumMap。EnumMap 的 key 只能是 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
public enum 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 class 中添加下面的方法

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

//测试

@Test
public void givenPizaOrder_whenDelivered_thenPizzaGetsDeliveredAndStatusChanges() {
Pizza pz = new Pizza();
pz.setStatus(Pizza.PizzaStatus.READY);
pz.deliver();
assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED);
}

参考