网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

枚举 ​

字数: 0 字 时长: 0 分钟

Java 枚举 ​

字数: 0 字 时长: 0 分钟

枚举(Enum)是 Java 5 引入的一种特殊类型,用来表示:

数量有限、取值固定的一组常量。

例如运动员类型只有:

text
网球

足球

篮球

就可以定义成枚举:

java
public enum PlayerType {

    TENNIS,

    FOOTBALL,

    BASKETBALL
}

相比直接使用:

java
String type = "football";

枚举具有更强的:

text
类型安全

可读性

可维护性

例如:

java
PlayerType type =
        PlayerType.FOOTBALL;

这里 type 只能使用:

text
PlayerType

定义好的枚举常量,不容易出现:

text
FOOTBALL

football

footBall

fotball

这种字符串拼写问题。

1. 枚举的本质 ​

Java 中的枚举并不是简单的几个常量。

每一个枚举类型,本质上都是一种特殊的类,并且都会直接继承:

java
java.lang.Enum

例如:

java
public enum PlayerType {

    TENNIS,

    FOOTBALL,

    BASKETBALL
}

从概念上可以理解为编译器帮我们生成了类似这样的结构:

java
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) {
        // ...
    }
}

编译器还会负责创建:

text
TENNIS

FOOTBALL

BASKETBALL

这些枚举对象。

因此我们不需要自己写:

java
new PlayerType(...)

实际上外部也不能直接:

java
new PlayerType()

创建枚举对象。

需要注意:

反编译工具看到的具体代码只是编译器生成结果的一种表现形式,不同 JDK 和反编译器生成的代码可能不同。

真正需要记住的是:

text
enum
    ↓
一种特殊的类
    ↓
继承 java.lang.Enum
    ↓
枚举常量都是该枚举类型的固定实例

2. 枚举常量 ​

定义:

java
public enum PlayerType {

    TENNIS,

    FOOTBALL,

    BASKETBALL
}

其中:

text
TENNIS

FOOTBALL

BASKETBALL

都是:

java
PlayerType

类型的对象。

可以直接:

java
PlayerType type =
        PlayerType.BASKETBALL;

不能:

java
// 编译错误
// PlayerType type = "BASKETBALL";

这也是枚举相比字符串常量更加类型安全的原因。

可以简单理解为:

text
PlayerType
│
├── TENNIS
├── FOOTBALL
└── BASKETBALL

3. values() 和 valueOf() ​

Java 编译器会为枚举生成一些常用方法。

3.1 values() ​

java
PlayerType.values()

可以获取枚举中的所有常量。

例如:

java
for (PlayerType type :
        PlayerType.values()) {

    System.out.println(type);
}

输出:

text
TENNIS
FOOTBALL
BASKETBALL

所以:

java
values()

很适合:

text
遍历枚举

生成下拉选项

初始化数据

等场景。

3.2 valueOf() ​

java
PlayerType.valueOf(
        "FOOTBALL"
)

可以根据枚举常量名称获取对应枚举对象。

例如:

java
PlayerType type =
        PlayerType.valueOf(
                "FOOTBALL"
        );

System.out.println(type);

输出:

text
FOOTBALL

需要注意:

java
valueOf()

要求名称必须完全匹配。

例如:

java
PlayerType.valueOf(
        "football"
);

会抛出:

text
IllegalArgumentException

因为:

text
FOOTBALL

和:

text
football

不是同一个名称。

4. 枚举可以定义在类内部 ​

枚举本身也是一种类型,因此可以定义在其他类内部。

例如:

java
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;
    }
}

使用:

java
Player.PlayerType type =
        Player.PlayerType.BASKETBALL;

这样可以把:

text
PlayerType

的作用范围和:

text
Player

联系得更加紧密。

适合:

这个枚举只服务于某一个类或者某一组相关业务时。

5. 枚举比较 ​

枚举通常推荐直接使用:

java
==

比较。

例如:

java
if (player.getType()
        == Player.PlayerType.BASKETBALL) {

    System.out.println(
            "篮球运动员"
    );
}

这是安全的。

因为一个枚举常量在当前 JVM 中对应的就是固定的枚举实例。

例如:

text
PlayerType.BASKETBALL

不会每次访问都创建一个新的对象。

因此:

java
type1 == type2

可以直接判断它们是不是同一个枚举常量。

5.1 为什么推荐使用 == ​

枚举的:

java
equals()

本身也采用对象身份进行比较,因此:

java
type1.equals(type2)

通常也能得到正确结果。

但:

java
==

有两个明显优点。

第一,不怕左侧为:

java
null

例如:

java
PlayerType type = null;

System.out.println(
        type ==
                PlayerType.BASKETBALL
);

结果:

text
false

不会抛出异常。

而:

java
type.equals(
        PlayerType.BASKETBALL
);

会抛出:

text
NullPointerException

第二,== 会进行类型检查。

例如两个完全不同的枚举类型不能随便通过 == 比较。

image-20250303224539257

所以枚举比较通常直接写:

java
type ==
        PlayerType.BASKETBALL

即可。

6. 枚举与 switch ​

枚举非常适合搭配:

java
switch

使用。

例如:

java
public static String getPlayerName(
        PlayerType playerType) {

    switch (playerType) {

        case TENNIS:

            return "网球运动员费德勒";

        case FOOTBALL:

            return "足球运动员 C 罗";

        case BASKETBALL:

            return "篮球运动员姚明";

        default:

            throw new IllegalArgumentException(
                    "未知运动员类型:"
                            + playerType
            );
    }
}

因为 switch 已经知道当前变量类型是:

java
PlayerType

所以:

java
case

中直接写:

java
TENNIS

FOOTBALL

BASKETBALL

即可,不需要写:

java
PlayerType.TENNIS

现代 Java 中还可以使用:

java
switch

表达式:

java
public static String getPlayerName(
        PlayerType playerType) {

    return switch (playerType) {

        case TENNIS ->
                "网球运动员费德勒";

        case FOOTBALL ->
                "足球运动员 C 罗";

        case BASKETBALL ->
                "篮球运动员姚明";
    };
}

如果枚举中的所有值都已经覆盖,代码会更加简洁。

7. 枚举可以有成员变量 ​

枚举并不只能写:

java
TENNIS,
FOOTBALL,
BASKETBALL

因为枚举本质上也是一种特殊的类,所以同样可以拥有:

text
成员变量

构造方法

普通方法

抽象方法

接口

例如给每一种运动增加中文名称:

java
public enum PlayerType {

    TENNIS("网球"),

    FOOTBALL("足球"),

    BASKETBALL("篮球");

    private final String name;

    PlayerType(String name) {

        this.name = name;
    }

    public String getName() {

        return name;
    }
}

使用:

java
System.out.println(
        PlayerType.BASKETBALL
                .getName()
);

输出:

text
篮球

这里:

java
BASKETBALL("篮球")

实际上是在创建:

text
BASKETBALL

这个枚举实例时,把:

text
篮球

传给构造方法。

7.1 枚举构造方法 ​

枚举构造方法不能供外部直接调用。

例如:

java
PlayerType(String name) {

    this.name = name;
}

它实际上只能在枚举内部创建枚举常量时使用。

枚举构造方法的访问权限只能是:

text
private

语义。

即使不显式写:

java
private

也是如此。

所以通常直接写:

java
PlayerType(String name) {
}

即可。

8. 枚举可以有多个字段 ​

实际项目中的枚举通常不会只有一个名称。

例如订单状态:

java
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;
    }
}

使用:

java
OrderStatus status =
        OrderStatus.PAID;

System.out.println(
        status.getCode()
);

System.out.println(
        status.getDescription()
);

输出:

text
1
已支付

这种写法在实际后端项目中非常常见,例如:

text
订单状态

支付状态

审核状态

