为什么需要枚举?
在 java 开发中,经常需要定义一组固定的常量,比如订单状态、用户角色、星期、季节等。早期的做法是使用 public static final 常量:
public class orderstatus {
public static final int pending = 0;
public static final int processing = 1;
public static final int completed = 2;
public static final int cancelled = 3;
}
这种方式存在明显缺陷:类型不安全(可以传入任意 int 值)、可读性差、无法遍历所有常量。java 5 引入的枚举(enum)正是为解决这些问题而生 。
枚举是一种特殊的类,它表示一组固定的常量,提供了类型安全、更好的可读性和丰富的扩展能力 。
枚举的概念
使用 enum 关键字定义枚举,常量之间用逗号分隔,以分号结尾:
public enum season {
spring, summer, autumn, winter;
}
使用方式非常简单直接:
season current = season.spring; system.out.println(current); // 输出: spring
枚举的常用内置方法
每个枚举类都隐式继承自 java.lang.enum,因此拥有以下实用方法 :
| 方法 | 说明 |
|---|---|
values() | 返回包含所有枚举常量的数组 |
valueof(string name) | 根据名称字符串返回对应的枚举常量 |
name() | 返回枚举常量的确切名称 |
ordinal() | 返回枚举常量在声明中的位置索引(从 0 开始) |
tostring() | 返回枚举常量的名称(可重写) |
使用示例:
// 遍历所有枚举值
for (season s : season.values()) {
system.out.println(s + " 序号: " + s.ordinal());
}
// 输出:
// spring 序号: 0
// summer 序号: 1
// autumn 序号: 2
// winter 序号: 3
// 字符串转枚举
season winter = season.valueof("winter");
system.out.println(winter); // 输出: winter注意:
ordinal()的值会随枚举常量声明顺序改变而变化,如果业务中需要固定的数值标识,建议自定义字段而非依赖ordinal()。
枚举与switch
枚举与 switch 能让分支逻辑更加清晰 :
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;
}进阶特性:枚举不只是常量
为枚举添加属性和构造函数⭐
枚举可以像普通类一样拥有成员变量、构造函数和自定义方法,这让每个枚举常量都能携带丰富的元数据 :
public enum color {
red("红色", "#ff0000"),
green("绿色", "#00ff00"),
blue("蓝色", "#0000ff");
private final string chinesename;
private final string hexcode;
// 枚举构造函数必须是 private(可省略不写)
color(string chinesename, string hexcode) {
this.chinesename = chinesename;
this.hexcode = hexcode;
}
public string getchinesename() {
return chinesename;
}
public string gethexcode() {
return hexcode;
}
}
// 使用
color red = color.red;
system.out.println(red.getchinesename() + " 的十六进制码是 " + red.gethexcode());
// 输出: 红色 的十六进制码是 #ff0000枚举(enum)的构造函数必须是 private(如果不写,编译器也会默认它是 private),这背后的核心原因在于枚举的本质是单例模式的扩展。
编译器的底层原理⭐
定义一个枚举时,java 编译器在底层会将其转换成一个继承自 java.lang.enum 的 final 类。
enum color { red; }
在编译后大致等同于:
public final class color extends enum<color> {
public static final color red = new color("red", 0);
// 强制私有,仅供内部静态初始化调用
private color(string name, int ordinal) {
super(name, ordinal);
}
}- 类是
final的,且构造函数是private的,所以除了在类内部定义的常量外,没有任何办法产生新的对象。而且枚举不能被继承,但是可以通过实现接口来扩展行为。 red是public static final的。静态变量是在类加载(class loading)阶段进行初始化的,而 jvm 保证了一个类在整个生命周期中只会被加载一次。
抽象方法与策略模式
枚举可以定义抽象方法,让每个枚举常量提供不同的实现。这本质上是策略模式的实现 :
public enum operation {
add {
@override
public int apply(int x, int y) {
return x + y;
}
},
subtract {
@override
public int apply(int x, int y) {
return x - y;
}
},
multiply {
@override
public int apply(int x, int y) {
return x * y;
}
},
divide {
@override
public int apply(int x, int y) {
if (y == 0) throw new illegalargumentexception("除数不能为零");
return x / y;
}
};
public abstract int apply(int x, int y);
}
// 使用
int result = operation.add.apply(10, 5); // 15
system.out.println(operation.multiply.apply(3, 4)); // 12这种方式消除了冗长的 if-else 或 switch 分支,新增操作只需添加新的枚举常量并实现抽象方法,符合开闭原则 。
实现接口
枚举可以实现接口,获得额外的抽象能力,同时保持类型安全 :
public interface describable {
string getdescription();
}
public enum day implements describable {
monday("周一综合征高发期"),
friday("即将迎来周末的快乐"),
sunday("周末余额不足的忧伤");
private final string description;
day(string description) {
this.description = description;
}
@override
public string getdescription() {
return description;
}
}枚举与反射 ⭐
枚举是否可以通过反射,拿到实例对象呢?结论:不可以。枚举是 java 中唯一能从根本上防御反射攻击的对象。
假设有一个简单的枚举:
public enum singleton {
instance;
}
如果尝试像对待普通类那样,通过反射获取构造器并创建实例:
constructor<singleton> constructor = singleton.class.getdeclaredconstructor(); constructor.setaccessible(true); singleton newinstance = constructor.newinstance();
会得到一个 nosuchmethodexception。是不是因为没传参数?(因为枚举编译后构造函数默认有两个参数:string name 和 int ordinal)。
好,加上参数再试:
constructor<singleton> constructor = singleton.class.getdeclaredconstructor(string.class, int.class);
constructor.setaccessible(true);
singleton newinstance = constructor.newinstance("hacker", 1);
这时,得到一个更绝望的异常:illegalargumentexception: cannot reflectively create enum objects
为什么反射失效了?
反射之所以失败,不是因为找不到方法,而是因为 constructor.newinstance() 的源代码里写了死命令。
public t newinstance(object ... initargs) {
// ... 前面的一些检查 ...
// 【关键点】这里直接判断了修饰符
if ((this.modifiers & modifier.enum) != 0)
throw new illegalargumentexception("cannot reflectively create enum objects");
// ... 后续逻辑 ...
}java 之父们在设计反射 api 时,就已经预料到了有人会尝试用反射破坏枚举的单例性。于是他们在 newinstance 方法里加了一个硬编码检查:只要发现这个类是 enum 类型,直接抛异常
在所有的单例模式实现方案中,枚举(enum)被公认为是最安全的。
| 攻击方式 | 普通单例(dcl、静态内部类) | 枚举单例 |
|---|---|---|
| 反射攻击 | 可以通过 setaccessible(true) 破解 | 彻底免疫(jvm 底层拦截) |
| 序列化攻击 | 需要重写 readresolve() 才能保证唯一 | 原生支持(jvm 自动保证) |
| 代码简洁度 | 较复杂 | 极其简洁 |
笔试面试题
枚举能否被继承?能否继承其他类?⭐
答案:
- 枚举不能被继承(隐式 final)
- 枚举不能显式继承其他类(因为已经隐式继承
java.lang.enum) - 但枚举可以实现接口
// 错误示例
// public enum myenum extends someclass { } // 编译错误
// public class subenum extends myenum { } // 编译错误
// 正确示例
public enum myenum implements runnable {
instance;
@override
public void run() { }
}枚举的构造函数可以用 public 修饰吗?⭐
答案:不可以。枚举的构造函数只能是
private(可省略,默认就是 private)。
public enum color {
red("红色");
// public color(string name) {} // 编译错误
color(string name) { } // 正确,默认 private
}原理:枚举实例由 jvm 在类加载时自动创建,不允许外部通过 new 实例化。
ordinal() 方法返回什么?能否在 switch 中使用?
答案:返回枚举常量在声明中的位置索引(从 0 开始)。不建议在 switch 中使用 ordinal(),直接用枚举常量即可。
public enum status {
pending, processing, completed;
}
// 错误用法
switch (status.ordinal()) {
case 0: // pending
case 1: // processing
}
// 正确用法
switch (status) {
case pending:
case processing:
}枚举构造函数的执行⭐
public enum singleton {
instance;
private singleton() {
system.out.println("构造函数执行");
}
public static void main(string[] args) {
singleton s1 = singleton.instance;
singleton s2 = singleton.instance;
system.out.println(s1 == s2);
}
}构造函数执行 true
public enum singleton {
instance;
private singleton() {
system.out.println("构造函数执行");
}
}相当于
public final class singleton {
// 静态常量,类加载时初始化
public static final singleton instance = new singleton();
private singleton() {
system.out.println("构造函数执行");
}
}- 枚举实例是静态常量属性(static final),它在类加载时初始化构造函数只执行一次
- 执行
singleton s1 = singleton.instance;时,触发了上述加载过程,对象创建。 - 执行
singleton s2 = singleton.instance;时,由于instance是静态常量,它已经存在于内存中了。此时s2只是直接指向了那个已经创建好的对象。
所以 s1 == s2 结果为 true,因为它们指向的是内存中同一个地址。
常用内置方法的使用
public enum weekday {
mon, tue, wed, thu, fri, sat, sun;
public static void main(string[] args) {
system.out.println(weekday.mon.ordinal());
system.out.println(weekday.valueof("fri").ordinal());
system.out.println(weekday.values()[2]);
}
}0 4 wed
解析:
mon.ordinal()为 0(第一个)fri.ordinal()为 4(第五个,索引从 0 开始)values()[2]返回索引 2 的常量,即 wed
用枚举实现一个简单的计算器,支持加减乘除
参考答案:
public enum calculator {
add("+") {
@override
public double calculate(double a, double b) {
return a + b;
}
},
subtract("-") {
@override
public double calculate(double a, double b) {
return a - b;
}
},
multiply("*") {
@override
public double calculate(double a, double b) {
return a * b;
}
},
divide("/") {
@override
public double calculate(double a, double b) {
if (b == 0) throw new arithmeticexception("除数不能为零");
return a / b;
}
};
private final string symbol;
calculator(string symbol) {
this.symbol = symbol;
}
public abstract double calculate(double a, double b);
public string getsymbol() {
return symbol;
}
}
// 测试
double result = calculator.add.calculate(10, 5); // 15.0为什么说枚举实现单例是最佳实践?
- 线程安全:jvm 保证枚举实例的初始化是线程安全的
- 防止反射攻击:
constructor.newinstance()对枚举会抛出illegalargumentexception - 防止序列化破坏:枚举的序列化机制保证反序列化后仍是同一实例
- 代码简洁:两行代码搞定
public enum singleton {
instance;
public void dosomething() {
// 业务方法
}
}
对比其他实现:
- 懒汉式需要
volatile+ 双重检查锁 - 静态内部类虽然好,但仍无法防止反射攻击
- 只有枚举能天然防御所有破坏方式
enumset 和 hashset 的区别?什么场景用 enumset?
| 特性 | enumset | hashset |
|---|---|---|
| 底层实现 | 位向量(bit vector) | hashmap |
| 元素类型 | 只能是枚举 | 任意对象 |
| 性能 | 极快(位运算) | 相对较慢 |
| 内存占用 | 极小 | 较大 |
| null 支持 | 不支持 | 支持 |
使用场景:需要存储枚举类型的集合时,优先使用 enumset。
// 示例:批量处理周末
enumset<weekday> weekend = enumset.of(weekday.sat, weekday.sun);
if (weekend.contains(today)) {
system.out.println("今天是周末");
}如何遍历所有枚举值并获取其属性?
public enum httpstatus {
ok(200, "成功"),
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; }
// 根据 code 查找对应的枚举
public static httpstatus fromcode(int code) {
for (httpstatus status : values()) {
if (status.code == code) {
return status;
}
}
throw new illegalargumentexception("未知状态码: " + code);
}
}枚举在 jvm 层面是如何保证线程安全的?
- 枚举的实例化在类加载的初始化阶段完成
- jvm 规范保证
<clinit>方法在多线程环境下是线程安全的(会加锁) - 枚举实例的创建由 jvm 调用,外部无法通过 new 创建新实例
枚举序列化为什么能防止创建新实例?
- 枚举类默认实现了
serializable接口 - 序列化时只保存枚举的
name属性,不保存成员变量 - 反序列化时通过
enum.valueof(class, string)获取已有的枚举实例 - 因此反序列化后获得的是同一个实例,而非新对象
// 反序列化本质调用 enum.valueof(myenum.class, name);
values() 方法来自哪里?⭐
values()不是继承自enum类的方法- 它是编译器自动生成的静态方法
- 在编译时被添加到枚举类中
用枚举实现一个简单的状态机(开关状态)⭐
public enum toggleswitch {
on {
@override
public toggleswitch toggle() {
return off;
}
},
off {
@override
public toggleswitch toggle() {
return on;
}
};
public abstract toggleswitch toggle();
}
// 使用
toggleswitch current = toggleswitch.off;
current = current.toggle(); // 切换到 on
current = current.toggle(); // 切换回 off设计一个订单状态枚举,包含状态流转校验⭐
题目:设计订单状态(待支付 → 已支付 → 已发货 → 已完成),每个状态只能按顺序流转,不能跳转或回退。
public enum orderstatus {
pending {
@override
public orderstatus next() {
return paid;
}
@override
public boolean cancancel() {
return true;
}
},
paid {
@override
public orderstatus next() {
return shipped;
}
@override
public boolean cancancel() {
return false;
}
},
shipped {
@override
public orderstatus next() {
return completed;
}
@override
public boolean cancancel() {
return false;
}
},
completed {
@override
public orderstatus next() {
throw new illegalstateexception("已完成订单无法继续流转");
}
@override
public boolean cancancel() {
return false;
}
};
public abstract orderstatus next();
public abstract boolean cancancel();
}到此这篇关于java枚举enum深度解析的文章就介绍到这了,更多相关java枚举enum内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论