关于枚举类型

什么是枚举类型

枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型

一般在实现枚举一些多次会被使用的常量时候,可以使用定义常量的方式,也就是 fianl int 枚举模式,这样的定义方式并没有什么错,但它存在许多不足,如在类型安全和使用方便性上并没有多少好处,如果存在定义int值相同的变量,混淆的几率还是很大的,编译器也不会提出任何警告

例如星期中每天的枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 不使用枚举类
public class DayWeek {
public static final int MONDAY =1;
public static final int TUESDAY=2;
public static final int WEDNESDAY=3;
public static final int THURSDAY=4;
public static final int FRIDAY=5;
public static final int SATURDAY=6;
public static final int SUNDAY=7;
}
//枚举类型,使用关键字enum
enum Day {
MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

在定义枚举类型时我们使用的关键字是enum,与class关键字类似,只不过前者是定义枚举类型,后者是定义类类型。然后然后定义枚举的名称、可访问性、基础类型和成员等。枚举声明的语法如下:

1
2
3
enum-modifiers enum enumname:enum-base {
enum-body,
}

其中,enum-modifiers 表示枚举的修饰符主要包括 public、private 和 internal;enumname 表示声明的枚举名称;enum-base 表示基础类型;enum-body 表示枚举的成员,它是枚举类型的命名常数。

要注意,任意两个枚举成员不能具有相同的名称,且它的常数值必须在该枚举的基础类型的范围之内,多个枚举成员之间使用逗号分隔。

其中,如果没有显式地声明基础类型的枚举,那么意味着它所对应的基础类型是 int。

而且枚举类型可以像类(class)类型一样,定义为一个单独的文件,当然也可以定义在其他类内部,但要保证枚举表示的类型其取值是必须有限的,也就是说每个值都是可以枚举出来的,有限且可枚举。

使用也很简单,直接引用就行这便是枚举类型的最简单模型。

1
2
3
4
public static void main(String[] args){
//直接引用
Day day = Day.MONDAY;
}

枚举的实现原理

实际上在使用关键字enum创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API中的java.lang.Enum类,也就是说通过关键字enum创建枚举类型在编译后事实上也是一个类类型,而且该类继承自java.lang.Enum

来看看反编译的一个枚举类文件

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//反编译Day.class
final class Day extends Enum
{
//编译器为我们添加的静态的values()方法
public static Day[] values()
{
return (Day[])$VALUES.clone();
}
//编译器为我们添加的静态的valueOf()方法,注意间接调用了Enum也类的valueOf方法
public static Day valueOf(String s)
{
return (Day)Enum.valueOf(com/zejian/enumdemo/Day, s);
}
//私有构造函数
private Day(String s, int i)
{
super(s, i);
}
//前面定义的7种枚举实例
public static final Day MONDAY;
public static final Day TUESDAY;
public static final Day WEDNESDAY;
public static final Day THURSDAY;
public static final Day FRIDAY;
public static final Day SATURDAY;
public static final Day SUNDAY;
private static final Day $VALUES[];

static
{
//实例化枚举实例
MONDAY = new Day("MONDAY", 0);
TUESDAY = new Day("TUESDAY", 1);
WEDNESDAY = new Day("WEDNESDAY", 2);
THURSDAY = new Day("THURSDAY", 3);
FRIDAY = new Day("FRIDAY", 4);
SATURDAY = new Day("SATURDAY", 5);
SUNDAY = new Day("SUNDAY", 6);
$VALUES = (new Day[] {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
});
}
}

使用关键字enum定义的枚举类型,在编译期后,也将转换成为一个实实在在的类,而在该类中,会存在每个在枚举类型中定义好变量的对应实例对象

编译器确实帮助我们生成了一个Day类(注意该类是final类型的,将无法被继承)而且 Java 中的每一个枚举都继承自 java.lang.Enum 类。当定义一个枚举类型时,每一个枚举类型成员都可以看作是 Enum 类的实例,这些枚举成员默认都被 final、public, static 修饰,当使用枚举类型成员时,直接使用枚举名称调用成员即可。

除此之外,编译器还帮助我们生成了7个Day类型的实例对象分别对应枚举中定义的7个日期,也就是说我们前面使用关键字enum定义的Day类型中的每种日期枚举常量也是实实在在的Day实例对象。

注意编译器还为我们生成了两个静态方法,分别是values()valueOf()

枚举的常见方法

Enum抽象类常见方法

Enum是所有 Java 语言枚举类型的公共基本类(注意Enum是抽象类),所有枚举实例都可以调用 Enum 类的方法

方法名称 描述
values() 以数组形式返回枚举类型的所有成员
valueOf() 将普通字符串转换为枚举实例
compareTo() 比较两个枚举成员在定义时的顺序
ordinal() 获取枚举成员的索引位置
name() 返回此枚举常量的名称,在其枚举声明中对其进行声明
static valueOf(Class enumType, String name) 返回带指定名称的指定枚举类型的枚举常量。

其他剩下的,别的抽象类和包装类都有的equalstoString,反射用的getDeclaringClass()这个类也有,不多介绍了。

values()方法是由编译器自动添加到每个枚举类中的静态方法,而非 Enum 类本身的方法。它会返回一个包含枚举所有成员的数组,顺序与枚举定义的顺序一致。

当你定义一个枚举类(如enum Season { SPRING, SUMMER })时,编译器会自动为该类添加两个静态方法:

  • public static Season[] values():返回枚举的所有实例。
  • public static Season valueOf(String name):根据名称查找枚举实例。

valueOf(String name)是Enum 类的静态方法,与Enum类中的valueOf方法的作用类似,用于根据名称获取枚举变量。字符串必须严格匹配枚举常量的名称,只不过编译器生成的valueOf方法更简洁些只需传递一个参数。这里我们还必须注意到,由于values()方法是由编译器插入到枚举类中的static方法,所以如果我们将枚举实例向上转型为Enum,那么values()方法将无法被调用,因为Enum类中并没有values()方法,valueOf()方法也是同样的道理,注意是一个参数的。

这里主要说明一下ordinal()方法,该方法获取的是枚举变量在枚举类中声明的顺序,顺序排列是下标从0开始,从第一个枚举的变量开始,如日期中的MONDAY在第一个位置,那么MONDAY的ordinal值就是0,如果MONDAY的声明位置发生变化,那么ordinal方法获取到的值也随之变化,注意在大多数情况下我们都不应该首先使用该方法。

compareTo(E o)方法则是比较枚举的大小,和其他的conpareTo一样,都是实现了 Comparable 接口的方法,用于比较枚举常量的顺序,注意其内部实现是根据每个枚举的ordinal值大小进行比较。

name()方法与toString()几乎是等同的,都是输出变量的字符串形式。

至于valueOf(Class<T> enumType, String name)方法则是用于根据指定的枚举类型和名称获取对应的枚举常量,允许在运行时动态解析枚举常量。

这个方法提供了一种反射方式来获取枚举实例,尤其适用于在运行时动态确定枚举类型的场景。注意,它是静态方法,直接通过 Enum.valueOf() 调用,无需枚举实例。在明确枚举类型的情况下,推荐优先使用 EnumType.valueOf(name) 以提高代码可读性和类型安全性。

举一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}

public class EnumValueOfExample {
public static void main(String[] args) {
// 方式一:直接通过枚举类调用 valueOf (更常用)
Season spring = Season.valueOf("SPRING");
System.out.println("直接调用: " + spring); // 输出: SPRING

// 方式二:通过 Enum 类的静态方法 valueOf (反射方式)
Season summer = Enum.valueOf(Season.class, "SUMMER");
System.out.println("反射调用: " + summer); // 输出: SUMMER

// 错误示例:名称不匹配
try {
Season invalid = Enum.valueOf(Season.class, "summer"); // 小写"s"
} catch (IllegalArgumentException e) {
System.out.println("错误: " + e.getMessage());
// 输出: No enum constant Season.summer
}
}
}
方法 调用方式 适用场景 异常处理
EnumType.valueOf(name) 直接通过枚举类调用 已知具体枚举类型时使用 编译时检查类型
Enum.valueOf(Class, name) 通过 Enum 类反射调用 运行时动态确定枚举类型 需手动处理 ClassNotFoundException

Enum类内部会有一个构造函数,该构造函数只能有编译器调用,我们是无法手动操作的,不妨看看Enum类的主要源码

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//实现了Comparable
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {

private final String name; //枚举字符串名称

public final String name() {
return name;
}

private final int ordinal;//枚举顺序值

public final int ordinal() {
return ordinal;
}

//枚举的构造方法,只能由编译器调用
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}

public String toString() {
return name;
}

public final boolean equals(Object other) {
return this==other;
}

