枚举
字数: 0 字 时长: 0 分钟
Java 枚举
字数: 0 字 时长: 0 分钟
枚举(Enum)是 Java 5 引入的一种特殊类型,用来表示:
数量有限、取值固定的一组常量。
例如运动员类型只有:
网球
足球
篮球就可以定义成枚举:
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}相比直接使用:
String type = "football";枚举具有更强的:
类型安全
可读性
可维护性例如:
PlayerType type =
PlayerType.FOOTBALL;这里 type 只能使用:
PlayerType定义好的枚举常量,不容易出现:
FOOTBALL
football
footBall
fotball这种字符串拼写问题。
1. 枚举的本质
Java 中的枚举并不是简单的几个常量。
每一个枚举类型,本质上都是一种特殊的类,并且都会直接继承:
java.lang.Enum例如:
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}从概念上可以理解为编译器帮我们生成了类似这样的结构:
public final class PlayerType
extends Enum<PlayerType> {
public static final PlayerType TENNIS;
public static final PlayerType FOOTBALL;
public static final PlayerType BASKETBALL;
public static PlayerType[] values() {
// ...
}
public static PlayerType valueOf(
String name) {
// ...
}
}编译器还会负责创建:
TENNIS
FOOTBALL
BASKETBALL这些枚举对象。
因此我们不需要自己写:
new PlayerType(...)实际上外部也不能直接:
new PlayerType()创建枚举对象。
需要注意:
反编译工具看到的具体代码只是编译器生成结果的一种表现形式,不同 JDK 和反编译器生成的代码可能不同。
真正需要记住的是:
enum
↓
一种特殊的类
↓
继承 java.lang.Enum
↓
枚举常量都是该枚举类型的固定实例2. 枚举常量
定义:
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}其中:
TENNIS
FOOTBALL
BASKETBALL都是:
PlayerType类型的对象。
可以直接:
PlayerType type =
PlayerType.BASKETBALL;不能:
// 编译错误
// PlayerType type = "BASKETBALL";这也是枚举相比字符串常量更加类型安全的原因。
可以简单理解为:
PlayerType
│
├── TENNIS
├── FOOTBALL
└── BASKETBALL3. values() 和 valueOf()
Java 编译器会为枚举生成一些常用方法。
3.1 values()
PlayerType.values()可以获取枚举中的所有常量。
例如:
for (PlayerType type :
PlayerType.values()) {
System.out.println(type);
}输出:
TENNIS
FOOTBALL
BASKETBALL所以:
values()很适合:
遍历枚举
生成下拉选项
初始化数据等场景。
3.2 valueOf()
PlayerType.valueOf(
"FOOTBALL"
)可以根据枚举常量名称获取对应枚举对象。
例如:
PlayerType type =
PlayerType.valueOf(
"FOOTBALL"
);
System.out.println(type);输出:
FOOTBALL需要注意:
valueOf()要求名称必须完全匹配。
例如:
PlayerType.valueOf(
"football"
);会抛出:
IllegalArgumentException因为:
FOOTBALL和:
football不是同一个名称。
4. 枚举可以定义在类内部
枚举本身也是一种类型,因此可以定义在其他类内部。
例如:
public class Player {
private PlayerType type;
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}
public boolean isBasketballPlayer() {
return type ==
PlayerType.BASKETBALL;
}
public PlayerType getType() {
return type;
}
public void setType(
PlayerType type) {
this.type = type;
}
}使用:
Player.PlayerType type =
Player.PlayerType.BASKETBALL;这样可以把:
PlayerType的作用范围和:
Player联系得更加紧密。
适合:
这个枚举只服务于某一个类或者某一组相关业务时。
5. 枚举比较
枚举通常推荐直接使用:
==比较。
例如:
if (player.getType()
== Player.PlayerType.BASKETBALL) {
System.out.println(
"篮球运动员"
);
}这是安全的。
因为一个枚举常量在当前 JVM 中对应的就是固定的枚举实例。
例如:
PlayerType.BASKETBALL不会每次访问都创建一个新的对象。
因此:
type1 == type2可以直接判断它们是不是同一个枚举常量。
5.1 为什么推荐使用 ==
枚举的:
equals()本身也采用对象身份进行比较,因此:
type1.equals(type2)通常也能得到正确结果。
但:
==有两个明显优点。
第一,不怕左侧为:
null例如:
PlayerType type = null;
System.out.println(
type ==
PlayerType.BASKETBALL
);结果:
false不会抛出异常。
而:
type.equals(
PlayerType.BASKETBALL
);会抛出:
NullPointerException第二,== 会进行类型检查。
例如两个完全不同的枚举类型不能随便通过 == 比较。