用户状态

业务类型

平台类型

都很适合使用枚举。

9. name() 和 ordinal() ​

因为所有枚举都继承:

java
java.lang.Enum

所以可以使用一些通用方法。

例如:

java
PlayerType type =
        PlayerType.FOOTBALL;

name() ​

java
type.name();

返回枚举常量声明时的名称:

text
FOOTBALL

ordinal() ​

java
type.ordinal();

返回枚举常量声明的顺序编号。

例如:

text
TENNIS       → 0

FOOTBALL     → 1

BASKETBALL   → 2

不过需要特别注意:

不建议把 ordinal() 当作数据库中的业务编号保存。

例如原来:

java
public enum OrderStatus {

    CREATED,

    PAID,

    COMPLETED
}

那么:

text
CREATED   → 0
PAID      → 1
COMPLETED → 2

如果后来中间增加:

java
CANCELLED

枚举顺序发生变化,原来的:

text
ordinal

也可能发生变化。

所以业务状态应该自己定义:

java
private final int code;

而不是依赖:

java
ordinal()

10. EnumSet ​

Java 专门为枚举提供了:

java
EnumSet

EnumSet 是:

java
Set

接口的一种高效实现,专门用来保存:

text
同一种枚举类型

的多个枚举常量。

例如:

java
public enum PlayerType {

    TENNIS,

    FOOTBALL,

    BASKETBALL
}

创建一个空的:

java
EnumSet

:

java
EnumSet<PlayerType> enumSet =
        EnumSet.noneOf(
                PlayerType.class
        );

创建包含全部枚举常量的集合:

java
EnumSet<PlayerType> enumSet =
        EnumSet.allOf(
                PlayerType.class
        );

完整示例:

java
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);
    }
}

输出:

text
[]

[TENNIS, FOOTBALL, BASKETBALL]

EnumSet 是抽象类,因此不能:

java
new EnumSet<>();

需要通过它提供的静态工厂方法创建。

常见方法包括:

java
EnumSet.noneOf(...)

EnumSet.allOf(...)

EnumSet.of(...)

EnumSet.range(...)

image-20250303224715304

EnumSet 内部针对枚举做了专门优化,通常比普通:

java
HashSet

更适合表示:

一组枚举值的集合。

例如权限:

java
EnumSet<Permission> permissions =
        EnumSet.of(
                Permission.READ,
                Permission.WRITE
        );

11. EnumMap ​

和:

java
EnumSet

类似,Java 还提供了:

java
EnumMap

它是专门针对:

text
枚举作为 Key

这种场景设计的:

java
Map

实现。

创建:

java
EnumMap<PlayerType, String> enumMap =
        new EnumMap<>(
                PlayerType.class
        );

和 EnumSet 不同:

text
EnumSet
    ↓
抽象类
    ↓
通过静态工厂方法创建


EnumMap
    ↓
具体类
    ↓
可以直接 new

使用:

java
enumMap.put(
        PlayerType.BASKETBALL,
        "篮球运动员"
);

enumMap.put(
        PlayerType.FOOTBALL,
        "足球运动员"
);

enumMap.put(
        PlayerType.TENNIS,
        "网球运动员"
);

获取:

java
System.out.println(
        enumMap.get(
                PlayerType.BASKETBALL
        )
);

还可以使用普通 Map 的:

text
containsKey()

remove()

keySet()

values()

entrySet()

等方法。

image-20250303224841101

完整示例:

java
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
        )
);

输出:

text
{TENNIS=网球运动员, FOOTBALL=足球运动员, BASKETBALL=篮球运动员}

篮球运动员

true

篮球运动员

EnumMap 内部针对枚举 Key 做了专门优化,因此如果:

text
Map 的 Key 明确就是某种枚举

通常优先考虑:

java
EnumMap

而不是:

java
HashMap

12. 枚举实现接口 ​

枚举本身虽然不能再继承普通类,但可以:

java
implements

接口。

例如:

java
public interface Operation {

    int calculate(
            int a,
            int b
    );
}

枚举实现:

java
public enum Calculator
        implements Operation {

    ADD,

    SUBTRACT;

    @Override
    public int calculate(
            int a,
            int b) {

        if (this == ADD) {

            return a + b;
        }

        return a - b;
    }
}

使用:

java
System.out.println(
        Calculator.ADD
                .calculate(10, 5)
);

输出:

text
15

因此枚举不仅可以保存:

text
数据

还可以保存:

text
行为

13. 枚举常量可以拥有不同的行为 ​

枚举还可以让每个枚举常量拥有不同的方法实现。

例如:

java
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
    );
}

调用:

java
System.out.println(
        Operation.ADD
                .calculate(10, 5)
);

System.out.println(
        Operation.SUBTRACT
                .calculate(10, 5)
);

输出:

text
15
5

这种写法本质上把:

text
枚举常量

和

对应行为

封装到了一起。

在某些状态机、策略类型比较固定的场景中非常实用。

14. 枚举实现单例 ​

枚举还有一个非常经典的用途:

text
实现单例模式

单例模式(Singleton)的目标是:

一个类在一个 JVM 运行环境中只提供一个共享实例,并提供统一的访问方式。

传统的线程安全单例可能会写成:

java
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;
    }
}

这里涉及:

text
volatile

synchronized

双重检查

而使用枚举可以非常简单:

java
public enum EasySingleton {

    INSTANCE;
}

使用:

java
EasySingleton singleton =
        EasySingleton.INSTANCE;

如果需要业务方法:

java
public enum EasySingleton {

    INSTANCE;

    public void execute() {

        System.out.println(
                "执行单例逻辑"
        );
    }
}

调用:

java
EasySingleton.INSTANCE
        .execute();

枚举实现单例具有几个重要优势:

text
代码简单

线程安全初始化

天然只有固定枚举实例

对 Java 原生序列化更加安全

Java 的枚举序列化机制会按照枚举常量名称进行处理,不会像普通 Serializable 单例那样在反序列化时轻易创建新的实例。

另外,通过普通反射:

java
Constructor.newInstance()

也不能正常创建新的枚举实例。

因此《Effective Java》中也推荐:

如果单例本身适合用枚举表达,单元素枚举是非常简洁可靠的单例实现方式。

15. 枚举适合什么场景 ​

枚举最适合:

取值范围有限并且相对固定的业务概念。

例如:

text
订单状态

支付状态

用户状态

审核状态

星期

月份

颜色类型

平台类型

操作类型

角色类型

比如订单状态不要写成:

java
public static final int CREATED = 0;

public static final int PAID = 1;

public static final int COMPLETED = 2;

更推荐:

java
public enum OrderStatus {

    CREATED,

    PAID,

    COMPLETED
}

如果需要和数据库中的数字对应:

java
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;
    }
}

这种方式比到处散落:

text
0

1

2

3

这样的魔法数字更加清晰。

16. 小结 ​

Java 枚举可以简单理解为:

text
一组数量有限的固定对象

它虽然使用:

java
enum

定义,但本质上仍然是一种特殊的类。

核心知识点:

text
enum
    ↓
继承 java.lang.Enum


枚举常量
    ↓
固定的枚举实例


values()
    ↓
获取全部枚举常量


valueOf()
    ↓
根据名称获取枚举常量


==
    ↓
推荐用于枚举比较


switch
    ↓
非常适合处理枚举


枚举字段 + 构造方法
    ↓
可以保存业务编码和描述


EnumSet
    ↓
专门保存枚举集合


EnumMap
    ↓
专门使用枚举作为 Key


单元素枚举
    ↓
可以实现单例

在实际 Java 项目中,枚举最重要的价值不是:

text
少写几个常量

而是:

把一组有限的业务取值封装成真正的类型,避免字符串、数字和魔法值散落在代码中。

Noctiflorous Garden · MIT License