//比较的是ordinal值
public final int compareTo(E o) {
Enum<?> other = (Enum<?>)o;
Enum<E> self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;//根据ordinal值比较大小
}

@SuppressWarnings("unchecked")
public final Class<E> getDeclaringClass() {
//获取class对象引用,getClass()是Object的方法
Class<?> clazz = getClass();
//获取父类Class对象引用
Class<?> zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
}


public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
//enumType.enumConstantDirectory()获取到的是一个map集合,key值就是name值,value则是枚举变量值
//enumConstantDirectory是class对象内部的方法,根据class对象获取一个map集合的值
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}

//.....省略其他没用的方法
}

枚举类与Class对象

上述我们提到当枚举实例向上转型为Enum类型后,values()方法将会失效,也就无法一次性获取所有枚举实例变量,因为,values()编译器自动生成 的静态方法,而非java.lang.Enum类的原生方法,Enum类本身只定义了所有枚举共有的方法(如name()ordinal()),并未包含values()values()是枚举类的 专属静态方法,而非继承自Enum

当你将枚举实例向上转型为Enum时:

1
2
Enum<?> season = Season.SPRING;  // 向上转型为 Enum
// season.values(); // 编译错误!Enum 类型没有 values() 方法
  • 静态绑定:Java 的方法调用在编译期会根据变量的 声明类型(而非实际类型)进行绑定。由于season的声明类型是Enum,而Enum类没有values()方法,因此编译器直接报错。
  • 类型信息丢失:向上转型后,变量season的具体枚举类型(如Season)被擦除,编译器只知道它是一个Enum,无法访问子类特有的方法。

虽然向上转型后无法直接调用values(),但是由于Class对象的存在,即使不使用values()方法,还是有可能一次获取到所有枚举实例变量的,在Class对象中存在如下方法:

返回类型 方法名称 方法说明
T[] getEnumConstants() 返回该枚举类型的所有元素,如果Class对象不是枚举类型,则返回null。
boolean isEnum() 当且仅当该类声明为源代码中的枚举时返回 true
1
2
3
4
5
6
7
8
Enum<?> season = Season.SPRING;
Class<?> enumClass = season.getDeclaringClass(); // 获取枚举的声明类(推荐使用该方法)

if (enumClass.isEnum()) {
// 使用 getEnumConstants() 直接获取枚举常量数组
Enum<?>[] enumConstants = enumClass.getEnumConstants();
System.out.println(Arrays.toString(enumConstants)); // 输出: [SPRING, SUMMER, AUTUMN, WINTER]
}

实际上这里使用EnumSet.allOf()是更常见的做法

1
2
3
4
5
6
Enum<?> season = Season.SPRING;
Class<? extends Enum> enumType = season.getDeclaringClass(); // 获取枚举类型

// 使用 EnumSet 获取所有枚举常量
EnumSet<?> allConstants = EnumSet.allOf(enumType);
System.out.println(allConstants); // 输出: [SPRING, SUMMER, AUTUMN, WINTER]

EnumSet等枚举类的容器

在 Java 中,针对枚举类型(enum),Java 集合框架提供了两个特殊的容器类:EnumSetEnumMap。它们专门为枚举类型设计,具有高效的性能和简洁的 API。

EnumSetSet接口的实现类,专门用于存储枚举类型的元素。它的设计充分利用了枚举类型的特性(元素数量固定、可枚举),因此性能远高于普通的HashSetTreeSet

更多内容关于这些容器的原理涉及到的内容太多了,打算单开一帖详细说明。

EnumSet 的特性

  • 元素唯一性:同Set接口,EnumSet中不允许重复元素。
  • 元素类型限制:只能存储同一枚举类型的元素(编译时检查)。
  • 有序性:元素的迭代顺序与枚举常量在枚举类中的定义顺序一致。
  • 高性能:底层通过位向量(bit-vector)实现(类似 “位运算”),添加、删除、查找元素的时间复杂度均为O(1)
  • 不可变实现:提供了noneOf()allOf()等方法创建可变实例,也可通过copyOf()等方法创建不可变实例(JDK 9+)。

但是,EnumSet没有公共构造方法,需通过静态工厂方法创建:

  • EnumSet.noneOf(Class<E> elementType):创建一个空的EnumSet,指定枚举类型。
  • EnumSet.allOf(Class<E> elementType):创建包含指定枚举类型所有元素的EnumSet
  • EnumSet.of(E e1, E e2, ...):创建包含指定枚举元素的EnumSet
  • EnumSet.range(E from, E to):创建包含从fromto(含)的所有枚举元素的EnumSet
  • EnumSet.copyOf(Collection<E> c):复制一个集合中的枚举元素到EnumSet(要求集合元素为同一枚举类型)。
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
import java.util.EnumSet;