所以枚举比较通常直接写:
type ==
PlayerType.BASKETBALL即可。
6. 枚举与 switch
枚举非常适合搭配:
switch使用。
例如:
public static String getPlayerName(
PlayerType playerType) {
switch (playerType) {
case TENNIS:
return "网球运动员费德勒";
case FOOTBALL:
return "足球运动员 C 罗";
case BASKETBALL:
return "篮球运动员姚明";
default:
throw new IllegalArgumentException(
"未知运动员类型:"
+ playerType
);
}
}因为 switch 已经知道当前变量类型是:
PlayerType所以:
case中直接写:
TENNIS
FOOTBALL
BASKETBALL即可,不需要写:
PlayerType.TENNIS现代 Java 中还可以使用:
switch表达式:
public static String getPlayerName(
PlayerType playerType) {
return switch (playerType) {
case TENNIS ->
"网球运动员费德勒";
case FOOTBALL ->
"足球运动员 C 罗";
case BASKETBALL ->
"篮球运动员姚明";
};
}如果枚举中的所有值都已经覆盖,代码会更加简洁。
7. 枚举可以有成员变量
枚举并不只能写:
TENNIS,
FOOTBALL,
BASKETBALL因为枚举本质上也是一种特殊的类,所以同样可以拥有:
成员变量
构造方法
普通方法
抽象方法
接口例如给每一种运动增加中文名称:
public enum PlayerType {
TENNIS("网球"),
FOOTBALL("足球"),
BASKETBALL("篮球");
private final String name;
PlayerType(String name) {
this.name = name;
}
public String getName() {
return name;
}
}使用:
System.out.println(
PlayerType.BASKETBALL
.getName()
);输出:
篮球这里:
BASKETBALL("篮球")实际上是在创建:
BASKETBALL这个枚举实例时,把:
篮球传给构造方法。
7.1 枚举构造方法
枚举构造方法不能供外部直接调用。
例如:
PlayerType(String name) {
this.name = name;
}它实际上只能在枚举内部创建枚举常量时使用。
枚举构造方法的访问权限只能是:
private语义。
即使不显式写:
private也是如此。
所以通常直接写:
PlayerType(String name) {
}即可。
8. 枚举可以有多个字段
实际项目中的枚举通常不会只有一个名称。
例如订单状态:
public enum OrderStatus {
CREATED(
0,
"待支付"
),
PAID(
1,
"已支付"
),
SHIPPED(
2,
"已发货"
),
COMPLETED(
3,
"已完成"
);
private final int code;
private final String description;
OrderStatus(
int code,
String description) {
this.code = code;
this.description =
description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
}使用:
OrderStatus status =
OrderStatus.PAID;
System.out.println(
status.getCode()
);
System.out.println(
status.getDescription()
);输出:
1
已支付这种写法在实际后端项目中非常常见,例如:
订单状态
支付状态
审核状态
用户状态
业务类型
平台类型都很适合使用枚举。
9. name() 和 ordinal()
因为所有枚举都继承:
java.lang.Enum所以可以使用一些通用方法。
例如:
PlayerType type =
PlayerType.FOOTBALL;name()
type.name();返回枚举常量声明时的名称:
FOOTBALLordinal()
type.ordinal();返回枚举常量声明的顺序编号。
例如:
TENNIS → 0
FOOTBALL → 1
BASKETBALL → 2不过需要特别注意:
不建议把 ordinal() 当作数据库中的业务编号保存。
例如原来:
public enum OrderStatus {
CREATED,
PAID,
COMPLETED
}那么:
CREATED → 0
PAID → 1
COMPLETED → 2如果后来中间增加:
CANCELLED枚举顺序发生变化,原来的:
ordinal也可能发生变化。
所以业务状态应该自己定义:
private final int code;而不是依赖:
ordinal()10. EnumSet
Java 专门为枚举提供了:
EnumSetEnumSet 是:
Set接口的一种高效实现,专门用来保存:
同一种枚举类型的多个枚举常量。
例如:
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}创建一个空的:
EnumSet:
EnumSet<PlayerType> enumSet =
EnumSet.noneOf(
PlayerType.class
);创建包含全部枚举常量的集合:
EnumSet<PlayerType> enumSet =
EnumSet.allOf(
PlayerType.class
);完整示例:
public class EnumSetTest {
public enum PlayerType {
TENNIS,
FOOTBALL,
BASKETBALL
}
public static void main(
String[] args) {
EnumSet<PlayerType> none =
EnumSet.noneOf(
PlayerType.class
);
System.out.println(none);
EnumSet<PlayerType> all =
EnumSet.allOf(
PlayerType.class
);
System.out.println(all);
}
}输出:
[]
[TENNIS, FOOTBALL, BASKETBALL]EnumSet 是抽象类,因此不能:
new EnumSet<>();需要通过它提供的静态工厂方法创建。
常见方法包括:
EnumSet.noneOf(...)
EnumSet.allOf(...)
EnumSet.of(...)
EnumSet.range(...)
EnumSet 内部针对枚举做了专门优化,通常比普通:
HashSet更适合表示:
一组枚举值的集合。
例如权限:
EnumSet<Permission> permissions =
EnumSet.of(
Permission.READ,
Permission.WRITE
);11. EnumMap
和:
EnumSet类似,Java 还提供了:
EnumMap它是专门针对:
枚举作为 Key这种场景设计的:
Map实现。
创建:
EnumMap<PlayerType, String> enumMap =
new EnumMap<>(
PlayerType.class
);和 EnumSet 不同:
EnumSet
↓
抽象类
↓
通过静态工厂方法创建
EnumMap
↓
具体类
↓
可以直接 new使用:
enumMap.put(
PlayerType.BASKETBALL,
"篮球运动员"
);
enumMap.put(
PlayerType.FOOTBALL,
"足球运动员"
);
enumMap.put(
PlayerType.TENNIS,
"网球运动员"
);获取:
System.out.println(
enumMap.get(
PlayerType.BASKETBALL
)
);还可以使用普通 Map 的:
containsKey()
remove()
keySet()
values()
entrySet()等方法。

