枚举类型

什么是枚举?

一句话理解:枚举就是”固定选项”——你定义好一组常量,使用时只能从中选一个,不能乱填

比喻:枚举就像单选题——ABCD四个选项写死了,你不能自己编个E出来

生活中到处都是枚举:一周七天、四季、红绿灯、HTTP状态码、性别

没有枚举之前怎么办?用 int 常量或 String 常量,但很容易传错值

一、基本定义

1
2
3
4
5
6
// 用 enum 关键字定义
enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}

// 枚举常量名通常全大写(和static final常量一样的风格)
1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testBasicEnum() {
Season season = Season.SPRING;
System.out.println(season); // SPRING(直接打印名字)

// 枚举是类型安全的:
// Season s = "春天"; // ❌ 编译报错,类型不匹配
// Season s = 1; // ❌ 编译报错

// 只能赋值为 Season 里定义的四个值之一
Season s = Season.WINTER; // ✅
}

二、枚举比常量好在哪?

对比:用int常量 vs 用枚举

1
2
3
4
5
6
7
8
9
10
11
12
// ❌ 老方法:用int常量表示季节
class SeasonOld {
public static final int SPRING = 0;
public static final int SUMMER = 1;
public static final int AUTUMN = 2;
public static final int WINTER = 3;
}

// 问题1:类型不安全,传个5进来编译器不会报错
// printSeason(5); // 啥季节?不知道,但编译通过了
// 问题2:可读性差,打印出来是数字0123,不知道啥意思
// 问题3:没法遍历所有值
维度 int常量 String常量 enum枚举
类型安全 ❌ 任何int都能传 ❌ 任何字符串都能传 ✅ 只能传定义的值
可读性 ❌ 打印是数字 ✅ 打印是文字 ✅ 打印是名字
可遍历 ❌ 不行 ❌ 不行 values()
IDE提示 ❌ 没有 ❌ 没有 ✅ 自动补全
能有方法
能做switch ✅(Java 7+)

结论:能用枚举的地方,别用常量

三、枚举的构造器和字段