// 定义一个枚举类
enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

public class EnumSetDemo {
public static void main(String[] args) {
// 创建包含所有星期的EnumSet
EnumSet<Day> allDays = EnumSet.allOf(Day.class);
System.out.println(allDays); // [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]

// 创建包含指定元素的EnumSet
EnumSet<Day> workDays = EnumSet.of(Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY, Day.THURSDAY, Day.FRIDAY);
System.out.println(workDays); // [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY]

// 创建范围元素的EnumSet(从MONDAY到FRIDAY)
EnumSet<Day> rangeDays = EnumSet.range(Day.MONDAY, Day.FRIDAY);
System.out.println(rangeDays); // 与workDays结果相同

// 复制另一个EnumSet
EnumSet<Day> weekend = EnumSet.copyOf(EnumSet.of(Day.SATURDAY, Day.SUNDAY));
System.out.println(weekend); // [SATURDAY, SUNDAY]

// 检查元素是否存在
System.out.println(workDays.contains(Day.SUNDAY)); // false
}
}

EnumMapMap接口的实现类,专门用于以枚举类型为键(Key)的映射。它同样利用枚举的特性,提供了比HashMap更高的性能和更简洁的实现。

EnumMap 的特性

  • 键类型限制:键必须是同一枚举类型的实例(编译时检查)。
  • 有序性:键的迭代顺序与枚举常量在枚举类中的定义顺序一致。
  • 高性能:底层通过数组实现(数组索引对应枚举常量的顺序),查找、插入、删除的时间复杂度为O(1)
  • 非同步:线程不安全,如需同步可使用Collections.synchronizedMap()包装。

EnumMap的构造方法需要指定枚举类型的Class对象:

  • EnumMap(Class<K> keyType):创建一个空的EnumMap,指定键的枚举类型。
  • EnumMap(EnumMap<K, ? extends V> m):复制另一个EnumMap
  • EnumMap(Map<K, ? extends V> m):复制普通Map(要求键为同一枚举类型)。

其他方法与普通Map类似(put()get()keySet()values()等)。

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
30
31
32
33
34
35
36
37
import java.util.EnumMap;
import java.util.Map;

// 定义枚举类型(键)
enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}

public class EnumMapDemo {
public static void main(String[] args) {
// 创建EnumMap,指定键的枚举类型
EnumMap<Season, String> seasonDesc = new EnumMap<>(Season.class);

// 添加键值对
seasonDesc.put(Season.SPRING, "春暖花开");
seasonDesc.put(Season.SUMMER, "夏日炎炎");
seasonDesc.put(Season.AUTUMN, "秋高气爽");
seasonDesc.put(Season.WINTER, "冬雪皑皑");

// 获取值
System.out.println(seasonDesc.get(Season.SUMMER)); // 夏日炎炎

// 遍历键值对(顺序与枚举定义一致)
for (Map.Entry<Season, String> entry : seasonDesc.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 输出:
// SPRING: 春暖花开
// SUMMER: 夏日炎炎
// AUTUMN: 秋高气爽
// WINTER: 冬雪皑皑

// 获取所有键/值
System.out.println(seasonDesc.keySet()); // [SPRING, SUMMER, AUTUMN, WINTER]
System.out.println(seasonDesc.values()); // [春暖花开, 夏日炎炎, 秋高气爽, 冬雪皑皑]
}
}

EnumSetEnumMap是 Java 为枚举类型量身定制的容器,充分利用了枚举的 “元素数量固定、可枚举” 特性,性能远高于普通集合。

EnumSet适合存储枚举元素的集合,支持高效的集合操作;EnumMap适合以枚举为键的映射,支持按枚举顺序遍历。

在涉及枚举类型的场景中,优先使用EnumSetEnumMap,而非HashSet/HashMap,可提升性能并增强代码可读性。

枚举的更多用法和深入理解

关于覆盖enum类方法

在 Java 中,enum(枚举)是一种特殊的类,它继承自 java.lang.Enum 类。与普通类一样,枚举类也可以覆盖父类的方法(主要是 Enum 类中的方法),或者自定义方法并进行覆盖(如果有枚举子类的话,不过枚举类默认是 final 的,一般不能被继承,所以更多是覆盖自身或父类的方法)。

枚举类最常覆盖的方法来自 Enum 类,主要包括:

