关于枚举类型
什么是枚举类型
枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型
一般在实现枚举一些多次会被使用的常量时候,可以使用定义常量的方式,也就是 fianl int 枚举模式,这样的定义方式并没有什么错,但它存在许多不足,如在类型安全和使用方便性上并没有多少好处,如果存在定义int值相同的变量,混淆的几率还是很大的,编译器也不会提出任何警告
例如星期中每天的枚举
1 | // 不使用枚举类 |
在定义枚举类型时我们使用的关键字是enum
,与class
关键字类似,只不过前者是定义枚举类型,后者是定义类类型。然后然后定义枚举的名称、可访问性、基础类型和成员等。枚举声明的语法如下:
1 | enum-modifiers enum enumname:enum-base { |
其中,enum-modifiers 表示枚举的修饰符主要包括 public、private 和 internal;enumname 表示声明的枚举名称;enum-base 表示基础类型;enum-body 表示枚举的成员,它是枚举类型的命名常数。
要注意,任意两个枚举成员不能具有相同的名称,且它的常数值必须在该枚举的基础类型的范围之内,多个枚举成员之间使用逗号分隔。
其中,如果没有显式地声明基础类型的枚举,那么意味着它所对应的基础类型是 int。
而且枚举类型可以像类(class)类型一样,定义为一个单独的文件,当然也可以定义在其他类内部,但要保证枚举表示的类型其取值是必须有限的,也就是说每个值都是可以枚举出来的,有限且可枚举。
使用也很简单,直接引用就行这便是枚举类型的最简单模型。
1 | public static void main(String[] args){ |
枚举的实现原理
实际上在使用关键字enum
创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API
中的java.lang.Enum
类,也就是说通过关键字enum
创建枚举类型在编译后事实上也是一个类类型,而且该类继承自java.lang.Enum
类
来看看反编译的一个枚举类文件
1 | //反编译Day.class |
使用关键字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 |
返回带指定名称的指定枚举类型的枚举常量。 |
其他剩下的,别的抽象类和包装类都有的equals
,toString
,反射用的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 | enum Season { |
方法 | 调用方式 | 适用场景 | 异常处理 |
---|---|---|---|
EnumType.valueOf(name) |
直接通过枚举类调用 | 已知具体枚举类型时使用 | 编译时检查类型 |
Enum.valueOf(Class, name) |
通过 Enum 类反射调用 |
运行时动态确定枚举类型 | 需手动处理 ClassNotFoundException |
Enum类内部会有一个构造函数,该构造函数只能有编译器调用,我们是无法手动操作的,不妨看看Enum类的主要源码
1 | //实现了Comparable |
枚举类与Class对象
上述我们提到当枚举实例向上转型为Enum类型后,values()方法将会失效,也就无法一次性获取所有枚举实例变量,因为,values()
是
编译器自动生成
的静态方法,而非java.lang.Enum
类的原生方法,Enum
类本身只定义了所有枚举共有的方法(如name()
、ordinal()
),并未包含values()
。values()
是枚举类的
专属静态方法,而非继承自Enum
。
当你将枚举实例向上转型为Enum
时:
1 | Enum<?> season = Season.SPRING; // 向上转型为 Enum |
- 静态绑定:Java 的方法调用在编译期会根据变量的
声明类型(而非实际类型)进行绑定。由于
season
的声明类型是Enum
,而Enum
类没有values()
方法,因此编译器直接报错。 - 类型信息丢失:向上转型后,变量
season
的具体枚举类型(如Season
)被擦除,编译器只知道它是一个Enum
,无法访问子类特有的方法。
虽然向上转型后无法直接调用values()
,但是由于Class对象的存在,即使不使用values()方法,还是有可能一次获取到所有枚举实例变量的,在Class对象中存在如下方法:
返回类型 | 方法名称 | 方法说明 |
---|---|---|
T[] |
getEnumConstants() |
返回该枚举类型的所有元素,如果Class对象不是枚举类型,则返回null。 |
boolean |
isEnum() |
当且仅当该类声明为源代码中的枚举时返回 true |
1 | Enum<?> season = Season.SPRING; |
实际上这里使用EnumSet.allOf()
是更常见的做法
1 | Enum<?> season = Season.SPRING; |
EnumSet等枚举类的容器
在 Java 中,针对枚举类型(enum
),Java
集合框架提供了两个特殊的容器类:EnumSet
和EnumMap
。它们专门为枚举类型设计,具有高效的性能和简洁的
API。
EnumSet
是Set
接口的实现类,专门用于存储枚举类型的元素。它的设计充分利用了枚举类型的特性(元素数量固定、可枚举),因此性能远高于普通的HashSet
或TreeSet
。
更多内容关于这些容器的原理涉及到的内容太多了,打算单开一帖详细说明。
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)
:创建包含从from
到to
(含)的所有枚举元素的EnumSet
。EnumSet.copyOf(Collection<E> c)
:复制一个集合中的枚举元素到EnumSet
(要求集合元素为同一枚举类型)。
1 | import java.util.EnumSet; |
而EnumMap
是Map
接口的实现类,专门用于以枚举类型为键(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 | import java.util.EnumMap; |
EnumSet
和EnumMap
是 Java
为枚举类型量身定制的容器,充分利用了枚举的 “元素数量固定、可枚举”
特性,性能远高于普通集合。
EnumSet
适合存储枚举元素的集合,支持高效的集合操作;EnumMap
适合以枚举为键的映射,支持按枚举顺序遍历。
在涉及枚举类型的场景中,优先使用EnumSet
和EnumMap
,而非HashSet
/HashMap
,可提升性能并增强代码可读性。
枚举的更多用法和深入理解
关于覆盖enum类方法
在 Java 中,enum
(枚举)是一种特殊的类,它继承自
java.lang.Enum
类。与普通类一样,枚举类也可以覆盖父类的方法(主要是 Enum
类中的方法),或者自定义方法并进行覆盖(如果有枚举子类的话,不过枚举类默认是
final 的,一般不能被继承,所以更多是覆盖自身或父类的方法)。
枚举类最常覆盖的方法来自 Enum
类,主要包括:
toString()
方法Enum
类的toString()
默认返回枚举常量的名称(即声明时的标识符)。覆盖该方法可以返回更具可读性的描述。equals()
和hashCode()
方法Enum
类已经实现了equals()
(基于引用相等,因为枚举常量是单例)和hashCode()
,通常无需覆盖。但如果有特殊需求(比如自定义相等逻辑),可以覆盖,但需遵循两者的一致性(相等的对象必须有相同的哈希码)。clone()
方法Enum
类的clone()
被声明为protected
且会抛出CloneNotSupportedException
,目的是防止枚举常量被克隆(保证单例性),一般不建议覆盖。compareTo()
方法Enum
类的compareTo()
基于枚举常量的声明顺序(自然顺序),如果需要自定义排序逻辑,可以覆盖。
toString()
是最常被覆盖的方法,因为默认的枚举名称可能不够直观,覆盖后可以返回更友好的描述。
1 | enum Season { |
覆盖 equals()
和 hashCode()
方法需要谨慎使用
Enum
类的 equals()
实现为
1 | public final boolean equals(Object other) { |
hashCode()
实现为
1 | public final int hashCode() { |
由于枚举常量是单例的,==
和 equals()
效果一致,通常无需覆盖。但如果有特殊需求(比如根据枚举的属性判断相等),可以覆盖,但需注意:
equals()
必须满足自反性、对称性、传递性。hashCode()
必须与equals()
保持一致(相等的对象必须有相同的哈希码)。
注意,枚举类默认继承 Enum
,且 Java
不支持多继承,因此枚举类不能继承其他类,但可以实现接口。枚举类的构造方法必须是
private
(默认也是 private),不能是 public
或
protected
,防止外部创建实例(保证单例性)。
enum 类中定义抽象方法
与常规抽象类一样,enum类允许我们为其定义抽象方法,然后使每个枚举实例都实现该方法,允许每个枚举常量提供自己的方法实现,它使枚举类能够根据不同的常量表现出不同的行为。
注意
abstract
关键字对于枚举类来说并不是必须的
枚举类可以声明抽象方法,但必须由每个枚举常量立即实现这些抽象方法。这类似于匿名内部类的语法。
每个枚举常量实际上是枚举类的一个实例,当枚举类包含抽象方法时,每个常量都必须实现该抽象方法,否则会导致编译错误。
1 | enum Operation { |
在枚举类中声明抽象方法(如
public abstract double calculate(double a, double b)
),和其他的抽象方法一样无需提供方法体。但是每个枚举常量(如
ADD
,
SUBTRACT
)必须实现该抽象方法。实现方式类似于匿名内部类,使用大括号
{}
包裹方法实现。如果某个枚举常量未实现抽象方法,编译器会报错。
通过这种方式就可以轻而易举地定义每个枚举实例的不同行为方式。我们可能注意到,enum类的实例似乎表现出了多态的特性,可惜的是枚举类型的实例终究不能作为类型传递使用,就像下面的使用方式,编译器是不可能答应的