跳到主要内容

Java 枚举

枚举(Enum)是 Java 5 引入的一种特殊类型,用于定义一组固定的常量。枚举类型是 java.lang.Enum 的子类。

为什么需要枚举?

没有枚举时的问题

// 使用 int 常量表示状态
public class Status {
public static final int PENDING = 0;
public static final int APPROVED = 1;
public static final int REJECTED = 2;
}

// 问题:
// 1. 类型不安全:可以传入任意 int 值
public void process(int status) {
// status 可能是 100、-5 等非法值
}

// 2. 没有命名空间:需要前缀区分
public static final int COLOR_RED = 1;
public static final int STATUS_APPROVED = 1; // 可能冲突

// 3. 打印不直观
System.out.println(status); // 输出: 1(不知道是什么意思)

使用枚举解决

public enum Status {
PENDING, APPROVED, REJECTED
}

// 类型安全
public void process(Status status) {
// 只能传入 Status 枚举值
}

// 使用
process(Status.APPROVED);
System.out.println(Status.APPROVED); // 输出: APPROVED

枚举基础

定义枚举

// 最简单的枚举
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

// 使用
Day today = Day.MONDAY;
System.out.println(today); // MONDAY

枚举的本质

枚举实际上是继承了 java.lang.Enum 的类:

// 编译器生成的代码(简化)
public final class Day extends Enum<Day> {
public static final Day MONDAY = new Day("MONDAY", 0);
public static final Day TUESDAY = new Day("TUESDAY", 1);
// ...

private Day(String name, int ordinal) {
super(name, ordinal);
}
}

枚举的方法

Day day = Day.MONDAY;

// 获取名称
System.out.println(day.name()); // "MONDAY"

// 获取序号(从0开始)
System.out.println(day.ordinal()); // 0

// 获取所有枚举值
Day[] days = Day.values();
for (Day d : days) {
System.out.println(d);
}

// 根据名称获取枚举
Day d = Day.valueOf("MONDAY");

// 比较枚举
System.out.println(day == Day.MONDAY); // true(可以直接用 ==)
System.out.println(day.equals(Day.MONDAY)); // true

// 比较序号
System.out.println(Day.MONDAY.compareTo(Day.FRIDAY)); // -4(MONDAY 在 FRIDAY 前4个位置)

带字段的枚举

枚举可以包含字段、方法和构造方法。

基本用法

public enum Planet {
// 枚举值必须在最前面
MERCURY(3.303e+23, 2.4397e6),
VENUS(4.869e+24, 6.0518e6),
EARTH(5.976e+24, 6.37814e6),
MARS(6.421e+23, 3.3972e6);

// 字段
private final double mass; // 质量(千克)
private final double radius; // 半径(米)

// 构造方法(自动是 private)
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}

// 方法
public double getMass() { return mass; }
public double getRadius() { return radius; }

// 计算表面重力
public double surfaceGravity() {
return 6.67300E-11 * mass / (radius * radius);
}

// 计算表面重量
public double surfaceWeight(double otherMass) {
return otherMass * surfaceGravity();
}
}

// 使用
Planet earth = Planet.EARTH;
System.out.println("地球质量: " + earth.getMass());
System.out.println("表面重力: " + earth.surfaceGravity());

// 计算在地球上的体重(假设质量 80kg)
double weight = earth.surfaceWeight(80);
System.out.println("体重: " + weight + " N");

枚举与描述

public enum Season {
SPRING("春天", "万物复苏"),
SUMMER("夏天", "烈日炎炎"),
AUTUMN("秋天", "硕果累累"),
WINTER("冬天", "白雪皑皑");

private final String name;
private final String description;

Season(String name, String description) {
this.name = name;
this.description = description;
}

public String getName() { return name; }
public String getDescription() { return description; }

@Override
public String toString() {
return name + " - " + description;
}
}

// 使用
System.out.println(Season.SPRING.getName()); // 春天
System.out.println(Season.SPRING.getDescription()); // 万物复苏
System.out.println(Season.SPRING); // 春天 - 万物复苏

带抽象方法的枚举

枚举可以定义抽象方法,每个枚举值必须实现该方法。

基本用法

