什么是枚举?
一句话理解 :枚举就是”固定选项”——你定义好一组常量,使用时只能从中选一个,不能乱填
比喻:枚举就像单选题——ABCD四个选项写死了,你不能自己编个E出来
生活中到处都是枚举:一周七天、四季、红绿灯、HTTP状态码、性别
没有枚举之前怎么办?用 int 常量或 String 常量,但很容易传错值
一、基本定义
1 2 3 4 5 6 enum Season {SPRING, SUMMER, AUTUMN, WINTER }
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void testBasicEnum () {Season season = Season.SPRING;System.out.println(season); Season s = Season.WINTER; }
二、枚举比常量好在哪?
对比:用int常量 vs 用枚举
1 2 3 4 5 6 7 8 9 10 11 12 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 ;}
维度
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;HttpStatus(int code, String message) { this .code = code; this .message = message; } 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()); System.out.println(status.getMessage()); System.out.println(status); for (HttpStatus s : HttpStatus.values()) { System.out.println(s.getCode() + " - " + s.name() + " - " + s.getMessage()); } }
枚举构造器必须是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 () {Season[] seasons = Season.values(); System.out.println(Arrays.toString(seasons)); Season spring = Season.valueOf("SPRING" );System.out.println(spring); System.out.println(Season.SUMMER.name()); System.out.println(Season.SUMMER.ordinal()); Season s = Season.WINTER;System.out.println(s == Season.WINTER); }
⚠️ 不要依赖 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()); } }
也可以用统一实现 + 抽象方法的方式:
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 (season) { case SPRING: System.out.println("春暖花开" ); break ; case SUMMER: System.out.println("烈日炎炎" ); break ; case AUTUMN: System.out.println("秋高气爽" ); break ; case WINTER: System.out.println("寒风凛冽" ); break ; } 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); db1.connect(); }
为什么枚举单例是”最安全”的?
攻击方式
普通单例
枚举单例
反射机制 创建新实例
❌ 会被攻破
✅ 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() 方法返回下一个状态,最终状态返回自身