Java基础(十四):枚举类详解
Java基础系列文章
Java基础(一):初识Java——发展历程、技术体系与JDK环境搭建
Java基础(二):八种基本数据类型详解
Java基础(三):逻辑运算符详解
Java基础(四):位运算符详解
Java基础(五):流程控制全解析——分支(if/switch)和循环(for/while)的深度指南
Java基础(六):数组全面解析
Java基础(七): 面向过程与面向对象、类与对象、成员变量与局部变量、值传递与引用传递、方法重载与方法重写
Java基础(八):封装、继承、多态与关键字this、super详解
Java基础(九):Object核心类深度剖析
Java基础(十):关键字static详解
Java基础(十一):关键字final详解
Java基础(十二):抽象类与接口详解
Java基础(十三):内部类详解
Java基础(十四):枚举类详解
目录
- 一、枚举类概述
- 二、基本枚举定义
- 1、最简单的枚举
- 2、带有属性和方法的枚举
- 3、枚举的特点
- 三、枚举方法和工具类
- 1、枚举的隐含方法和属性
- 2、工具类EnumSet/EnumMap
- 四、枚举实现接口
- 五、枚举的单例模式
- 1、为什么使用枚举实现单例?
- 2、对比传统单例写法(如双重检查锁、静态内部类等)
一、枚举类概述
枚举
(Enum)是Java 5引入的一种特殊数据类型,它允许我们预定义一组常量
。在Java中,枚举是一种特殊的类,它继承自java.lang.Enum
类,具有类的所有特性。
为什么需要枚举?
- 在枚举出现之前,我们通常使用以下方式表示一组常量
public class Season {public static final int SPRING = 1;public static final int SUMMER = 2;public static final int AUTUMN = 3;public static final int WINTER = 4;
}
这种方式存在几个问题:
类型系统约束
:枚举变量只能接受特定的枚举常量,传统常量编译时不能校验合法性可读性差
:数字本身没有表达其含义,难以理解无法添加属性或方法
难以维护
:如果常量值需要修改,可能影响多处代码
枚举解决了这些问题,提供了更安全、更强大的常量表示方式。
二、基本枚举定义
1、最简单的枚举
- 这个枚举定义了7个常量,每个都是
Day类型的实例
public enum Day {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
编译器生成的大致等价代码:
// 编译器生成的类,实际名称仍然是 Day,继承自 java.lang.Enum
public final class Day extends Enum<Day> {// 枚举实例,是 Day 类的静态 final 实例,每个都是唯一的public static final Day SUNDAY = new Day("SUNDAY", 0);public static final Day MONDAY = new Day("MONDAY", 1);public static final Day TUESDAY = new Day("TUESDAY", 2);public static final Day WEDNESDAY = new Day("WEDNESDAY", 3);public static final Day THURSDAY = new Day("THURSDAY", 4);public static final Day FRIDAY = new Day("FRIDAY", 5);public static final Day SATURDAY = new Day("SATURDAY", 6);// 内部维护一个所有枚举值的数组,可通过 Day.values() 获取private static final Day[] $VALUES = new Day[]{SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY};// 枚举的构造方法是私有的,不允许外部创建新的实例private Day(String name, int ordinal) {super(name, ordinal); // 调用父类 Enum 的构造方法}// 返回所有枚举值,等同于你调用 Day.values()public static Day[] values() {return $VALUES.clone(); // 返回一个副本,防止外部修改内部数组}// 根据名称返回对应的枚举实例,等同于你调用 Day.valueOf("MONDAY")public static Day valueOf(String name) {return Enum.valueOf(Day.class, name); // 调用 Enum 类的静态方法}
}
2、带有属性和方法的枚举
实例声明
:枚举实例是类级别的,必须在首行用逗号(,)分隔
,若枚举类包含方法/属性,最后一个实例后需加分号(;)
构造函数
:枚举的构造函数默认且只能是private
(即使不写修饰符),防止外部创建新实例属性限制
:属性通常声明为final
(不强求),保证不可变性(枚举实例通常是单例且线程安全的)
// 订单状态枚举
public enum OrderStatus {// 枚举实例(必须位于首行)PENDING("待支付", 0),PAID("已支付", 1),SHIPPED("已发货", 2),COMPLETED("已完成", 3);// 枚举属性(通常为final)private final String desc; // 描述private final int code; // 状态码// 构造函数(必须私有!,没有修饰符也表示private)private OrderStatus(String desc, int code) {this.desc = desc;this.code = code;}// 根据 code状态码 直接获取对应的 desc描述private static final Map<Integer, OrderStatus> CODE_TO_ENUM_MAP = new HashMap<>();// 静态代码块:初始化映射static {for (OrderStatus status : OrderStatus.values()) {CODE_TO_ENUM_MAP.put(status.getCode(), status);}}public static String getDescByCode(int code) {OrderStatus status = CODE_TO_ENUM_MAP.get(code);if (status == null){throw new IllegalArgumentException("无效的订单状态码: " + code);}return status.getDesc();}// Getter方法public String getDesc() {return desc;}public int getCode() {return code;}
}
- 枚举实例本质是类的
静态常量
,通过类名直接访问
OrderStatus status = OrderStatus.PAID;
System.out.println(status.getDesc()); // 输出:已支付
System.out.println(status.getCode()); // 输出:1
编译器生成的大致等价代码:
public final class OrderStatus extends Enum<OrderStatus> {// 静态的枚举实例,每个都是 OrderStatus 类型的静态 final 对象public static final OrderStatus PENDING = new OrderStatus("PENDING", 0, "待支付", 0);public static final OrderStatus PAID = new OrderStatus("PAID", 1, "已支付", 1);public static final OrderStatus SHIPPED = new OrderStatus("SHIPPED", 2, "已发货", 2);public static final OrderStatus COMPLETED = new OrderStatus("COMPLETED", 3, "已完成", 3);// // 编译器生成的 values 数组,用于 values() 方法(按声明顺序)private static final OrderStatus[] $VALUES = {PENDING, PAID, SHIPPED, COMPLETED};// 自定义字段private final String desc;private final int code;// 私有构造器,确保外部无法随意创建枚举实例private OrderStatus(String name, int ordinal, String desc, int code) {super(name, ordinal); // 调用父类 Enum 的构造方法this.desc = desc;this.code = code;}// 自定义的code获取desc方法private static final Map<Integer, OrderStatus> CODE_TO_ENUM_MAP = new HashMap<>();static {for (OrderStatus status : OrderStatus.values()) {CODE_TO_ENUM_MAP.put(status.getCode(), status);}}public static String getDescByCode(int code) {OrderStatus status = CODE_TO_ENUM_MAP.get(code);if (status == null){throw new IllegalArgumentException("无效的订单状态码: " + code);}return status.getDesc();}// 自定义的getter方法public String getDesc() {return desc;}public int getCode() {return code;}// 编译器生成的方法public static OrderStatus[] values() {return $VALUES.clone();// 返回所有枚举值的拷贝}public static OrderStatus valueOf(String name) {// 根据 name 查找对应的枚举实例,找不到则抛出 IllegalArgumentExceptionreturn Enum.valueOf(OrderStatus.class, name);}
}
3、枚举的特点
- 枚举常量默认是
public static final
的 - 枚举类隐式继承自
java.lang.Enum
- 枚举不能被继承(因为已经继承了
Enum
) - 枚举构造函数总是私有的(可以
省略private
关键字) - 枚举可以添加
字段
、方法
和构造函数
三、枚举方法和工具类
1、枚举的隐含方法和属性
所有枚举都隐式继承自java.lang.Enum
,因此具有以下方法:
name()
:返回枚举常量的名称(字符串)ordinal()
:返回枚举常量的序数(声明顺序
,从0开始)toString()
:默认返回 name,可重写
提供友好信息equals()
:比较两个枚举引用是否指向同一个对象(但通常直接用==
更直观)hashCode()
:枚举的哈希码实现( 一般不用关心)compareTo(E o)
:比较两个枚举常量的序数(一般不用,除非需要排序)valueOf(String name)
:通过name字符串获取枚举常量(由编译器生成,不是Enum类的方法)values()
:返回枚举的所有常量(也是由编译器生成,不是Enum类的方法)
示例:
enum Color { RED, GREEN, BLUE }public class EnumDemo {public static void main(String[] args) {Color c = Color.RED;System.out.println(c.name()); // 输出: REDSystem.out.println(c.ordinal()); // 输出: 0System.out.println(c.toString()); // 输出: RED// 使用values()方法for (Color color : Color.values()) {System.out.println(color);}// 使用valueOf()方法Color red = Color.valueOf("RED");System.out.println(red == Color.RED); // 输出: true}
}
2、工具类EnumSet/EnumMap
Java提供了专门用于枚举的集合实现,位于java.util
包中:
EnumSet
- 高性能的枚举集合
实现EnumMap
-键为枚举
的高性能Map实现
EnumSet示例:
enum Day {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}public class EnumSetDemo {public static void main(String[] args) {// 创建一个包含所有Day枚举的EnumSetEnumSet<Day> allDays = EnumSet.allOf(Day.class);System.out.println("所有天: " + allDays);// 创建一个空的EnumSetEnumSet<Day> noDays = EnumSet.noneOf(Day.class);noDays.add(Day.MONDAY);noDays.add(Day.FRIDAY);System.out.println("工作日: " + noDays);// 创建一个包含范围的EnumSetEnumSet<Day> weekend = EnumSet.range(Day.SATURDAY, Day.SUNDAY);System.out.println("周末: " + weekend);// 创建一个补集EnumSet<Day> weekdays = EnumSet.complementOf(weekend);System.out.println("工作日: " + weekdays);}
}
EnumMap示例:
enum Day {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}public class EnumMapDemo {public static void main(String[] args) {EnumMap<Day, String> activityMap = new EnumMap<>(Day.class);activityMap.put(Day.MONDAY, "上班");activityMap.put(Day.TUESDAY, "上班");activityMap.put(Day.WEDNESDAY, "上班");activityMap.put(Day.THURSDAY, "上班");activityMap.put(Day.FRIDAY, "上班");activityMap.put(Day.SATURDAY, "休息");activityMap.put(Day.SUNDAY, "休息");for (Day day : Day.values()) {System.out.println(day + ": " + activityMap.get(day));}}
}
四、枚举实现接口
枚举类可以实现一个或多个接口
,为所有实例或单个实例提供统一行为
。这在需要为不同枚举实例定义差异化逻辑时非常有用。
示例:定义支付策略接口:
// 支付策略接口
public interface PaymentStrategy {void pay(double amount);
}// 支付方式枚举(实现接口)
public enum PaymentMethod implements PaymentStrategy {ALIPAY {@Overridepublic void pay(double amount) {System.out.println("使用支付宝支付:" + amount + "元");}},WECHAT_PAY {@Overridepublic void pay(double amount) {System.out.println("使用微信支付:" + amount + "元");}},BANK_CARD {@Overridepublic void pay(double amount) {System.out.println("使用银行卡支付:" + amount + "元");}};
}
使用接口方法:
PaymentMethod method = PaymentMethod.ALIPAY;
method.pay(199.9); // 输出:使用支付宝支付:199.9元
五、枚举的单例模式
单例模式
的核心是确保类仅一个实例
,并提供全局访问点。枚举凭借JVM的类加载机制,天生具备线程安全、防反射攻击、防反序列化漏洞的特性,是《Effective Java》
作者Joshua Bloch推荐的最佳实现方式。
1、为什么使用枚举实现单例?
线程安全
:枚举实例的创建是由JVM在类加载时完成的
,保证线程安全防止反射攻击
:普通的单例如果通过反射调用私有构造方法,可能会创建多个实例,但枚举类型不允许通过反射创建实例
- Java 语言规范禁止反射创建枚举实例,否则抛异常
Cannot reflectively create enum objects
- Java 语言规范禁止反射创建枚举实例,否则抛异常
避免序列化问题
:普通的单例如果实现了Serializable接口,在反序列化时可能会生成新的对象。而枚举天然防止了这一问题- 因为普通单例反序列化时,会通过反射或其他方式创建一个新的对象实例,而不是返回原来的那个单例对象
- 而枚举
序列化只保存枚举常量名称
,不保存对象本身,反序列化时,JVM 根据这个名字直接返回对应的、已经存在的枚举常量对象
示例:使用枚举实现单例模式
public enum Singleton {INSTANCE; // 这就是单例对象// 可以添加单例的其他成员变量和方法private int value;public void doSomething() {System.out.println("Singleton is doing something. Value = " + value);}public int getValue() {return value;}public void setValue(int value) {this.value = value;}
}
使用方法:
public class Main {public static void main(String[] args) {// 获取单例对象Singleton singleton = Singleton.INSTANCE;singleton.setValue(42);singleton.doSomething(); // 输出: Singleton is doing something. Value = 42// 再次获取,仍然是同一个对象Singleton anotherSingleton = Singleton.INSTANCE;System.out.println(singleton == anotherSingleton); // 输出: true}
}
2、对比传统单例写法(如双重检查锁、静态内部类等)
实现方式 | 线程安全 | 防止反射攻击 | 防止序列化破坏单例 | 代码复杂度 |
---|---|---|---|---|
枚举(推荐) | ✅ | ✅ | ✅ | ⭐ |
双重检查锁 | ✅ | ❌ | ❌ (需额外处理) | ⭐⭐⭐ |
静态内部类 | ✅ | ❌ | ❌ (需额外处理) | ⭐⭐ |
饿汉式 | ✅ | ❌ | ❌ (需额外处理) | ⭐ |
懒汉式(非线程安全) | ❌ | ❌ | ❌ | ⭐ |
不需要兼容老代码,且追求最安全、最简洁的单例实现,那么使用枚举是最佳选择!