枚举不是简单的常量列表——每个枚举值本质上是一个对象,可以有自己的属性和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum HttpStatus {
// 枚举值(每个值调用构造器)
OK(200, "成功"),
NOT_FOUND(404, "未找到"),
FORBIDDEN(403, "禁止访问"),
INTERNAL_ERROR(500, "服务器内部错误");

// 字段
private final int code;
private final String message;

// 构造器(必须private,枚举不能外部new)
HttpStatus(int code, String message) {
this.code = code;
this.message = message;
}

// getter方法
public int getCode() { return code; }
public String getMessage() { return message; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testEnumWithFields() {
HttpStatus status = HttpStatus.NOT_FOUND;
System.out.println(status.getCode()); // 404
System.out.println(status.getMessage()); // 未找到
System.out.println(status); // NOT_FOUND

// 遍历所有状态码
for (HttpStatus s : HttpStatus.values()) {
System.out.println(s.getCode() + " - " + s.name() + " - " + s.getMessage());
}
// 输出:
// 200 - OK - 成功
// 404 - NOT_FOUND - 未找到
// 403 - FORBIDDEN - 禁止访问
// 500 - INTERNAL_ERROR - 服务器内部错误
}

枚举构造器必须是private(不写也默认是private),因为枚举值是固定的,不允许外部创建新实例

四、常用方法

方法 说明 示例
values() 返回所有枚举值的数组 Season.values()[SPRING, SUMMER, AUTUMN, WINTER]
valueOf(String) 字符串 → 枚举值 Season.valueOf("SPRING")Season.SPRING
name() 返回枚举常量的名字 Season.SPRING.name()"SPRING"
ordinal() 返回枚举常量的序号(从0开始) Season.SPRING.ordinal()0
toString() 默认和name()一样,可以重写 Season.SPRING.toString()"SPRING"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testEnumMethods() {
// values():遍历
Season[] seasons = Season.values();
System.out.println(Arrays.toString(seasons)); // [SPRING, SUMMER, AUTUMN, WINTER]

// valueOf():字符串转枚举
Season spring = Season.valueOf("SPRING");
System.out.println(spring); // SPRING
// Season.valueOf("spring"); // ❌ IllegalArgumentException,大小写必须完全匹配

// name() 和 ordinal()
System.out.println(Season.SUMMER.name()); // SUMMER
System.out.println(Season.SUMMER.ordinal()); // 1

// 枚举可以用 == 比较(不需要equals)
Season s = Season.WINTER;
System.out.println(s == Season.WINTER); // true ✅
}

⚠️ 不要依赖 ordinal()!

ordinal() 的值取决于枚举声明的顺序,如果以后有人在中间插入一个新值,所有序号都会变

需要数字标识时,自己定义字段(像上面HttpStatus的code)

五、枚举实现接口

枚举可以实现接口,每个枚举值可以有不同的实现——这是一种优雅的策略模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Describable {
String describe();
}

enum Planet implements Describable {
MERCURY {
@Override
public String describe() { return "离太阳最近的行星"; }
},
EARTH {
@Override
public String describe() { return "人类的家园"; }
},
MARS {
@Override
public String describe() { return "红色星球,未来移民目标"; }
};
}
1
2
3
4
5
6
7
8
9
@Test
public void testEnumImplementInterface() {
for (Planet p : Planet.values()) {
System.out.println(p.name() + ":" + p.describe());
}
// MERCURY:离太阳最近的行星
// EARTH:人类的家园
// MARS:红色星球,未来移民目标
}

也可以用统一实现 + 抽象方法的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum Operation {
ADD {
@Override
public double apply(double a, double b) { return a + b; }
},
SUBTRACT {
@Override
public double apply(double a, double b) { return a - b; }
},
MULTIPLY {
@Override
public double apply(double a, double b) { return a * b; }
};

public abstract double apply(double a, double b);
}

六、枚举用于switch

枚举和switch是天生一对,代码可读性极好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Test
public void testEnumSwitch() {
Season season = Season.SUMMER;

// switch + 枚举
switch (season) {
case SPRING: // 注意:case里不写Season.SPRING,直接写SPRING
System.out.println("春暖花开");
break;
case SUMMER:
System.out.println("烈日炎炎");
break;
case AUTUMN:
System.out.println("秋高气爽");
break;
case WINTER:
System.out.println("寒风凛冽");
break;
}

// Java 14+ 增强switch(更简洁)
String desc = switch (season) {
case SPRING -> "春暖花开";
case SUMMER -> "烈日炎炎";
case AUTUMN -> "秋高气爽";
case WINTER -> "寒风凛冽";
};
System.out.println(desc); // 烈日炎炎
}

switch + 枚举的优势:如果你少写了一个case,IDE会给警告

七、⭐ 枚举单例模式:最安全的单例

单例模式 = 一个类只能有一个实例。枚举天然就是单例的

1
2
3
4
5
6
7
8
9
10
// 最简单、最安全的单例实现
enum DatabaseConnection {
INSTANCE; // 只有一个枚举值,就是单例

private String url = "jdbc:mysql://localhost:3306/db";

public void connect() {
System.out.println("连接数据库:" + url);
}
}
1
2
3
4
5
6
7
8
9
@Test
public void testEnumSingleton() {
// 获取单例
DatabaseConnection db1 = DatabaseConnection.INSTANCE;
DatabaseConnection db2 = DatabaseConnection.INSTANCE;
System.out.println(db1 == db2); // true,确实是同一个对象

db1.connect(); // 连接数据库:jdbc:mysql://localhost:3306/db
}

为什么枚举单例是”最安全”的?

攻击方式 普通单例 枚举单例
反射机制 创建新实例 ❌ 会被攻破 ✅ JVM禁止反射创建枚举
反序列化创建新实例 ❌ 会被攻破(需要加readResolve) ✅ JVM保证反序列化返回同一实例
线程安全 ❌ 需要自己处理 ✅ JVM保证
克隆创建新实例 ❌ 需要自己处理 ✅ 枚举不能clone

⭐ Effective Java(Java圣经)的作者 Joshua Bloch 说:”单元素枚举是实现单例的最佳方式”

八、常见坑

坑1:valueOf() 大小写敏感

Season.valueOf("spring") → 直接抛 IllegalArgumentException

必须写 Season.valueOf("SPRING"),和定义时一模一样

安全写法:先转大写 Season.valueOf(input.toUpperCase())

坑2:ordinal() 不可靠

有人在枚举中间插入新值,后面所有ordinal都变了

如果需要固定数字,自己定义code字段

坑3:枚举不能继承

enum 隐式继承了 java.lang.Enum,Java不允许多继承

所以 enum MyEnum extends SomeClass 是编译错误

但枚举可以实现接口

坑4:枚举的构造器不能是public

写了public构造器会编译报错

枚举值是固定的,不允许外部new

坑5:在枚举的构造器中不能访问静态字段

枚举值在静态字段之前初始化,构造器执行时静态字段还没初始化

1
2
3
4
5
6
7
enum Bad {
A, B;
static int count = 0;
Bad() {
count++; // ⚠️ 编译可能不报错但行为可能不符合预期
}
}

九、练习题

练习1(基础):定义一个 Weekday 枚举(周一到周日),带中文名属性,写一个方法判断给定的Weekday是工作日还是周末

练习2(进阶):定义一个 Permission 枚举(READ, WRITE, EXECUTE),每个权限有一个int值(4, 2, 1),实现一个方法把权限组合转成数字(类似Linux文件权限 rwx = 7)

练习3(综合):用枚举实现一个简单的状态机——订单状态(CREATED → PAID → SHIPPED → DELIVERED),每个状态有一个 next() 方法返回下一个状态,最终状态返回自身


上一章 目录 下一章
注解 java基础 多线程与并发