枚举
1. 什么是枚举
枚举(enum)是 Java 5 引入的一种特殊类型,用于定义一组固定的常量。与传统的 public static final 常量相比,枚举提供了类型安全性、更强的可读性以及丰富的内置方法和扩展能力。
在没有枚举之前,开发者通常这样定义常量:
// 传统常量定义方式 —— 类型不安全
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;这种方式的问题是:任何 int 值都可以被传进来,编译器无法检查类型安全。而枚举从根本上解决了这个问题。
2. 枚举的定义
使用 enum 关键字定义一个枚举,枚举常量列表写在花括号中,常量之间用逗号分隔。
// 最简单的枚举定义
public enum Day {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
}使用枚举:
public class EnumDemo {
public static void main(String[] args) {
Day today = Day.MONDAY;
System.out.println(today); // 输出: MONDAY
}
}枚举本质上继承自 java.lang.Enum 类,每个枚举常量都是该枚举类的一个实例。枚举构造方法默认是 private 的,不允许外部创建实例。
3. 枚举的属性和方法
枚举可以拥有自己的字段、构造方法和普通方法。构造方法必须是 private(不写也默认是 private)。
3.1 带字段的枚举
public enum Planet {
MERCURY(3.303e23, 2.4397e6),
VENUS(4.869e24, 6.0518e6),
EARTH(5.976e24, 6.37814e6),
MARS(6.421e23, 3.3972e6),
JUPITER(1.9e27, 7.1492e7),
SATURN(5.688e26, 6.0268e7),
URANUS(8.686e25, 2.5559e7),
NEPTUNE(1.024e26, 2.4746e7);
// 字段
private final double mass; // 质量(kg)
private final double radius; // 半径(m)
// 构造方法 —— 隐式 private
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
// Getter 方法
public double getMass() {
return mass;
}
public double getRadius() {
return radius;
}
// 计算表面重力加速度
public double surfaceGravity() {
final double G = 6.67300E-11;
return G * mass / (radius * radius);
}
}使用示例:
public class PlanetDemo {
public static void main(String[] args) {
for (Planet planet : Planet.values()) {
System.out.printf("%s: 质量=%.2e kg, 半径=%.2e m, 表面重力=%.2f m/s²%n",
planet, planet.getMass(), planet.getRadius(),
planet.surfaceGravity());
}
}
}输出:
MERCURY: 质量=3.30e+23 kg, 半径=2.44e+06 m, 表面重力=3.70 m/s²
VENUS: 质量=4.87e+24 kg, 半径=6.05e+06 m, 表面重力=8.87 m/s²
EARTH: 质量=5.98e+24 kg, 半径=6.38e+06 m, 表面重力=9.80 m/s²
...3.2 构造方法必须是 private
枚举的构造方法不允许使用 public 或 protected 修饰符,因为枚举实例是在编译时确定的,外部不能创建新的实例。即使不写访问修饰符,也默认是 private。
public enum Color {
RED("#FF0000"),
GREEN("#00FF00"),
BLUE("#0000FF");
private String hex;
// 编译正确 —— 默认 private
Color(String hex) {
this.hex = hex;
}
// 编译错误 —— 枚举构造方法不允许 public
// public Color(String hex) { this.hex = hex; }
}4. 常用内置方法
java.lang.Enum 为所有枚举提供了以下内置方法:
| 方法 | 描述 |
|---|---|
values() | 返回该枚举的所有常量数组,按声明顺序排列 |
valueOf(String) | 根据名称返回对应的枚举常量,不存在则抛 IllegalArgumentException |
ordinal() | 返回枚举常量的声明顺序(从 0 开始) |
name() | 返回枚举常量的名称(与 toString() 相同) |
compareTo(E o) | 比较两个枚举常量的 ordinal 值 |
public class EnumMethodsDemo {
public enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}
public static void main(String[] args) {
// values() —— 遍历所有常量
System.out.println("=== values() 遍历 ===");
for (Season s : Season.values()) {
System.out.println(s);
}
// valueOf() —— 字符串转枚举
System.out.println("\n=== valueOf() ===");
Season s1 = Season.valueOf("SPRING");
System.out.println("valueOf(\"SPRING\"): " + s1);
// name() —— 获取常量名称
System.out.println("\n=== name() ===");
System.out.println("SUMMER.name(): " + Season.SUMMER.name());
// ordinal() —— 获取声明顺序(从 0 开始)
System.out.println("\n=== ordinal() ===");
System.out.println("SPRING.ordinal(): " + Season.SPRING.ordinal()); // 0
System.out.println("WINTER.ordinal(): " + Season.WINTER.ordinal()); // 3
// compareTo() —— 比较 ordinal 值
System.out.println("\n=== compareTo() ===");
System.out.println("SPRING.compareTo(SUMMER): " +
Season.SPRING.compareTo(Season.SUMMER)); // -1
System.out.println("SUMMER.compareTo(SPRING): " +
Season.SUMMER.compareTo(Season.SPRING)); // 1
System.out.println("SPRING.compareTo(SPRING): " +
Season.SPRING.compareTo(Season.SPRING)); // 0
}
}输出:
=== values() 遍历 ===
SPRING
SUMMER
AUTUMN
WINTER
=== valueOf() ===
valueOf("SPRING"): SPRING
=== name() ===
SUMMER.name(): SUMMER
=== ordinal() ===
SPRING.ordinal(): 0
WINTER.ordinal(): 3
=== compareTo() ===
SPRING.compareTo(SUMMER): -1
SUMMER.compareTo(SPRING): 1
SPRING.compareTo(SPRING): 05. 枚举与 switch
枚举天然适合与 switch-case 搭配使用,代码简洁且类型安全。
public class EnumSwitchDemo {
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
public static String getDayType(Day day) {
switch (day) {
case SATURDAY:
case SUNDAY:
return "周末";
case MONDAY:
return "周一 —— 新的一周开始了";
case FRIDAY:
return "周五 —— 马上周末了";
default:
return "工作日";
}
}
public static void main(String[] args) {
for (Day day : Day.values()) {
System.out.println(day + ": " + getDayType(day));
}
}
}输出:
MONDAY: 周一 —— 新的一周开始了
TUESDAY: 工作日
WEDNESDAY: 工作日
THURSDAY: 工作日
FRIDAY: 周五 —— 马上周末了
SATURDAY: 周末
SUNDAY: 周末注意:
switch的 case 中使用枚举常量时,不需要带上枚举类型前缀(即写MONDAY而不是Day.MONDAY)。
6. 枚举实现单例
枚举是实现单例模式的最佳方式 —— 没有之一。Java 语言规范保证了枚举实例的创建是线程安全的,而且枚举天然防止反射攻击和序列化破坏。
6.1 基础枚举单例
public enum Singleton {
INSTANCE;
private String data;
public void setData(String data) {
this.data = data;
}
public String getData() {
return data;
}
public void doSomething() {
System.out.println("Singleton 实例正在工作,data=" + data);
}
}使用:
public class SingletonDemo {
public static void main(String[] args) {
Singleton s1 = Singleton.INSTANCE;
Singleton s2 = Singleton.INSTANCE;
System.out.println("s1 == s2: " + (s1 == s2)); // true
System.out.println("hashCode 相同: " +
(s1.hashCode() == s2.hashCode())); // true
s1.setData("Hello Enum Singleton");
s2.doSomething(); // 输出: Singleton 实例正在工作,data=Hello Enum Singleton
}
}6.2 枚举单例为何安全
import java.lang.reflect.Constructor;
public class SingletonSafetyDemo {
public enum SafeSingleton {
INSTANCE;
public void show() {
System.out.println("安全单例实例");
}
}
public static void main(String[] args) throws Exception {
// 1. 测试反序列化安全
// 枚举的 readObject 机制会直接返回已有的 INSTANCE,不会创建新对象
// 2. 测试反射攻击 —— 会抛出异常
try {
Constructor<SafeSingleton> constructor =
SafeSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
SafeSingleton newInstance = constructor.newInstance();
System.out.println("反射创建成功: " + newInstance);
} catch (IllegalArgumentException e) {
// JDK 内部会抛出: Cannot reflectively create enum objects
System.out.println("反射攻击被阻止: " + e.getMessage());
}
// 3. 验证仍然是同一个实例
SafeSingleton instance = SafeSingleton.INSTANCE;
System.out.println("正常获取实例: " + instance);
}
}枚举单例的优势:
- 线程安全:枚举实例的创建由 JVM 保证,不存在并发问题
- 序列化安全:Java 对枚举的序列化做了特殊处理,反序列化时不会创建新对象
- 反射安全:
Constructor.newInstance()内部会检查是否是枚举类型,如果是则直接抛异常 - 代码简洁:相比双重检查锁定(DCL)或静态内部类方式,枚举单例的代码量最少
7. 枚举实现接口
枚举可以像普通类一样实现一个或多个接口。
// 定义一个接口
public interface Describable {
String getDescription();
}
// 枚举实现接口
public enum Operation implements Describable {
ADD("加法"),
SUBTRACT("减法"),
MULTIPLY("乘法"),
DIVIDE("除法");
private final String description;
Operation(String description) {
this.description = description;
}
@Override
public String getDescription() {
return description;
}
// 还可以有自己的业务方法
public double apply(double a, double b) {
switch (this) {
case ADD: return a + b;
case SUBTRACT: return a - b;
case MULTIPLY: return a * b;
case DIVIDE:
if (b == 0) throw new ArithmeticException("除数不能为零");
return a / b;
default: throw new AssertionError("未知操作: " + this);
}
}
}使用:
public class EnumInterfaceDemo {
public static void main(String[] args) {
// 以接口类型引用枚举 —— 多态
Describable describable = Operation.ADD;
System.out.println(describable.getDescription()); // 加法
// 调用枚举自身方法
Operation op = Operation.MULTIPLY;
System.out.println(op + ": " + op.getDescription());
System.out.println("5 * 3 = " + op.apply(5, 3)); // 15.0
// 遍历所有操作
System.out.println("\n所有操作:");
for (Operation o : Operation.values()) {
System.out.printf("%s (%s): 10 %s 3 = %.2f%n",
o, o.getDescription(), o.name(), o.apply(10, 3));
}
}
}输出:
加法
MULTIPLY: 乘法
5 * 3 = 15.0
所有操作:
ADD (加法): 10 ADD 3 = 13.00
SUBTRACT (减法): 10 SUBTRACT 3 = 7.00
MULTIPLY (乘法): 10 MULTIPLY 3 = 30.00
DIVIDE (除法): 10 DIVIDE 3 = 3.338. 枚举自己的抽象方法
枚举可以定义抽象方法,每个枚举常量必须独立实现该方法。这是实现"每个常量有不同行为"的经典模式。
public enum Calculator {
// 每个枚举常量用匿名内部类的方式实现抽象方法
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) {
if (y == 0) {
throw new ArithmeticException("除数不能为零");
}
return x / y;
}
};
// 字段
private final String symbol;
// 构造方法
Calculator(String symbol) {
this.symbol = symbol;
}
// 抽象方法 —— 每个常量必须实现
public abstract double apply(double x, double y);
// 普通方法
public String getSymbol() {
return symbol;
}
}使用:
public class AbstractMethodDemo {
public static void main(String[] args) {
double a = 10;
double b = 3;
for (Calculator calc : Calculator.values()) {
double result = calc.apply(a, b);
System.out.printf("%.0f %s %.0f = %.2f%n",
a, calc.getSymbol(), b, result);
}
}
}输出:
10 + 3 = 13.00
10 - 3 = 7.00
10 * 3 = 30.00
10 / 3 = 3.33这种模式非常强大 —— 它把不同常量的行为差异封装在了枚举自身内部,避免了在外部使用大量的 switch-case 或 if-else 判断。这其实就是策略模式的一种简洁实现。
9. EnumSet 和 EnumMap
java.util.EnumSet 和 java.util.EnumMap 是专为枚举设计的高性能集合类。它们内部使用位向量或数组实现,比普通的 HashSet / HashMap 效率更高。
9.1 EnumSet
EnumSet 是一个抽象类,提供了丰富的静态工厂方法。它要求元素必须是同一个枚举类型。
import java.util.EnumSet;
public class EnumSetDemo {
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
public static void main(String[] args) {
// 1. 创建包含所有常量的 EnumSet
EnumSet<Day> allDays = EnumSet.allOf(Day.class);
System.out.println("所有天: " + allDays);
// 2. 创建空 EnumSet
EnumSet<Day> empty = EnumSet.noneOf(Day.class);
System.out.println("空集合: " + empty);
// 3. 创建指定范围的 EnumSet
EnumSet<Day> weekdays = EnumSet.range(Day.MONDAY, Day.FRIDAY);
System.out.println("工作日: " + weekdays);
// 4. 创建指定常量子集
EnumSet<Day> weekend = EnumSet.of(Day.SATURDAY, Day.SUNDAY);
System.out.println("周末: " + weekend);
// 5. 补集
EnumSet<Day> notWeekend = EnumSet.complementOf(weekend);
System.out.println("非周末: " + notWeekend);
// 6. 批量操作
EnumSet<Day> myDays = EnumSet.copyOf(weekdays);
myDays.remove(Day.WEDNESDAY);
myDays.add(Day.SATURDAY);
System.out.println("我的选择: " + myDays);
// 7. 遍历
System.out.println("\n工作日列表:");
for (Day day : weekdays) {
System.out.println(" - " + day);
}
}
}输出:
所有天: [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
空集合: []
工作日: [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY]
周末: [SATURDAY, SUNDAY]
非周末: [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY]
我的选择: [MONDAY, TUESDAY, THURSDAY, FRIDAY, SATURDAY]
工作日列表:
- MONDAY
- TUESDAY
- WEDNESDAY
- THURSDAY
- FRIDAY9.2 EnumMap
EnumMap 是一个以枚举为键的 Map 实现,内部用数组存储,性能优于 HashMap。
import java.util.EnumMap;
import java.util.Map;
public class EnumMapDemo {
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
public static void main(String[] args) {
// 创建 EnumMap —— 必须在构造时指定枚举类型
EnumMap<Day, String> schedule = new EnumMap<>(Day.class);
// 添加元素
schedule.put(Day.MONDAY, "开会");
schedule.put(Day.WEDNESDAY, "代码审查");
schedule.put(Day.FRIDAY, "项目汇报");
schedule.put(Day.SATURDAY, "休息");
schedule.put(Day.SUNDAY, "休息");
// 遍历 —— 按枚举声明顺序输出
System.out.println("=== 一周日程 ===");
for (Map.Entry<Day, String> entry : schedule.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 常见操作
System.out.println("\n=== 其他操作 ===");
System.out.println("MONDAY 的安排: " + schedule.get(Day.MONDAY));
System.out.println("是否包含 TUESDAY: " +
schedule.containsKey(Day.TUESDAY));
System.out.println("大小: " + schedule.size());
// 使用 Lambda 遍历
System.out.println("\n=== 使用 forEach ===");
schedule.forEach((day, task) ->
System.out.println(day + " -> " + task));
}
}输出:
=== 一周日程 ===
MONDAY: 开会
WEDNESDAY: 代码审查
FRIDAY: 项目汇报
SATURDAY: 休息
SUNDAY: 休息
=== 其他操作 ===
MONDAY 的安排: 开会
是否包含 TUESDAY: false
大小: 5
=== 使用 forEach ===
MONDAY -> 开会
WEDNESDAY -> 代码审查
FRIDAY -> 项目汇报
SATURDAY -> 休息
SUNDAY -> 休息9.3 EnumSet / EnumMap 与普通集合的性能对比
import java.util.*;
public class EnumPerformanceDemo {
public enum Fruit {
APPLE, BANANA, ORANGE, GRAPE, WATERMELON,
PEAR, PEACH, CHERRY, LEMON, MANGO
}
public static void main(String[] args) {
int iterations = 10_000_000;
// ---- EnumSet vs HashSet ----
// EnumSet
long start1 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
EnumSet<Fruit> set = EnumSet.of(Fruit.APPLE, Fruit.BANANA);
set.contains(Fruit.APPLE);
}
long end1 = System.nanoTime();
System.out.printf("EnumSet 耗时: %.2f ms%n", (end1 - start1) / 1_000_000.0);
// HashSet
long start2 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
HashSet<Fruit> set = new HashSet<>();
set.add(Fruit.APPLE);
set.add(Fruit.BANANA);
set.contains(Fruit.APPLE);
}
long end2 = System.nanoTime();
System.out.printf("HashSet 耗时: %.2f ms%n", (end2 - start2) / 1_000_000.0);
// ---- EnumMap vs HashMap ----
// EnumMap
long start3 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
EnumMap<Fruit, String> map = new EnumMap<>(Fruit.class);
map.put(Fruit.APPLE, "苹果");
map.get(Fruit.APPLE);
}
long end3 = System.nanoTime();
System.out.printf("EnumMap 耗时: %.2f ms%n", (end3 - start3) / 1_000_000.0);
// HashMap
long start4 = System.nanoTime();
for (int i = 0; i < iterations; i++) {
HashMap<Fruit, String> map = new HashMap<>();
map.put(Fruit.APPLE, "苹果");
map.get(Fruit.APPLE);
}
long end4 = System.nanoTime();
System.out.printf("HashMap 耗时: %.2f ms%n", (end4 - start4) / 1_000_000.0);
}
}输出示例(在实际机器上运行):
EnumSet 耗时: 45.23 ms
HashSet 耗时: 312.87 ms
EnumMap 耗时: 38.56 ms
HashMap 耗时: 285.43 ms为什么 EnumSet / EnumMap 更快?
EnumSet内部使用位向量(long的比特位),每位代表一个枚举常量,操作通过位运算完成EnumMap内部使用对象数组,键的ordinal()直接作为数组下标,没有哈希计算和碰撞处理
10. 小结
| 知识点 | 要点 |
|---|---|
| 枚举定义 | 使用 enum 关键字,常量列表用逗号分隔 |
| 字段与方法 | 枚举可以有字段、构造方法(必须是 private)和普通方法 |
| 内置方法 | values()、valueOf()、ordinal()、name()、compareTo() |
| switch | 枚举与 switch 搭配使用,case 中不需要枚举类型前缀 |
| 单例模式 | 枚举是实现单例的最佳方式,天然线程安全、序列化安全、反射安全 |
| 实现接口 | 枚举可以 implements 接口,统一不同常量的行为 |
| 抽象方法 | 枚举可以定义抽象方法,每个常量独立实现,实现策略模式 |
| EnumSet / EnumMap | 高性能枚举专用集合,内部使用位向量/数组,性能远超普通集合 |
Java 枚举远不止是"常量的集合"——它是一种完整的类型,支持字段、方法、接口实现、抽象方法,甚至可以作为单例模式和策略模式的载体。合理使用枚举可以显著提升代码的类型安全性、可读性和运行效率。