public enum Operation {
ADD {
@Override
public double apply(double x, double y) {
return x + y;
}
},
SUBTRACT {
@Override
public double apply(double x, double y) {
return x - y;
}
},
MULTIPLY {
@Override
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE {
@Override
public double apply(double x, double y) {
return x / y;
}
};

// 抽象方法
public abstract double apply(double x, double y);
}

// 使用
double result1 = Operation.ADD.apply(5, 3); // 8.0
double result2 = Operation.MULTIPLY.apply(5, 3); // 15.0

// 配合 switch 使用
Operation op = Operation.ADD;
switch (op) {
case ADD:
System.out.println("加法");
break;
case SUBTRACT:
System.out.println("减法");
break;
// ...
}

策略模式实现

public enum PayType {
ALIPAY {
@Override
public void pay(double amount) {
System.out.println("支付宝支付: " + amount + " 元");
}

@Override
public double getDiscount() {
return 0.95; // 95折
}
},
WECHAT {
@Override
public void pay(double amount) {
System.out.println("微信支付: " + amount + " 元");
}

@Override
public double getDiscount() {
return 0.98; // 98折
}
},
CREDIT_CARD {
@Override
public void pay(double amount) {
System.out.println("信用卡支付: " + amount + " 元");
}

@Override
public double getDiscount() {
return 1.0; // 无折扣
}
};

public abstract void pay(double amount);
public abstract double getDiscount();

// 具体业务方法
public void processPayment(double amount) {
double finalAmount = amount * getDiscount();
pay(finalAmount);
}
}

// 使用
PayType.ALIPAY.processPayment(100); // 支付宝支付: 95.0 元
PayType.WECHAT.processPayment(100); // 微信支付: 98.0 元

枚举实现接口

枚举可以实现接口,提供不同的行为。

public interface Printer {
void print(String message);
}

public enum LogLevel implements Printer {
DEBUG {
@Override
public void print(String message) {
System.out.println("[DEBUG] " + message);
}
},
INFO {
@Override
public void print(String message) {
System.out.println("[INFO] " + message);
}
},
WARNING {
@Override
public void print(String message) {
System.out.println("[WARNING] " + message);
}
},
ERROR {
@Override
public void print(String message) {
System.err.println("[ERROR] " + message);
}
};
}

// 使用
LogLevel.INFO.print("程序启动"); // [INFO] 程序启动
LogLevel.ERROR.print("发生错误"); // [ERROR] 发生错误

EnumSet 和 EnumMap

Java 提供了专门针对枚举的高性能集合类。

EnumSet

EnumSet 是专门为枚举设计的 Set 实现,内部使用位向量存储,非常高效。

import java.util.EnumSet;

enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

// 创建 EnumSet
EnumSet<Day> weekdays = EnumSet.range(Day.MONDAY, Day.FRIDAY);
EnumSet<Day> weekend = EnumSet.of(Day.SATURDAY, Day.SUNDAY);
EnumSet<Day> allDays = EnumSet.allOf(Day.class);
EnumSet<Day> empty = EnumSet.noneOf(Day.class);

// 补集
EnumSet<Day> notWeekend = EnumSet.complementOf(weekend);

// 操作
weekdays.add(Day.SATURDAY); // 添加元素
weekdays.remove(Day.SUNDAY); // 移除元素
weekdays.contains(Day.MONDAY); // 检查元素

// 遍历
for (Day day : weekdays) {
System.out.println(day);
}

// 实际应用:权限控制
enum Permission {
READ, WRITE, EXECUTE, DELETE
}

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

// 检查权限
if (userPermissions.contains(Permission.READ)) {
System.out.println("有读取权限");
}

// 添加权限
userPermissions.add(Permission.EXECUTE);

// 批量检查
if (userPermissions.containsAll(EnumSet.of(Permission.READ, Permission.WRITE))) {
System.out.println("有读写权限");
}

EnumMap

EnumMap 是专门为枚举键设计的 Map 实现,内部使用数组存储,效率比 HashMap 高。

import java.util.EnumMap;

enum Status {
PENDING, APPROVED, REJECTED
}

// 创建 EnumMap
EnumMap<Status, String> statusMessages = new EnumMap<>(Status.class);
statusMessages.put(Status.PENDING, "等待审批");
statusMessages.put(Status.APPROVED, "已通过");
statusMessages.put(Status.REJECTED, "已拒绝");

// 获取值
System.out.println(statusMessages.get(Status.APPROVED)); // 已通过

// 遍历
for (Status status : Status.values()) {
System.out.println(status + ": " + statusMessages.get(status));
}

// 实际应用:配置映射
enum Environment {
DEV, TEST, PRODUCTION
}

EnumMap<Environment, String> dbUrls = new EnumMap<>(Environment.class);
dbUrls.put(Environment.DEV, "jdbc:mysql://localhost:3306/dev");
dbUrls.put(Environment.TEST, "jdbc:mysql://test-server:3306/test");
dbUrls.put(Environment.PRODUCTION, "jdbc:mysql://prod-server:3306/prod");

// 根据环境获取数据库URL
Environment env = Environment.DEV;
String url = dbUrls.get(env);

EnumMap 嵌套

// 状态转换表
enum State {
NEW, RUNNING, BLOCKED, TERMINATED
}

EnumMap<State, EnumMap<State, Boolean>> transitions = new EnumMap<>(State.class);

// 初始化转换规则
for (State from : State.values()) {
transitions.put(from, new EnumMap<>(State.class));
}

transitions.get(State.NEW).put(State.RUNNING, true);
transitions.get(State.RUNNING).put(State.BLOCKED, true);
transitions.get(State.RUNNING).put(State.TERMINATED, true);
transitions.get(State.BLOCKED).put(State.RUNNING, true);

// 检查转换是否合法
boolean canTransition = transitions.get(State.NEW).getOrDefault(State.RUNNING, false);

枚举的单例模式

枚举是实现单例模式的最佳方式,天然防止反射攻击和序列化问题。

// 传统单例(有缺陷)
public class Singleton {
private static Singleton instance;

private Singleton() {}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

// 枚举单例(推荐)
public enum Singleton {
INSTANCE;

// 实例方法
public void doSomething() {
System.out.println("单例方法执行");
}
}

// 使用
Singleton.INSTANCE.doSomething();

// 获取单例实例
Singleton singleton = Singleton.INSTANCE;

枚举单例的优点:

  1. 线程安全:JVM 保证枚举实例的唯一性
  2. 防止反射攻击:反射无法创建枚举实例
  3. 序列化安全:枚举天然支持序列化

枚举的序列化

枚举的序列化机制保证了实例的唯一性:

import java.io.*;

public class EnumSerialization {
public static void main(String[] args) throws Exception {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(Status.APPROVED);

// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Status status = (Status) ois.readObject();

// 仍然是同一个实例
System.out.println(status == Status.APPROVED); // true
}
}

枚举的最佳实践

1. 使用 Enum 代替 int 常量

// 不好
public static final int STATUS_PENDING = 0;
public static final int STATUS_APPROVED = 1;

// 好
public enum Status { PENDING, APPROVED }

2. 枚举中存储相关数据

public enum HttpStatus {
OK(200, "成功"),
BAD_REQUEST(400, "请求错误"),
NOT_FOUND(404, "未找到"),
INTERNAL_ERROR(500, "服务器内部错误");

private final int code;
private final String message;

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

public int getCode() { return code; }
public String getMessage() { return message; }

// 根据状态码查找枚举
public static HttpStatus fromCode(int code) {
for (HttpStatus status : values()) {
if (status.code == code) {
return status;
}
}
return null;
}
}

3. 使用 EnumSet 代替位运算

// 不好:使用位运算
public static final int PERMISSION_READ = 1;
public static final int PERMISSION_WRITE = 2;
public static final int PERMISSION_EXECUTE = 4;
int permissions = PERMISSION_READ | PERMISSION_WRITE;

// 好:使用 EnumSet
EnumSet<Permission> permissions = EnumSet.of(Permission.READ, Permission.WRITE);

4. 使用 switch 表达式处理枚举

// Java 14+ switch 表达式
String getDayType(Day day) {
return switch (day) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "工作日";
case SATURDAY, SUNDAY -> "周末";
};
}

小结

本章我们学习了:

  1. 枚举基础:为什么需要枚举、定义和使用枚举
  2. 枚举方法:name()、ordinal()、values()、valueOf()
  3. 带字段的枚举:字段、构造方法、方法
  4. 带抽象方法的枚举:实现策略模式
  5. 枚举实现接口:提供不同行为
  6. EnumSet 和 EnumMap:高性能枚举集合
  7. 枚举单例模式:线程安全、防止反射攻击
  8. 枚举序列化:保证实例唯一性
  9. 最佳实践:使用枚举代替常量、EnumSet 代替位运算

练习

  1. 创建一个表示一周七天的枚举,添加方法判断是否是周末
  2. 创建一个表示 HTTP 状态码的枚举,包含状态码和描述
  3. 使用 EnumSet 实现权限管理系统
  4. 使用枚举实现计算器操作(加减乘除)
  5. 使用 EnumMap 实现状态机转换表