  1. toString() 方法 Enum 类的 toString() 默认返回枚举常量的名称(即声明时的标识符)。覆盖该方法可以返回更具可读性的描述。
  2. equals()hashCode() 方法 Enum 类已经实现了 equals()(基于引用相等,因为枚举常量是单例)和 hashCode(),通常无需覆盖。但如果有特殊需求(比如自定义相等逻辑),可以覆盖,但需遵循两者的一致性(相等的对象必须有相同的哈希码)。
  3. clone() 方法 Enum 类的 clone() 被声明为 protected 且会抛出 CloneNotSupportedException,目的是防止枚举常量被克隆(保证单例性),一般不建议覆盖。
  4. compareTo() 方法 Enum 类的 compareTo() 基于枚举常量的声明顺序(自然顺序),如果需要自定义排序逻辑,可以覆盖。

toString() 是最常被覆盖的方法,因为默认的枚举名称可能不够直观,覆盖后可以返回更友好的描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum Season {
SPRING, SUMMER, AUTUMN, WINTER;

// 覆盖 toString() 方法,返回中文描述
@Override
public String toString() {
switch (this) {
case SPRING: return "春天";
case SUMMER: return "夏天";
case AUTUMN: return "秋天";
case WINTER: return "冬天";
default: return super.toString(); // 兜底
}
}
}

public class EnumDemo {
public static void main(String[] args) {
System.out.println(Season.SPRING); // 输出:春天(而非默认的 SPRING)
System.out.println(Season.SUMMER); // 输出:夏天
}
}

覆盖 equals()hashCode() 方法需要谨慎使用

Enum 类的 equals() 实现为

1
2
3
public final boolean equals(Object other) {
return this == other; // 基于引用相等(枚举常量是单例,每个常量仅一个实例)
}

hashCode() 实现为

1
2
3
public final int hashCode() {
return super.hashCode(); // 即 Object 的 hashCode(),基于对象地址
}

由于枚举常量是单例的,==equals() 效果一致,通常无需覆盖。但如果有特殊需求(比如根据枚举的属性判断相等),可以覆盖,但需注意:

  • equals() 必须满足自反性、对称性、传递性。
  • hashCode() 必须与 equals() 保持一致(相等的对象必须有相同的哈希码)。

注意,枚举类默认继承 Enum,且 Java 不支持多继承,因此枚举类不能继承其他类,但可以实现接口。枚举类的构造方法必须是 private(默认也是 private),不能是 publicprotected,防止外部创建实例(保证单例性)。

enum 类中定义抽象方法

与常规抽象类一样,enum类允许我们为其定义抽象方法,然后使每个枚举实例都实现该方法,允许每个枚举常量提供自己的方法实现,它使枚举类能够根据不同的常量表现出不同的行为。

注意abstract关键字对于枚举类来说并不是必须的

枚举类可以声明抽象方法,但必须由每个枚举常量立即实现这些抽象方法。这类似于匿名内部类的语法。

每个枚举常量实际上是枚举类的一个实例,当枚举类包含抽象方法时,每个常量都必须实现该抽象方法,否则会导致编译错误。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
enum Operation {
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;
}
};

// 声明抽象方法
public abstract double calculate(double a, double b);
}

public class EnumDemo {
public static void main(String[] args) {
double a = 10, b = 5;

// 遍历所有枚举常量并调用其实现的抽象方法
for (Operation op : Operation.values()) {
System.out.printf("%s: %.2f %s %.2f = %.2f%n",
op.name(), a, op.getSymbol(), b, op.calculate(a, b));
}
}
}

在枚举类中声明抽象方法(如 public abstract double calculate(double a, double b)),和其他的抽象方法一样无需提供方法体。但是每个枚举常量(如 ADD, SUBTRACT)必须实现该抽象方法。实现方式类似于匿名内部类,使用大括号 {} 包裹方法实现。如果某个枚举常量未实现抽象方法,编译器会报错。

通过这种方式就可以轻而易举地定义每个枚举实例的不同行为方式。我们可能注意到,enum类的实例似乎表现出了多态的特性,可惜的是枚举类型的实例终究不能作为类型传递使用,就像下面的使用方式,编译器是不可能答应的