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;
枚举单例的优点:
- 线程安全:JVM 保证枚举实例的唯一性
- 防止反射攻击:反射无法创建枚举实例
- 序列化安全:枚举天然支持序列化
枚举的序列化
枚举的序列化机制保证了实例的唯一性:
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 -> "周末";
};
}
小结
本章我们学习了:
- 枚举基础:为什么需要枚举、定义和使用枚举
- 枚举方法:name()、ordinal()、values()、valueOf()
- 带字段的枚举:字段、构造方法、方法
- 带抽象方法的枚举:实现策略模式
- 枚举实现接口:提供不同行为
- EnumSet 和 EnumMap:高性能枚举集合
- 枚举单例模式:线程安全、防止反射攻击
- 枚举序列化:保证实例唯一性
- 最佳实践:使用枚举代替常量、EnumSet 代替位运算
练习
- 创建一个表示一周七天的枚举,添加方法判断是否是周末
- 创建一个表示 HTTP 状态码的枚举,包含状态码和描述
- 使用 EnumSet 实现权限管理系统
- 使用枚举实现计算器操作(加减乘除)
- 使用 EnumMap 实现状态机转换表