完整示例:
EnumMap<PlayerType, String> enumMap =
new EnumMap<>(
PlayerType.class
);
enumMap.put(
PlayerType.BASKETBALL,
"篮球运动员"
);
enumMap.put(
PlayerType.FOOTBALL,
"足球运动员"
);
enumMap.put(
PlayerType.TENNIS,
"网球运动员"
);
System.out.println(enumMap);
System.out.println(
enumMap.get(
PlayerType.BASKETBALL
)
);
System.out.println(
enumMap.containsKey(
PlayerType.BASKETBALL
)
);
System.out.println(
enumMap.remove(
PlayerType.BASKETBALL
)
);输出:
{TENNIS=网球运动员, FOOTBALL=足球运动员, BASKETBALL=篮球运动员}
篮球运动员
true
篮球运动员EnumMap 内部针对枚举 Key 做了专门优化,因此如果:
Map 的 Key 明确就是某种枚举通常优先考虑:
EnumMap而不是:
HashMap12. 枚举实现接口
枚举本身虽然不能再继承普通类,但可以:
implements接口。
例如:
public interface Operation {
int calculate(
int a,
int b
);
}枚举实现:
public enum Calculator
implements Operation {
ADD,
SUBTRACT;
@Override
public int calculate(
int a,
int b) {
if (this == ADD) {
return a + b;
}
return a - b;
}
}使用:
System.out.println(
Calculator.ADD
.calculate(10, 5)
);输出:
15因此枚举不仅可以保存:
数据还可以保存:
行为13. 枚举常量可以拥有不同的行为
枚举还可以让每个枚举常量拥有不同的方法实现。
例如:
public enum Operation {
ADD {
@Override
public int calculate(
int a,
int b) {
return a + b;
}
},
SUBTRACT {
@Override
public int calculate(
int a,
int b) {
return a - b;
}
};
public abstract int calculate(
int a,
int b
);
}调用:
System.out.println(
Operation.ADD
.calculate(10, 5)
);
System.out.println(
Operation.SUBTRACT
.calculate(10, 5)
);输出:
15
5这种写法本质上把:
枚举常量
和
对应行为封装到了一起。
在某些状态机、策略类型比较固定的场景中非常实用。
14. 枚举实现单例
枚举还有一个非常经典的用途:
实现单例模式单例模式(Singleton)的目标是:
一个类在一个 JVM 运行环境中只提供一个共享实例,并提供统一的访问方式。
传统的线程安全单例可能会写成:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (
Singleton.class) {
if (instance == null) {
instance =
new Singleton();
}
}
}
return instance;
}
}这里涉及:
volatile
synchronized
双重检查而使用枚举可以非常简单:
public enum EasySingleton {
INSTANCE;
}使用:
EasySingleton singleton =
EasySingleton.INSTANCE;如果需要业务方法:
public enum EasySingleton {
INSTANCE;
public void execute() {
System.out.println(
"执行单例逻辑"
);
}
}调用:
EasySingleton.INSTANCE
.execute();枚举实现单例具有几个重要优势:
代码简单
线程安全初始化
天然只有固定枚举实例
对 Java 原生序列化更加安全Java 的枚举序列化机制会按照枚举常量名称进行处理,不会像普通 Serializable 单例那样在反序列化时轻易创建新的实例。
另外,通过普通反射:
Constructor.newInstance()也不能正常创建新的枚举实例。
因此《Effective Java》中也推荐:
如果单例本身适合用枚举表达,单元素枚举是非常简洁可靠的单例实现方式。
15. 枚举适合什么场景
枚举最适合:
取值范围有限并且相对固定的业务概念。
例如:
订单状态
支付状态
用户状态
审核状态
星期
月份
颜色类型
平台类型
操作类型
角色类型比如订单状态不要写成:
public static final int CREATED = 0;
public static final int PAID = 1;
public static final int COMPLETED = 2;更推荐:
public enum OrderStatus {
CREATED,
PAID,
COMPLETED
}如果需要和数据库中的数字对应:
public enum OrderStatus {
CREATED(
0,
"待支付"
),
PAID(
1,
"已支付"
),
COMPLETED(
2,
"已完成"
);
private final int code;
private final String description;
OrderStatus(
int code,
String description) {
this.code = code;
this.description =
description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
}这种方式比到处散落:
0
1
2
3这样的魔法数字更加清晰。
16. 小结
Java 枚举可以简单理解为:
一组数量有限的固定对象它虽然使用:
enum定义,但本质上仍然是一种特殊的类。
核心知识点:
enum
↓
继承 java.lang.Enum
枚举常量
↓
固定的枚举实例
values()
↓
获取全部枚举常量
valueOf()
↓
根据名称获取枚举常量
==
↓
推荐用于枚举比较
switch
↓
非常适合处理枚举
枚举字段 + 构造方法
↓
可以保存业务编码和描述
EnumSet
↓
专门保存枚举集合
EnumMap
↓
专门使用枚举作为 Key
单元素枚举
↓
可以实现单例在实际 Java 项目中,枚举最重要的价值不是:
少写几个常量而是:
把一组有限的业务取值封装成真正的类型,避免字符串、数字和魔法值散落在代码中。