在
Java 5
中新增了枚举类型,它是一种特殊的数据类型,之所以特殊是因为它是一种class
类型却又比class
类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。🚀本篇章代码 Demo
# 定义枚举
下面这是在没有枚举类型的情况下定义常量常见的方法如下:
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: 普通方式定义常量 | |
* @DateTime: 2023-12-05 21:14 | |
* @Version:1.0 | |
**/ | |
public class OrdinaryConstantDemo { | |
/** | |
* 星期一 | |
*/ | |
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; | |
} |
在上述中的常量定义的方式称为
int
枚举模式,这种定义方法并没有什么错,但它存在许多不足的地方,如:类型安全和使用方便性上并没有多少好处,如果存在定义int
值相同的变量,混淆的几率还是很大的,编译器并不会做出任何警告提示,因此这种方式在枚举出现后并不提倡,现在我们利用枚举类型来重新定义上述常量,定义方式如下:
package top.rem.rain; | |
/** | |
* 枚举定义,枚举类型使用 enum 关键字 | |
* @author LightRain | |
*/ | |
public enum DayType { | |
/** | |
* 星期一 | |
*/ | |
MONDAY, | |
/** | |
* 星期二 | |
*/ | |
TUESDAY, | |
/** | |
* 星期三 | |
*/ | |
WEDNESDAY, | |
/** | |
* 星期四 | |
*/ | |
THURSDAY, | |
/** | |
* 星期五 | |
*/ | |
FRIDAY, | |
/** | |
* 星期六 | |
*/ | |
SATURDAY, | |
/** | |
* 星期天 | |
*/ | |
SUNDAY | |
} |
在定义枚举类型时我们使用的关键字是
enum
与class
关键字类似,枚举类型DayType
中分别定义了从周一到周日的值,注意:值
一般是大写字母,多个值之间使用逗号分隔,同时我们应该知道的是枚举类型可以像class
类型一样,定义为一个独立的文件,当然也可以定义在其它类内部,更重要的是枚举常量在类型安全性和便捷性都有保证,如果类型出现问题编译器会提示警告信息,请务必记住枚举类型表示的类型其取值是有限的,也就是说每个值都是可以枚举出来的,如何使用枚举描述上述的一周呢,代码如下:
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: DayType 测试 | |
* @DateTime: 2023-12-05 21:54 | |
* @Version:1.0 | |
**/ | |
public class DayTypeTest { | |
public static void main(String[] args) { | |
// 引用内部枚举,在 cmd 中使用 javac 编译成 class 文件时不可引用外部枚举会显示错误:找不到符合错误 | |
// InternalDayType monday = InternalDayType.MONDAY; | |
// 创建枚举数组 | |
InternalDayType[] days = new InternalDayType[]{InternalDayType.MONDAY, InternalDayType.TUESDAY, InternalDayType.WEDNESDAY, InternalDayType.THURSDAY, InternalDayType.FRIDAY, InternalDayType.SATURDAY, InternalDayType.SUNDAY}; | |
for (int i = 0; i < days.length; i++) { | |
System.out.println("day[" + i + "].ordinal():" + days[i].ordinal()); | |
} | |
System.out.println("-------------------------------------"); | |
// 通过 compareTo 方法比较,实际上其内部是通过 ordinal () 值比较的 | |
System.out.println("days[0].compareTo(days[1]):" + days[0].compareTo(days[1])); | |
System.out.println("days[0].compareTo(days[1]):" + days[0].compareTo(days[2])); | |
// 获取该枚举对象的 Class 对象引用,当然也可以通过 getClass 方法 | |
Class<?> clazz = days[0].getDeclaringClass(); | |
System.out.println("clazz:" + clazz); | |
System.out.println("-------------------------------------"); | |
//name() | |
System.out.println("days[0].name():" + days[0].name()); | |
System.out.println("days[1].name():" + days[1].name()); | |
System.out.println("days[2].name():" + days[2].name()); | |
System.out.println("days[3].name():" + days[3].name()); | |
System.out.println("-------------------------------------"); | |
System.out.println("days[0].toString():" + days[0].toString()); | |
System.out.println("days[1].toString():" + days[1].toString()); | |
System.out.println("days[2].toString():" + days[2].toString()); | |
System.out.println("days[3].toString():" + days[3].toString()); | |
System.out.println("-------------------------------------"); | |
InternalDayType d = Enum.valueOf(InternalDayType.class, days[0].name()); | |
InternalDayType d2 = InternalDayType.valueOf(InternalDayType.class, days[0].name()); | |
System.out.println("d:" + d); | |
System.out.println("d2:" + d2); | |
/* | |
执行结果: | |
day [0].ordinal ():0 | |
day [1].ordinal ():1 | |
day [2].ordinal ():2 | |
day [3].ordinal ():3 | |
day [4].ordinal ():4 | |
day [5].ordinal ():5 | |
day [6].ordinal ():6 | |
------------------------------------- | |
days [0].compareTo (days [1]):-1 | |
days [0].compareTo (days [1]):-2 | |
clazz:class top.rem.rain.InternalDayType | |
------------------------------------- | |
days [0].name ():MONDAY | |
days [1].name ():TUESDAY | |
days [2].name ():WEDNESDAY | |
days [3].name ():THURSDAY | |
------------------------------------- | |
days [0].toString ():MONDAY | |
days [1].toString ():TUESDAY | |
days [2].toString ():WEDNESDAY | |
days [3].toString ():THURSDAY | |
------------------------------------- | |
d:MONDAY | |
d2:MONDAY | |
*/ | |
} | |
/** | |
* 此处使用内部方式定义并使用 javac 来变成成 class 文件 | |
* 使用外部类会显示找不到文件 | |
*/ | |
private enum InternalDayType { | |
/** | |
* 星期一 | |
*/ | |
MONDAY, | |
/** | |
* 星期二 | |
*/ | |
TUESDAY, | |
/** | |
* 星期三 | |
*/ | |
WEDNESDAY, | |
/** | |
* 星期四 | |
*/ | |
THURSDAY, | |
/** | |
* 星期五 | |
*/ | |
FRIDAY, | |
/** | |
* 星期六 | |
*/ | |
SATURDAY, | |
/** | |
* 星期天 | |
*/ | |
SUNDAY | |
} | |
} |
直接引用枚举的值即可,这便是枚举类型的最简单的模型。
# 枚举实现原理
- 我们了解了枚举类型的定义与简单使用后,现在有必要来了解一下枚举类型的基本实现原理。
- 在使用关键字
enum
创建枚举类型并编译后,编译器会为我们生成一个相关的类。 - 这个类继承了
Java API
中的java.lang.Enum
类,通过关键字enum
创建枚举类型在编译后事实上就是一个类类型。
javac DayTypeTest.java -encoding UTF-8 |
- IntelliJ IDEA 无法反编译出以下效果。
- 工具使用
cfr-0.152.jar
,下载地址 https://www.benf.org/other/cfr/。 cmd
窗口中使用的命令:命令行提示符 java -jar cfr-0.152.jar --sugarenums false -outputpath 请将此处替换为反编译后的储存路径 InternalDayType.class
--sugarenums false
代表将使用CFR 0_6
反编译,详细介绍请看此处 Java 1.5 枚举是如何实现的?。-outputpath
代表要保存反编译后的文件储存路径。
/* | |
* IntelliJ IDEA 无法反编译出此效果 | |
* 工具使用 cfr-0.152.jar | |
* 工具下载地址 https://www.benf.org/other/cfr/ | |
* cmd 窗口中使用的命令 java -jar cfr-0.152.jar --sugarenums false -outputpath D:\ InternalDayType.class | |
* 反编译出如下效果请添加 --sugarenums false 语法 | |
* -outputpath 代表要保存反编译后的文件储存路径 | |
* Decompiled with CFR 0.152. | |
*/ | |
package top.rem.rain; | |
// 反编译 InternalDayType.class | |
final class InternalDayType extends Enum<InternalDayType> { | |
// 前面咱们自己定义的 7 种枚举实例,并实例化枚举 | |
public static final /* enum */ InternalDayType MONDAY = new InternalDayType("MONDAY", 0); | |
public static final /* enum */ InternalDayType TUESDAY = new InternalDayType("TUESDAY", 1); | |
public static final /* enum */ InternalDayType WEDNESDAY = new InternalDayType("WEDNESDAY", 2); | |
public static final /* enum */ InternalDayType THURSDAY = new InternalDayType("THURSDAY", 3); | |
public static final /* enum */ InternalDayType FRIDAY = new InternalDayType("FRIDAY", 4); | |
public static final /* enum */ InternalDayType SATURDAY = new InternalDayType("SATURDAY", 5); | |
public static final /* enum */ InternalDayType SUNDAY = new InternalDayType("SUNDAY", 6); | |
private static final /* synthetic */ InternalDayType[] $VALUES; | |
// 编译器为我们添加的静态 values () 方法 | |
public static InternalDayType[] values() { | |
return (InternalDayType[])$VALUES.clone(); | |
} | |
// 编译器为我们添加的静态 valuesOf () 方法 | |
// 注意返回时调用了 Enum 类的 valueOf () 方法 | |
public static InternalDayType valueOf(String string) { | |
return Enum.valueOf(InternalDayType.class, string); | |
} | |
// 私有构造函数 | |
private InternalDayType(String string, int n) { | |
super(string, n); | |
} | |
// 将实例化后的枚举添加到了静态块中,也就是静态常量池中 | |
static { | |
$VALUES = new InternalDayType[]{MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY}; | |
} | |
} |
- 从上面反编译的代码可以看出编译器确实帮助我们生成了一个
InternalDayType
类 (注意:该类型是final
类型的,将无法被继承) 而且该类继承自java.lang.Enum
类,该类是一个抽象类。 - 编译器还帮助我们生成了
7
个InternalDayType
类型的实例对象分别对应枚举中定义的7
个日期,也就是使用关键字enum
定义的InternalDayType
类型中的每种日期枚举常量也是实实在在的InternalDayType
实例对象,只不过代表的内容不一样。 - 注意:编译器还为我们生成了两个静态方法,分别是
values()
和valuesOf()
,稍后分析它们的用法,使用关键字enum
定义的枚举类型,在编译后也将转换为一个实实在在的类,而在该类中会存在每个在枚举类型中定义好变量的对应实例对象。 - 如上述
MONDAY
枚举类型对应public static final InternalDayType MONDAY = new InternalDayType("MONDAY", 0);
,同时编译器会为该类创建两个方法,分别是values()
和valueOf()
。
下面我们深入来探究一下
java.lang.Enum
类以及values()
和valueOf()
方法的用途。
# 枚举常见方法
# Enum 抽象类常见方法
Enum
是所有Java
语音枚举类型的公共基本类 (注意:Enum
是抽象类),它的常见方法如下:
返回类型 | 方法名称 | 方法说明 |
---|---|---|
int | compareTo(E o) | 比较此枚举与指定对象的顺序 |
boolean | equals(Object other) | 当指定对象等于此枚举常量是,返回 true |
Class<?> | getDeclaringClass() | 返回与此枚举常量的枚举类型相对应的 Class 对象 |
String | name() | 返回此枚举常量的名称,在其枚举声明中对其进行声明 |
int | ordinal() | 返回枚举常量的序数 (它在枚举声明中的位置,其中初始常量序数为零) |
String | toString() | 返回枚举常量的名称,它包含在声明中 |
static<T extends Enum<T>> T | static valueOf(Class<T> enumType,String name) | 返回带指定名称的指定枚举类型的枚举常量 |
- 主要说明一下
ordinal()
方法,该方法获取的是枚举变量在枚举类中声明的顺序,下标从0
开始。- 如:日期中的
MONDAY
在第一个位置,那么MONDAY
的ordinal
值就是0
。 - 如果
MONDAY
的声明位置发生变化,那么使用ordinal()
方法获取到的值也随之变化,注意在大多数情况下我们都不应该首选使用该方法。
- 如:日期中的
compareTo(E o)
方法则是比较枚举的大小,注意:其内部实现是根据每个枚举的ordinal
值大小来进行比较的。name()
方法与toString()
几乎是等同的,都是输出变量的字符串形式。valueOf(Class<T> enumType,String name)
方法是根据枚举类的Class
对象和枚举名称获取枚举常量,该方法是静态的。
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: DayType 测试 | |
* @DateTime: 2023-12-05 21:54 | |
* @Version:1.0 | |
**/ | |
public class DayTypeTest { | |
public static void main(String[] args) { | |
// 创建枚举数组 | |
InternalDayType[] days=new InternalDayType[]{InternalDayType.MONDAY, InternalDayType.TUESDAY, InternalDayType.WEDNESDAY, InternalDayType.THURSDAY, InternalDayType.FRIDAY, InternalDayType.SATURDAY, InternalDayType.SUNDAY}; | |
for (int i = 0; i <days.length ; i++) { | |
System.out.println("day["+i+"].ordinal():"+days[i].ordinal()); | |
} | |
System.out.println("-------------------------------------"); | |
// 通过 compareTo 方法比较,实际上其内部是通过 ordinal () 值比较的 | |
System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[1])); | |
System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[2])); | |
// 获取该枚举对象的 Class 对象引用,当然也可以通过 getClass 方法 | |
Class<?> clazz = days[0].getDeclaringClass(); | |
System.out.println("clazz:"+clazz); | |
System.out.println("-------------------------------------"); | |
//name() | |
System.out.println("days[0].name():"+days[0].name()); | |
System.out.println("days[1].name():"+days[1].name()); | |
System.out.println("days[2].name():"+days[2].name()); | |
System.out.println("days[3].name():"+days[3].name()); | |
System.out.println("-------------------------------------"); | |
System.out.println("days[0].toString():"+days[0].toString()); | |
System.out.println("days[1].toString():"+days[1].toString()); | |
System.out.println("days[2].toString():"+days[2].toString()); | |
System.out.println("days[3].toString():"+days[3].toString()); | |
System.out.println("-------------------------------------"); | |
InternalDayType d= Enum.valueOf(InternalDayType.class,days[0].name()); | |
InternalDayType d2=InternalDayType.valueOf(InternalDayType.class,days[0].name()); | |
System.out.println("d:"+d); | |
System.out.println("d2:"+d2); | |
/* | |
执行结果: | |
day [0].ordinal ():0 | |
day [1].ordinal ():1 | |
day [2].ordinal ():2 | |
day [3].ordinal ():3 | |
day [4].ordinal ():4 | |
day [5].ordinal ():5 | |
day [6].ordinal ():6 | |
------------------------------------- | |
days [0].compareTo (days [1]):-1 | |
days [0].compareTo (days [1]):-2 | |
clazz:class top.rem.rain.InternalDayType | |
------------------------------------- | |
days [0].name ():MONDAY | |
days [1].name ():TUESDAY | |
days [2].name ():WEDNESDAY | |
days [3].name ():THURSDAY | |
------------------------------------- | |
days [0].toString ():MONDAY | |
days [1].toString ():TUESDAY | |
days [2].toString ():WEDNESDAY | |
days [3].toString ():THURSDAY | |
------------------------------------- | |
d:MONDAY | |
d2:MONDAY | |
*/ | |
} | |
} | |
/** | |
* 此处使用内部方式定义并使用 javac 来变成成 class 文件 | |
* 使用外部类会显示找不到文件 | |
*/ | |
enum InternalDayType { | |
/** | |
* 星期一 | |
*/ | |
MONDAY, | |
/** | |
* 星期二 | |
*/ | |
TUESDAY, | |
/** | |
* 星期三 | |
*/ | |
WEDNESDAY, | |
/** | |
* 星期四 | |
*/ | |
THURSDAY, | |
/** | |
* 星期五 | |
*/ | |
FRIDAY, | |
/** | |
* 星期六 | |
*/ | |
SATURDAY, | |
/** | |
* 星期天 | |
*/ | |
SUNDAY | |
} |
抽象类
Enum
类的基本内容到此就介绍完了,这里提醒一点就是,Enum
类内部会有一个构造函数,该类构造只能有编译器调用,无法手动操作,来看Enum
类的主要源码。
package java.lang; | |
// 实现了 Comparable | |
public abstract class Enum<E extends Enum<E>> | |
implements Constable, Comparable<E>, Serializable { | |
/** | |
* 枚举字符串名称 | |
*/ | |
private final String name; | |
public final String name() { | |
return name; | |
} | |
/** | |
* 枚举顺序值 | |
*/ | |
private final int ordinal; | |
public final int ordinal() { | |
return ordinal; | |
} | |
/** | |
* 枚举的构造方法,只能由编译器调用 | |
* @param name 枚举字符串名称 | |
* @param 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; | |
} | |
public final int hashCode() { | |
return super.hashCode(); | |
} | |
protected final Object clone() throws CloneNotSupportedException { | |
throw new CloneNotSupportedException(); | |
} | |
/** | |
* 比较的是 ordinal 的值 | |
* @param o 任意枚举类型 | |
* @return int | |
*/ | |
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(); | |
// 根据 ordinal 值比较大小 | |
return self.ordinal - other.ordinal; | |
} | |
/** | |
* 获取声明类 | |
* @return Class<E> | |
*/ | |
@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; | |
} | |
/** | |
* 返回带指定名称的指定枚举类型的枚举常量 | |
* @param enumClass | |
* @param name | |
* @return | |
* @param <T> | |
*/ | |
public static <T extends Enum<T>> T valueOf(Class<T> enumClass, | |
String name) { | |
//enumClass.enumConstantDirectory ():获取到的是一个 map 集合,key 值就是 name 值,value 则是枚举变量值 | |
//enumConstantDirectory 是 class 对象内部的方法,根据 class 对象获取一个 map 集合的值 | |
T result = enumClass.enumConstantDirectory().get(name); | |
if (result != null) | |
return result; | |
if (name == null) | |
throw new NullPointerException("Name is null"); | |
throw new IllegalArgumentException( | |
"No enum constant " + enumClass.getCanonicalName() + "." + name); | |
} | |
} |
通过
Enum
源码可以知道,Enum
实现了Comparable
接口,这也是可以使用compareTo
比较的原因,当然Enum
构造函数也是存在的,该构造函数只能由编译器调用,毕竟我们只能使用enum
关键字来定义枚举,其它就交给编译器了。
package java.lang; | |
// 实现了 Comparable | |
public abstract class Enum<E extends Enum<E>> | |
implements Constable, Comparable<E>, Serializable { | |
/** | |
* 枚举字符串名称 | |
*/ | |
private final String name; | |
/** | |
* 枚举顺序值 | |
*/ | |
private final int ordinal; | |
/** | |
* 枚举的构造方法,只能由编译器调用 | |
* @param name 枚举字符串名称 | |
* @param ordinal 枚举顺序值 | |
*/ | |
protected Enum(String name, int ordinal) { | |
this.name = name; | |
this.ordinal = ordinal; | |
} | |
} |
# Values ()&ValueOf () 方法
values()
和valueOf(String name)
方法是编译器生成的static
方法。- 从前面分析中,在
Enum
类中并没有出现values()
方法,但valueOf()
方法还是有出现的。 - 只不过编译器生成的
valueOf()
方法需要传递一个name
参数,而Enum
自带的静态方法valueOf()
则需要传递两个参数。 - 从前面反编译后的代码可以看到,编译器生成的
valueOf()
方法最终还是调用了Enum
类的valueOf()
方法。
package top.rem.rain; | |
import java.util.Arrays; | |
/** | |
* @Author: LightRain | |
* @Description: | |
* @DateTime: 2023-12-06 23:45 | |
* @Version:1.0 | |
**/ | |
public class EnumDemo { | |
public static void main(String[] args) { | |
DayType[] days2 = DayType.values(); | |
System.out.println("day2:" + Arrays.toString(days2)); | |
DayType day = DayType.valueOf("MONDAY"); | |
System.out.println("day:" + day); | |
/* | |
执行结果: | |
day2:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY] | |
day:MONDAY | |
*/ | |
} | |
} |
从结果上看就可以知道,
values()
方法的作用就是获取枚举类中的所有变量,作为数组返回,而valueOf(String name)
方法与Enum
类中的valueOf(String name)
方法的作用类似,根据名称获取枚举变量,只不过编译器生成的valueOf(String name)
方法更简洁一些只需要传递一个参数,注意:values()
方法是由编译器插入到枚举类中的static
方法,所以如果我们将枚举实例向上转型为Enum
,那么values()
方法将无法被调用,因为Enum
类中并没有values()
方法,valueOf(String name)
方法也是同样的道理。
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: | |
* @DateTime: 2023-12-07 20:41 | |
* @Version:1.0 | |
**/ | |
public class EnumDemo2 { | |
public static void main(String[] args) { | |
// 正常使用 | |
DayType[] ds = DayType.values(); | |
// 向上转型 Enum | |
Enum<DayType> e = DayType.MONDAY; | |
// 无法调用,没有此方法 | |
//e.values(); | |
} | |
} |
# Class 对象 & 枚举
在上述我们提到当枚举实例向上转型为
Enum
类型后,values()
方法将会失效,也就无法一次性获取所有枚举实例变量,但是由于Class
对象的存在,即使不使用values()
方法,还是可以一次获取到所有枚举实例变量的,Class
对象中存在如下方法:
返回类型 | 方法名称 | 方法说明 |
---|---|---|
T[] | getEnumConstants() | 返回该类枚举类型的所有元素,如果 Class 对象不是枚举类型则返回 null |
boolean | isEnum | 当且仅当该类声明为源代码中的枚举是返回 true |
因此通过
getEnumConstants()
方法,同样可以轻而易举地获取所有枚举实例变量,下面通过代码来演示此功能,代码如下:
package top.rem.rain; | |
import java.util.Arrays; | |
/** | |
* @Author: LightRain | |
* @Description: | |
* @DateTime: 2023-12-07 20:45 | |
* @Version:1.0 | |
**/ | |
public class EnumDemo3 { | |
public static void main(String[] args) { | |
// 向上转型 Enum | |
Enum<DayType> e = DayType.MONDAY; | |
// 获取 class 对象引用 | |
Class<?> clasz = e.getDeclaringClass(); | |
if (clasz.isEnum()) { | |
DayType[] dsz = (DayType[]) clasz.getEnumConstants(); | |
System.out.println("DayType:" + Arrays.toString(dsz)); | |
} | |
/* | |
输出结果: | |
DayType:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY] | |
*/ | |
} | |
} |
我们通过
Enum
的class
对象的getEnumConstants()
方法仍能一次性获取所有的枚举实例常量。
# 枚举进阶用法
在前面的分析中,我们都是基于简单枚举类型的定义,也就是在定义枚举时只定义了枚举实例类型,并没有定义方法或者成员变量,实际上使用
enum
关键字定义的枚举类,除了不能使用继承 (因为编译器会自动为我们继承Enum
抽象类,而Java
只支持单继承,因此枚举类是无法手动实现继承的),可以把enum
类当成常规类,就是可以说我们向enum
类中添加方法和变量,甚至是mian
方法。
# 枚举类添加方法与构造函数
重新定义一个日期枚举类,带有成员变量,描述该日期枚举类的作用,同时定义一个
getDescriptionChinese()
方法来返回描述内容,并将构造函数私有化,防止外部调用,在声明枚举实例时传入对应的描述,代码如下:
package top.rem.rain; | |
/** | |
* 枚举进阶用法 | |
* | |
* @author LightRain | |
*/ | |
public enum DayType2 { | |
/** | |
* 星期一 | |
*/ | |
MONDAY("星期一"), | |
/** | |
* 星期二 | |
*/ | |
TUESDAY("星期二"), | |
/** | |
* 星期三 | |
*/ | |
WEDNESDAY("星期三"), | |
/** | |
* 星期四 | |
*/ | |
THURSDAY("星期四"), | |
/** | |
* 星期五 | |
*/ | |
FRIDAY("星期五"), | |
/** | |
* 星期六 | |
*/ | |
SATURDAY("星期六"), | |
/** | |
* 星期天,使用逗号分隔,使用分号结尾 | |
*/ | |
SUNDAY("星期天"); | |
/** | |
* 中文描述 | |
*/ | |
private final String descriptionChinese; | |
/** | |
* 将构造私有化,防止外部调用 | |
* | |
* @param descriptionChinese 中文描述 | |
*/ | |
private DayType2(String descriptionChinese) { | |
this.descriptionChinese = descriptionChinese; | |
} | |
/** | |
* 获取中文描述 | |
* | |
* @return String | |
*/ | |
public String getDescriptionChinese() { | |
return descriptionChinese; | |
} | |
public static void main(String[] args) { | |
for (DayType2 day : DayType2.values()) { | |
System.out.println("name: " + day.name() + ",getDescriptionChinese: " + day.getDescriptionChinese()); | |
} | |
} | |
/* | |
执行结果: | |
name: MONDAY,getDescriptionChinese: 星期一 | |
name: TUESDAY,getDescriptionChinese: 星期二 | |
name: WEDNESDAY,getDescriptionChinese: 星期三 | |
name: THURSDAY,getDescriptionChinese: 星期四 | |
name: FRIDAY,getDescriptionChinese: 星期五 | |
name: SATURDAY,getDescriptionChinese: 星期六 | |
name: SUNDAY,getDescriptionChinese: 星期天 | |
*/ | |
} |
在
enum
类中确实可以像定义常规类一样声明变量和成员方法,但是我们必须注意到,如果打算在enum
类中定义方法,务必在声明完枚举实例后使用逗号分隔和使用分号来结束,倘若在枚举实例前定义任何方法,编译器都将会报错将无法通过编译,同时即使自定义了构造函数且enum
的定义结束,我们也永远无法手动调用构造函数创建枚举实例,这件事只能由编译器来执行。
# 覆盖 enum 类方法
既然
enum
类跟常规类的定义没有什么区别 (enum
类还是有些约束的),那么覆盖父类的方法也不会很难,可惜的是父类Enum
中定义的方法只有toString()
方法没有使用final
来修饰,因此只能覆盖toString()
方法,下面将通过代码覆盖toString()
来省去编写getDescriptionChinese()
方法,代码如下:
package top.rem.rain; | |
/** | |
* 枚举进阶用法 - 覆盖 toString 方法来省去 getDescriptionChinese () 方法 | |
* | |
* @author LightRain | |
*/ | |
public enum DayType3 { | |
/** | |
* 星期一 | |
*/ | |
MONDAY("星期一"), | |
/** | |
* 星期二 | |
*/ | |
TUESDAY("星期二"), | |
/** | |
* 星期三 | |
*/ | |
WEDNESDAY("星期三"), | |
/** | |
* 星期四 | |
*/ | |
THURSDAY("星期四"), | |
/** | |
* 星期五 | |
*/ | |
FRIDAY("星期五"), | |
/** | |
* 星期六 | |
*/ | |
SATURDAY("星期六"), | |
/** | |
* 星期天,使用逗号分隔,使用分号结尾 | |
*/ | |
SUNDAY("星期天"); | |
/** | |
* 中文描述 | |
*/ | |
private final String descriptionChinese; | |
/** | |
* 将构造私有化,防止外部调用 | |
* | |
* @param descriptionChinese 中文描述 | |
*/ | |
private DayType3(String descriptionChinese) { | |
this.descriptionChinese = descriptionChinese; | |
} | |
@Override | |
public String toString() { | |
return descriptionChinese; | |
} | |
public static void main(String[] args) { | |
for (DayType3 day : DayType3.values()) { | |
System.out.println("name: " + day.name() + ",getDescriptionChinese: " + day.toString()); | |
} | |
} | |
/* | |
执行结果: | |
name: MONDAY,getDescriptionChinese: 星期一 | |
name: TUESDAY,getDescriptionChinese: 星期二 | |
name: WEDNESDAY,getDescriptionChinese: 星期三 | |
name: THURSDAY,getDescriptionChinese: 星期四 | |
name: FRIDAY,getDescriptionChinese: 星期五 | |
name: SATURDAY,getDescriptionChinese: 星期六 | |
name: SUNDAY,getDescriptionChinese: 星期天 | |
*/ | |
} |
# 自定义 enum 类抽象方法
与常规抽象类一样,在
enum
类中允许我们为其定义抽象方法,然后使每个枚举实例都实现该方法,以便产生不同的行为方式,注意:abstract
关键字对于枚举类来说并不是必须的,代码如下:
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: 自定义 enum 类抽象方法 | |
* @DateTime: 2023-12-07 22:17 | |
* @Version:1.0 | |
**/ | |
public enum EnumDemo4 { | |
/** | |
* 使用枚举静态块重写 getInfo 方法来为抽象方法添加具体实现 | |
*/ | |
FIRST{ | |
@Override | |
public String getInfo() { | |
return "FIRST TIME"; | |
} | |
}, | |
SECOND{ | |
@Override | |
public String getInfo() { | |
return "SECOND TIME"; | |
} | |
}; | |
/** | |
* 在枚举类中定义抽象方法 | |
* @return String | |
*/ | |
public abstract String getInfo(); | |
public static void main(String[] args) { | |
System.out.println("F:"+EnumDemo4.FIRST.getInfo()); | |
System.out.println("S:"+EnumDemo4.SECOND.getInfo()); | |
/* | |
执行结果: | |
F:FIRST TIME | |
S:SECOND TIME | |
*/ | |
} | |
} |
通过这种方式就可以轻而易举地定义每个枚举实例的不同行为方式,我们可以注意到使用
enum
类实例似乎表现出了多态的特性,可惜的是枚举类型的实例终究不能作为类型传递使用,就像下面的使用方法,就无法通过编译器。
// 无法通过编译,毕竟 EnumDemo4.FIRST 是个实例对象 | |
public void text(EnumDemo4.FIRST instance){ } |
以上就是在枚举实例常量中定义抽象方法。
# 枚举与接口
由于
Java
单继承的原因,enum
类不能继承其它类,但它不妨碍实现接口,因此enum
类同样可以实现多接口,代码如下:
package top.rem.rain; | |
/** | |
* 食品接口 | |
*/ | |
interface Food { | |
String eat(); | |
} | |
/** | |
* 运动接口 | |
*/ | |
interface Sport { | |
String run(); | |
} | |
/** | |
* 使用 enum 类实现多接口 | |
* @author LightRain | |
*/ | |
public enum EnumDemo5 implements Food,Sport{ | |
/** | |
* 食品 | |
*/ | |
FOOD, | |
/** | |
* 运动 | |
*/ | |
SPORT; | |
@Override | |
public String eat() { | |
return "xxx喜欢吃甜食"; | |
} | |
@Override | |
public String run() { | |
return "xxx喜欢跑步"; | |
} | |
public static void main(String[] args) { | |
System.out.println(EnumDemo5.FOOD.eat()); | |
System.out.println(EnumDemo5.SPORT.run()); | |
} | |
/* | |
执行结果: | |
xxx 喜欢吃甜食 | |
xxx 喜欢跑步 | |
*/ | |
} |
有时候我们可能需要对一组数据进行分类,比如食品菜单分类而且希望这些菜单都属于
Foods
类型,SichuanCuisine(川菜)
、LuCuisine(鲁菜)
、Cantonese(粤菜)
等,每种分类下又有多种具体的菜式和食品,此时可以利用接口来组织,代码如下:
package top.rem.rain; | |
/** | |
* 食品菜单接口 | |
* | |
* @author LightRain | |
*/ | |
public interface Foods { | |
/** | |
* 川菜 | |
*/ | |
enum SichuanCuisine implements Foods { | |
/** | |
* 蚂蚁上树 | |
*/ | |
ANTS_ON_TREES("蚂蚁上树"), | |
/** | |
* 麻辣血旺 | |
*/ | |
SPICY_BLOOD("麻辣血旺"), | |
/** | |
* 四川腊肉 | |
*/ | |
SICHUAN_BACON("四川腊肉"); | |
/** | |
* 菜品描述 | |
*/ | |
private final String description; | |
SichuanCuisine(String desc) { | |
this.description = desc; | |
} | |
@Override | |
public String toString() { | |
return description; | |
} | |
} | |
/** | |
* 鲁菜 | |
*/ | |
enum LuCuisine implements Foods { | |
/** | |
* 木须肉 | |
*/ | |
MUSHU_MEAT("木须肉"), | |
/** | |
* 把子肉 | |
*/ | |
PUT_THE_MEAT("把子肉"), | |
/** | |
* 德州扒鸡 | |
*/ | |
TEXAS_GRILLED_CHICKEN("德州扒鸡"); | |
/** | |
* 菜品描述 | |
*/ | |
private final String description; | |
LuCuisine(String desc) { | |
this.description = desc; | |
} | |
@Override | |
public String toString() { | |
return description; | |
} | |
} | |
/** | |
* 粤菜 | |
*/ | |
enum Cantonese implements Foods { | |
/** | |
* 碌鸭 | |
*/ | |
DUCK("碌鸭"), | |
/** | |
* 虎皮凤爪 | |
*/ | |
TIGER_SKIN_CHICKEN_FEET("虎皮凤爪"), | |
/** | |
* 炒河粉 | |
*/ | |
FRIED_RIVER_NOODLES("炒河粉"); | |
/** | |
* 菜品描述 | |
*/ | |
private final String description; | |
Cantonese(String desc) { | |
this.description = desc; | |
} | |
@Override | |
public String toString() { | |
return description; | |
} | |
} | |
} |
在接口中是可以定义枚举静态块的。
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: 食品菜单 | |
* @DateTime: 2023-12-07 22:58 | |
* @Version:1.0 | |
**/ | |
public class FoodMenu { | |
public static void main(String[] args) { | |
Foods foods = Foods.SichuanCuisine.ANTS_ON_TREES; | |
System.out.println("ANTS_ON_TREES:" + foods); | |
foods = Foods.Cantonese.FRIED_RIVER_NOODLES; | |
System.out.println("FRIED_RIVER_NOODLES:" + foods); | |
foods = Foods.LuCuisine.TEXAS_GRILLED_CHICKEN; | |
System.out.println("TEXAS_GRILLED_CHICKEN:" + foods); | |
/* | |
执行结果: | |
ANTS_ON_TREES:蚂蚁上树 | |
FRIED_RIVER_NOODLES:炒河粉 | |
TEXAS_GRILLED_CHICKEN:德州扒鸡 | |
*/ | |
} | |
} |
通过这种方式就可以很方便组织上述场景需求,同时确保每种具体类型的事物都属于
Foods
,现在我们利用一个枚举嵌套枚举的方式,把前面定义的菜谱存放到一个Meal
菜单中,通过这种方式就可以统一管理菜单数据了,代码如下:
package top.rem.rain; | |
import java.util.Arrays; | |
/** | |
* 食品菜单整合 | |
* | |
* @author LightRain | |
*/ | |
public enum Meal { | |
/** | |
* 川菜菜单 | |
*/ | |
SICHUANCUISINE(Foods.SichuanCuisine.class), | |
/** | |
* 鲁菜菜单 | |
*/ | |
LUCUISINE(Foods.LuCuisine.class), | |
/** | |
* 粤菜菜单 | |
*/ | |
CANTONESE(Foods.Cantonese.class); | |
private final Foods[] values; | |
private Meal(Class<? extends Foods> kind) { | |
// 通过 class 对象获取枚举实例 | |
this.values = kind.getEnumConstants(); | |
} | |
public static void main(String[] args) { | |
Foods[] foods = Meal.SICHUANCUISINE.values; | |
Arrays.stream(foods).forEach(f -> { | |
System.out.println("川菜菜单:" + f); | |
}); | |
foods = Meal.LUCUISINE.values; | |
Arrays.stream(foods).forEach(f -> { | |
System.out.println("鲁菜菜单:" + f); | |
}); | |
foods = Meal.CANTONESE.values; | |
Arrays.stream(foods).forEach(f -> { | |
System.out.println("粤菜菜单:" + f); | |
}); | |
/* | |
执行结果: | |
川菜菜单:蚂蚁上树 | |
川菜菜单:麻辣血旺 | |
川菜菜单:四川腊肉 | |
鲁菜菜单:木须肉 | |
鲁菜菜单:把子肉 | |
鲁菜菜单:德州扒鸡 | |
粤菜菜单:碌鸭 | |
粤菜菜单:虎皮凤爪 | |
粤菜菜单:炒河粉 | |
*/ | |
} | |
} |
# 枚举与 switch
在使用
switch
进行条件判断时,条件参数一般只能是整形、字符型,而枚举型确实也被switch
所支持,在Java 1.7
后switch
也对字符串进行了支持,switch
与枚举类型的使用,代码如下:
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: 枚举与 switch 饿汉式 | |
* @DateTime: 2023-12-08 20:45 | |
* @Version:1.0 | |
**/ | |
public class EnumAndSwitch { | |
public static void main(String[] args) { | |
getColorName(Color.BLUE); | |
getColorName(Color.RED); | |
/* | |
执行结果: | |
蓝色 | |
红色 | |
*/ | |
} | |
private static void getColorName(Color color) { | |
switch (color) { | |
case BROWN -> System.out.println("棕色"); | |
case RED -> System.out.println("红色"); | |
case ORANGE -> System.out.println("橙色"); | |
case YELLOW -> System.out.println("黄色"); | |
case GREEN -> System.out.println("绿色"); | |
case BLUE -> System.out.println("蓝色"); | |
case PURPLE -> System.out.println("紫色"); | |
case GRAY -> System.out.println("灰色"); | |
case WHITE -> System.out.println("白色"); | |
case BLACK -> System.out.println("黑色"); | |
} | |
} | |
private enum Color { | |
/** | |
* 棕 | |
*/ | |
BROWN, | |
/** | |
* 红 | |
*/ | |
RED, | |
/** | |
* 橙 | |
*/ | |
ORANGE, | |
/** | |
* 黄 | |
*/ | |
YELLOW, | |
/** | |
* 绿 | |
*/ | |
GREEN, | |
/** | |
* 蓝 | |
*/ | |
BLUE, | |
/** | |
* 紫 | |
*/ | |
PURPLE, | |
/** | |
* 灰 | |
*/ | |
GRAY, | |
/** | |
* 白 | |
*/ | |
WHITE, | |
/** | |
* 黑 | |
*/ | |
BLACK; | |
} | |
} |
使用枚举进行
switch
条件判断时无需使用Color
引用。
# 枚举与单例模式
单例模式它的作用是确保某个类只有一个实例,自行实例化并向整个系统提供整个实例,在实际应用中,线程池、缓存、日志对象、对话框对象常被设计成单例模式,总之选择单例模式就是为了避免状态不一致,下面是单例模式的几种主要编写方式,从而对比出使用枚举实现单例模式的优点。
# 饿汉式单例模式
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: 饿汉式单例模式 (基于 classloder 机制避免了多线程的同步问题) | |
* @DateTime: 2023-12-08 21:25 | |
* @Version:1.0 | |
**/ | |
public class HungryChineseStyle { | |
private static final HungryChineseStyle INSTANCE = new HungryChineseStyle(); | |
private HungryChineseStyle() { | |
} | |
public static HungryChineseStyle getInstance() { | |
return INSTANCE; | |
} | |
} |
显然这种写法很简单,但问题是无法做到延迟创建对象,事实上如果该单例类涉及资源较多,创建比较耗费时间时,我们更希望它可以尽可能地延迟加载,从而减小初始化的负载,从而就有了懒汉式单例模式。
# 懒汉式单例模式
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: 懒汉式单例模式 (适合多线程安全) | |
* @DateTime: 2023-12-08 23:05 | |
* @Version:1.0 | |
**/ | |
public class Lazy { | |
private static Lazy INSTANCE; | |
private Lazy() { | |
} | |
public static synchronized Lazy getInstance() { | |
if (INSTANCE == null) { | |
INSTANCE = new Lazy(); | |
} | |
return INSTANCE; | |
} | |
} |
这种写法能够在多线程中很好避免同步问题,同时也具备
lazy loading
机制,遗憾的是由于synchronized
的存在,效率很低,在单线程的情境下完全可以去掉synchronize
为了效率与性能问题,改进后的代码如下:
# 双重检查锁单例模式
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: 懒汉式 - 双重检查锁 | |
* @DateTime: 2023-12-08 23:14 | |
* @Version:1.0 | |
**/ | |
public class DoubleCheckLock { | |
private static volatile DoubleCheckLock instance; | |
private DoubleCheckLock(){ | |
} | |
public static DoubleCheckLock getInstance(){ | |
if(instance==null){ | |
synchronized (DoubleCheckLock.class){ | |
if (instance==null){ | |
instance = new DoubleCheckLock(); | |
} | |
} | |
} | |
return instance; | |
} | |
} |
这种方式被称为
双重检查锁
,主要在getInstance()
方法中,进行了两次if(instance==null)
判空检查,可以极大提升并发度,进而提升性能,毕竟在单例模式中new
的情况非常少,绝大多都是可以并行操作,因此在枷锁前多进行一次null
检查可以减少绝大多数的枷锁操作,也就提高了执行效率。但是必须注意的是volatile
关键字,该关键字有两层语义,第一层是可见性,可见性是指在一个线程中对该变量的修改会马上由工作内存 (Work Memory
) 写回主内存 (Main Memory
),所以其它线程会马上读取到已修改的值,关于工作内存和主内存可以简单理解为高速缓存 (直接与CPU
打交道) 和主存 (代表内存条),注意工作内存是线程独享的,主存是线程共享的。volatile
的第二次语义是禁止指令重排序优化,我们写的代码 (特别是多线代码),由于编译器优化,会在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源码相同,却不保证实际指令的顺序与源码相同,这在单线程中并没有什么问题,然而一旦引入多线程环境,这种乱序就可能导致严重问题。volatile
关键字就可以从语义上解决这个问题,值得关注的是volatile
的禁止指令重排序优化功能在Java 1.5
后才得以实现,因此在Java 1.5
之前的版本仍然是不安全的,即使使用volatile
关键字,或许我们可以利用静态内部类来实现更安全的机制。
# 静态内部类单例模式
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: 静态内部类单例模式 | |
* @DateTime: 2023-12-08 23:34 | |
* @Version:1.0 | |
**/ | |
public class StaticInnerClass { | |
private static class Holder{ | |
private static final StaticInnerClass INSTANCE = new StaticInnerClass(); | |
} | |
private StaticInnerClass(){ | |
} | |
public static StaticInnerClass getInstance(){ | |
return Holder.INSTANCE; | |
} | |
} |
上述代码展示的就是静态内部类单例模式,我们把
INSTANCE
实例放到一个静态内部类中,这样就可以避免了静态实例在INSTANCE
类的加载阶段就创建对象,毕竟静态变量初始化是在StaticInnerClass
类初始化时触发的,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全。
# 防止被破坏单例模式
从上述
4
种单例模式的写法中,似乎也解决了效率与懒加载问题,但是他们都有两个共同缺点:- 序列化可能会破坏单例模式,比较每次反序列化一个序列化的对象实例时都会创建一个新的实例,解决方案代码如下:
Singleton.java本示例使用JAVA 8 package top.rem.rain;
import java.io.Serial;
import java.io.Serializable;
/**
* @Author: LightRain
* @Description: 避免反序列化破坏单例模式
* @DateTime: 2023-12-08 23:48
* @Version:1.0
**/
public class Singleton implements Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() {
}
/**
* 反序列时直接返回当前 INSTANCE
* @return Object;
*/
@Serial
protected Object readResolve() {
return INSTANCE;
}
}
使用反射强行调用私有构造器,解决方式可以修改构造器,让它在创建第二个实例的时候抛出异常,解决方案代码如下:
Singleton2本示例使用JAVA 8 package top.rem.rain;
/**
* @Author: LightRain
* @Description: 避免使用反射强行破坏单例模式
* @DateTime: 2023-12-08 23:56
* @Version:1.0
**/
public class Singleton2 {
public static final Singleton2 INSTANCE = new Singleton2();
private static volatile boolean flag = true;
private Singleton2() {
if (flag) {
flag = false;
} else {
throw new RuntimeException("The instance already exists !");
}
}
public static Singleton2 getInstance() {
return INSTANCE;
}
}
如上所述,问题确实也得到了解决,但问题是我们为此付出了不少努力,即添加了不少的代码,还应该注意到如果单例类维持了其它对象的状态时还需要使它们成为
transient
对象,这种就更复杂了,那么有没有简单还高效的呢?当然那就是枚举单例了,来看看如何实现,如下:
package top.rem.rain; | |
/** | |
* 枚举单例模式 | |
* | |
* @author LightRain | |
*/ | |
public enum EnumSingleton { | |
/** | |
* 实例 | |
*/ | |
INSTANCE; | |
private String name; | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
} |
代码相当简洁,我们也可以像常规类一样编写
enum
类,为其添加变量和方法,访问方式也更简单,使用EnumSingleton.INSTANCE
进行访问,这样也就避免调用getInstance()
方法,更重要的是使用枚举单例的写法,我们完全不用考虑序列化和反射问题,枚举序列化是由JVM
保证的,每一个枚举类型和定义的枚举变量在JVM
中都是唯一的,在枚举类型的序列化和反序列化上Java
做了特殊的规定,在序列化是Java
仅仅是将枚举对象的name
属性输出到结果中,反序列化的时候则是通过java.lang.Enum
的valueOf()
方法来根据名字查找枚举对象,同时编译器是不允许任何对这种序列化机制的定制并禁用writeObject
、readObject
、readObjectNoDat
、writeReplace
、readResolve
等方法,从而保证了枚举实例的唯一性,不妨来看看Enum
类的valueOf()
方法源码:
package java.lang; | |
public abstract class Enum<E extends Enum<E>> implements Constable, Comparable<E>, Serializable { | |
public static <T extends Enum<T>> T valueOf(Class<T> enumClass, String name) { | |
T result = enumClass.enumConstantDirectory().get(name); | |
if (result != null) | |
return result; | |
if (name == null) | |
throw new NullPointerException("Name is null"); | |
throw new IllegalArgumentException( | |
"No enum constant " + enumClass.getCanonicalName() + "." + name); | |
} | |
} |
实际上通过调用
enumClass
的enumConstantDirectory()
方法获取到的是一个Map
集合,在该集合中存放了以枚举name
为key
和以枚举实例变量为value
的key
&value
数据,因此通过name
的值就可以获取到枚举实例,下面是enumConstantDirectory()
源码:
package java.lang; | |
public final class Class<T> implements java.io.Serializable, | |
GenericDeclaration, | |
Type, | |
AnnotatedElement, | |
TypeDescriptor.OfField<Class<?>>, | |
Constable { | |
Map<String, T> enumConstantDirectory() { | |
Map<String, T> directory = enumConstantDirectory; | |
if (directory == null) { | |
//getEnumConstantsShared () 最终通过反射调用枚举类的 values 方法 | |
T[] universe = getEnumConstantsShared(); | |
if (universe == null) | |
throw new IllegalArgumentException(getName() + " is not an enum class"); | |
directory = new HashMap<>((int)(universe.length / 0.75f) + 1); | |
//map 存放了当前 enum 类的所有枚举实例变量,以 name 为 key | |
for (T constant : universe) { | |
directory.put(((Enum<?>)constant).name(), constant); | |
} | |
enumConstantDirectory = directory; | |
} | |
return directory; | |
} | |
private transient volatile Map<String, T> enumConstantDirectory; | |
} |
到这里我们也就可以看出枚举序列化确实不会重新创建新实例,
JVM
保证了每个枚举实例变量的唯一性,下面就是来看使用反射能不能创建枚举,通过反射获取构造器并创建枚举,代码如下:
package top.rem.rain; | |
import java.lang.reflect.Constructor; | |
import java.lang.reflect.InvocationTargetException; | |
/** | |
* @Author: LightRain | |
* @Description: 反射创建枚举实例测试 | |
* @DateTime: 2023-12-09 11:30 | |
* @Version:1.0 | |
**/ | |
public class ReflectionCreateEnumeration { | |
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { | |
// 获取枚举类的构造函数 | |
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class); | |
// 暴力破解 | |
declaredConstructor.setAccessible(true); | |
// 创建枚举 | |
EnumSingleton otherInstance = declaredConstructor.newInstance("otherInstance", 8); | |
/* | |
执行结果: | |
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects | |
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller (Constructor.java:492) | |
at java.base/java.lang.reflect.Constructor.newInstance (Constructor.java:480) | |
at top.rem.rain.ReflectionCreateEnumeration.main (ReflectionCreateEnumeration.java:19) | |
*/ | |
} | |
} |
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects | |
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492) | |
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480) | |
at top.rem.rain.ReflectionCreateEnumeration.main(ReflectionCreateEnumeration.java:19) |
很显然是告诉我们不能使用反射创建枚举类,这是为什么呢?下面来继续探究
newInstance()
方法源码:
package java.lang.reflect; | |
public final class Constructor<T> extends Executable { | |
@CallerSensitive | |
@ForceInline // to ensure Reflection.getCallerClass optimization | |
public T newInstance(Object ... initargs) | |
throws InstantiationException, IllegalAccessException, | |
IllegalArgumentException, InvocationTargetException | |
{ | |
Class<?> caller = override ? null : Reflection.getCallerClass(); | |
return newInstanceWithCaller(initargs, !override, caller); | |
} | |
/* package-private */ | |
T newInstanceWithCaller(Object[] args, boolean checkAccess, Class<?> caller) | |
throws InstantiationException, IllegalAccessException, | |
InvocationTargetException | |
{ | |
if (checkAccess) | |
checkAccess(caller, clazz, clazz, modifiers); | |
// 在这里判断 Modifier.ENUM 是不是枚举修饰的,如果是则抛出异常 | |
if ((clazz.getModifiers() & Modifier.ENUM) != 0) | |
throw new IllegalArgumentException("Cannot reflectively create enum objects"); | |
ConstructorAccessor ca = constructorAccessor; // read volatile | |
if (ca == null) { | |
ca = acquireConstructorAccessor(); | |
} | |
@SuppressWarnings("unchecked") | |
T inst = (T) ca.newInstance(args); | |
return inst; | |
} | |
} |
从源码中来看很显然,确实无法使用反射创建枚举实例,也就说明了创建枚举实例只有编译器能够做到,显然枚举订单里模式确实是个不错的选择,因此我们推荐使用它。
但是这总不是万能的,对于Android
平台这个可能未必是最好的选择,在Android
开发中,内存优化是个大块头,而使用枚举时占用的内存尝尝是静态变量订单两倍,因此Android
官方在内存优化方面给出的建议是尽量避免在Android
中使用enum
。不管如何,关于单例模式,我们总是应该记住:线程安全、延迟加载、序列化与反序列化安全,反射安全是很重要的。
# EnumMap
# EnumMap 基本用法
有这样一个问题,现在我们有一堆大小相同而颜色不同的数据,需要统计出每种颜色的数量是多少以便将数据录入仓库,定义如下枚举用于表示颜色,代码如下:
package top.rem.rain; | |
/** | |
* @author LightRain | |
*/ | |
public enum Colors { | |
GREEN,RED,BLUE,YELLOW | |
} |
解决方案如下,使用
Map
集合来统计,key
值作为颜色名称,value
代表运费数量。
package top.rem.rain; | |
import java.util.*; | |
/** | |
* @Author: LightRain | |
* @Description: EnumMap 基本用法 | |
* @DateTime: 2023-12-09 13:01 | |
* @Version:1.0 | |
**/ | |
public class EnumMapBasicUsage { | |
public static void main(String[] args) { | |
List<Clothes> list = new ArrayList<>(); | |
list.add(new Clothes("C001", Color.BLUE)); | |
list.add(new Clothes("C002", Color.YELLOW)); | |
list.add(new Clothes("C003", Color.RED)); | |
list.add(new Clothes("C004", Color.GREEN)); | |
list.add(new Clothes("C005", Color.BLUE)); | |
list.add(new Clothes("C006", Color.BLUE)); | |
list.add(new Clothes("C007", Color.RED)); | |
list.add(new Clothes("C008", Color.YELLOW)); | |
list.add(new Clothes("C009", Color.YELLOW)); | |
list.add(new Clothes("C010", Color.GREEN)); | |
// 第一种:使用 HashMap | |
Map<String, Integer> map = new HashMap<>(); | |
for (Clothes clothes : list) { | |
String colorName = clothes.getColor().name(); | |
Integer count = map.get(colorName); | |
if (count != null) { | |
map.put(colorName, count + 1); | |
} else { | |
map.put(colorName, 1); | |
} | |
} | |
System.out.println(map.toString()); | |
System.out.println("---------------"); | |
// 第二种:使用 EnumMap | |
Map<Color, Integer> enumMap = new EnumMap<>(Color.class); | |
for (Clothes clothes : list) { | |
Color color = clothes.getColor(); | |
Integer count = enumMap.get(color); | |
if (count!=null){ | |
enumMap.put(color, count+1); | |
}else { | |
enumMap.put(color, 1); | |
} | |
} | |
System.out.println(enumMap.toString()); | |
/* | |
执行结果: | |
{RED=2, BLUE=3, YELLOW=3, GREEN=2} | |
--------------- | |
{GREEN=2, RED=2, BLUE=3, YELLOW=3} | |
*/ | |
} | |
private enum Color { | |
/** | |
* 绿色 | |
*/ | |
GREEN, | |
/** | |
* 红色 | |
*/ | |
RED, | |
/** | |
* 蓝色 | |
*/ | |
BLUE, | |
/** | |
* 黄色 | |
*/ | |
YELLOW | |
} | |
private static class Clothes { | |
private String name; | |
private Color colors; | |
public Clothes(String name, Color colors) { | |
this.name = name; | |
this.colors = colors; | |
} | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
public Color getColors() { | |
return colors; | |
} | |
public void setColors(Color colors) { | |
this.colors = colors; | |
} | |
public Color getColor() { | |
return Color.valueOf(colors.name()); | |
} | |
} | |
} |
我们使用了两种解决方案,第一种是
HashMap
, 第二种是EnumMap
, 虽然都统计出了正确的结果,但是EnumMap
作为枚举的专属集合,我们没有理由再去使用HashMap
,毕竟EnumMap
要求其KEY
必须为Enum
类型的,因此使用Color
枚举实例作为KEY
最恰当不过了,也避免了获取name
的步骤,更重要的是EnumMap
效率更高,因为其内部是通过数组实现的,注意EnumMap
的Key
值不能为null
,虽说是枚举专属集合,但其操作与一般的Map
差不多,概括性来说EnumMap
是专门为枚举类型量身定制的Map
实现,虽然使用其它的Map
(如:HashMap
) 类型也能完成相同功能,但是使用EnumMap
会更加高效,它只能接收同一枚枚举类型的实例作为键值且不能为null
,由于枚举类型实例的数量相对固定并且有限,所以EnumMap
使用数组来存放与枚举类型对应的值,毕竟数组是一段连续的内存空间,根据程序局部性原理,效率会相当高,下面来进一步了解EnumMap
的用法。EnumMap
构造函数如下:
package java.util; | |
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable { | |
// 创建一个具有指定键类型的空枚举映射 | |
public EnumMap(Class<K> keyType) { | |
this.keyType = keyType; | |
keyUniverse = getKeyUniverse(keyType); | |
vals = new Object[keyUniverse.length]; | |
} | |
// 创建一个其键类型与指定枚举映射相同的枚举映射,最初包含相同的映射关系 (如果有的话) | |
public EnumMap(EnumMap<K, ? extends V> m) { | |
keyType = m.keyType; | |
keyUniverse = m.keyUniverse; | |
vals = m.vals.clone(); | |
size = m.size; | |
} | |
// 创建一个枚举映射,从指定映射对其初始化 | |
public EnumMap(Map<K, ? extends V> m) { | |
if (m instanceof EnumMap) { | |
EnumMap<K, ? extends V> em = (EnumMap<K, ? extends V>) m; | |
keyType = em.keyType; | |
keyUniverse = em.keyUniverse; | |
vals = em.vals.clone(); | |
size = em.size; | |
} else { | |
if (m.isEmpty()) | |
throw new IllegalArgumentException("Specified map is empty"); | |
keyType = m.keySet().iterator().next().getDeclaringClass(); | |
keyUniverse = getKeyUniverse(keyType); | |
vals = new Object[keyUniverse.length]; | |
putAll(m); | |
} | |
} | |
} |
与
HashMap
不同,它需要传递一个类型信息,即Class
对象,通过这个参数EnumMap
就可以根据类型信息初始化其内部数据结构,另外两个是初始化时传入一个Map
集合,代码如下:
package top.rem.rain; | |
import java.util.EnumMap; | |
import java.util.HashMap; | |
import java.util.Map; | |
/** | |
* @Author: LightRain | |
* @Description: EnumMap 构造器示例 | |
* @DateTime: 2023-12-09 16:37 | |
* @Version:1.0 | |
**/ | |
public class EnumMapConstructor { | |
public static void main(String[] args) { | |
// 第一种构造 | |
Map<Color, Integer> enumMap = new EnumMap<>(Color.class); | |
// 第二种构造 | |
Map<Color, Integer> enumMap2 = new EnumMap<>(enumMap); | |
// 第三种构造 | |
Map<Color, Integer> hashMap = new HashMap<>(); | |
hashMap.put(Color.GREEN, 2); | |
hashMap.put(Color.BLUE, 3); | |
Map<Color, Integer> enumMap3 = new EnumMap<>(hashMap); | |
} | |
private enum Color { | |
/** | |
* 绿色 | |
*/ | |
GREEN, | |
/** | |
* 红色 | |
*/ | |
RED, | |
/** | |
* 蓝色 | |
*/ | |
BLUE, | |
/** | |
* 黄色 | |
*/ | |
YELLOW | |
} | |
} |
至于
EnumMap
的方法,跟普通的Map
几乎没有区别,注意与HashMap
的主要不同在于构造器需要传递类型参数和EnumMap
保证Key
顺序与枚举中的顺序一致,但请记住Key
不能为null
。
# EnumMap 实现原理解析
这里我们主要分析其内部存储结构,添加查找的实现,了解了这几点后对
EnumMap
内部实现原理就比较清晰了,先来看数据结构和构造函数。
package java.util; | |
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable { | |
// Class 对象引用 | |
private final Class<K> keyType; | |
// 存储 Key 值的数组 | |
private transient K[] keyUniverse; | |
// 存储 Value 值的数组 | |
private transient Object[] vals; | |
//map 的 size | |
private transient int size = 0; | |
// 构造函数 | |
public EnumMap(Class<K> keyType) { | |
this.keyType = keyType; | |
keyUniverse = getKeyUniverse(keyType); | |
vals = new Object[keyUniverse.length]; | |
} | |
} |
EnumMap
继承了AbstractMap
类,因此EnumMap
具备一般Map
的使用方法,keyType
表示类型信息,keyUniverse
表示键数组,存储的是所有可能的枚举值,vals
数组表示键对应的值,size
表示键值对个数,在构造函数中通过keyUniverse = getKeyUniverse(keyType);
初始化了keyUniverse
数组的值,内部存储的是所有可能的枚举值,接着初始化了存在Value
值的数组vals
,其大小枚举实例的个数相同,getKeyUniverse
方法实现如下:
# getKeyUniverse 方法实现分析
package java.util; | |
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable { | |
/** | |
* 意思就是返回枚举数组 | |
* Returns all of the values comprising K. | |
* The result is uncloned, cached, and shared by all callers. -> 返回包含 K 的所有值。 结果是未克隆的、缓存的,并由所有调用方共享 | |
*/ | |
private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) { | |
// 最终调用到枚举类型的 values 方法,values 方法返回所有可能的枚举值 | |
return SharedSecrets.getJavaLangAccess().getEnumConstantsShared(keyType); | |
} | |
} |
将鼠标放入到
getEnumConstantsShared
上然后按Ctrl+鼠标左边
进入方法内部,以此追查。
package jdk.internal.access; | |
public interface JavaLangAccess { | |
<E extends Enum<E>> E[] getEnumConstantsShared(Class<E> klass); | |
} |
package java.lang; | |
public final class System { | |
public <E extends Enum<E>> E[] getEnumConstantsShared(Class<E> klass) { | |
return (Enum[])klass.getEnumConstantsShared(); | |
} | |
} |
package java.lang; | |
public final class Class<T> implements java.io.Serializable, GenericDeclaration, Type, AnnotatedElement, TypeDescriptor.OfField<Class<?>>, Constable { | |
T[] getEnumConstantsShared() { | |
T[] constants = enumConstants; | |
if (constants == null) { | |
if (!isEnum()) return null; | |
try { | |
//values 的调用就在这 | |
final Method values = getMethod("values"); | |
java.security.AccessController.doPrivileged( | |
new java.security.PrivilegedAction<>() { | |
public Void run() { | |
values.setAccessible(true); | |
return null; | |
} | |
}); | |
@SuppressWarnings("unchecked") | |
T[] temporaryConstants = (T[])values.invoke(null); | |
enumConstants = constants = temporaryConstants; | |
} | |
// These can happen when users concoct enum-like classes | |
// that don't comply with the enum spec. | |
catch (InvocationTargetException | NoSuchMethodException | | |
IllegalAccessException ex) { return null; } | |
} | |
return constants; | |
} | |
private transient volatile T[] enumConstants; | |
} |
从方法的返回值来看,返回类型是枚举数组,事实也是如此,最终返回值正是枚举类型的
values
方法的返回值,因此keyUniverse
数组存储就是枚举类型的所有可能的枚举值。
# put 方法实现分析
put
方法实现代码如下:
package java.util; | |
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable { | |
public V put(K key, V value) { | |
// 检测 key 的类型 | |
typeCheck(key); | |
// 获取存放 value 值的数组下标 | |
int index = key.ordinal(); | |
// 设置旧值 | |
Object oldValue = vals[index]; | |
// 设置 value 值 | |
vals[index] = maskNull(value); | |
if (oldValue == null) | |
size++; | |
// 返回旧值 | |
return unmaskNull(oldValue); | |
} | |
} |
这里通过
typeCheck
方法进行了key
类型检测,判断是否为枚举类型,如果类型不对则会抛出异常。
package java.util; | |
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable { | |
private void typeCheck(K key) { | |
Class<?> keyClass = key.getClass(); | |
if (keyClass != keyType && keyClass.getSuperclass() != keyType) | |
throw new ClassCastException(keyClass + " != " + keyType); | |
} | |
} |
接着通过
key.ordinal()
方式获取到该枚举实例的顺序值,利用此值作为下标,把值存储在vals
数组对应下标的元素中 (即vals[index]
),这也是为什么EnumMap
能维持与枚举实例相同存储顺序的原因,我们发现在对vals[]
中元素进行赋值和返回旧值时分别调用了maskNull()
和unmaskNull()
方法。
package java.util; | |
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable { | |
// 代表 Null 值的空对象实例 | |
private static final Object NULL = new Object() { | |
public int hashCode() { | |
return 0; | |
} | |
public String toString() { | |
return "java.util.EnumMap.NULL"; | |
} | |
}; | |
private Object maskNull(Object value) { | |
// 如果值为空,返回 NULL 对象否则返回 value | |
return (value == null ? NULL : value); | |
} | |
@SuppressWarnings("unchecked") | |
private V unmaskNull(Object value) { | |
// 将 NULL 对象转换为 null 值 | |
return (V)(value == NULL ? null : value); | |
} | |
} |
由此看来
EnumMap
还是允许存放null
的,但key
是绝对不能为null
,对于null
值EnumMap
进行了特殊处理,将其包装为NULL
对象,毕竟vals[]
存的是Object
,maskNull()
和unmaskNull()
方法正是用于null
的包装和解包装的,这就是EnumMap
集合的添加过程。
# get 方法实现分析
get()
方法实现代码如下:
package java.util; | |
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable { | |
public V get(Object key) { | |
return (isValidKey(key) ? | |
unmaskNull(vals[((Enum<?>)key).ordinal()]) : null); | |
} | |
// 对 Key 值的有效性和类型信息进行判断 | |
private boolean isValidKey(Object key) { | |
if (key == null) | |
return false; | |
// Cheaper than instanceof Enum followed by getDeclaringClass | |
Class<?> keyClass = key.getClass(); | |
return keyClass == keyType || keyClass.getSuperclass() == keyType; | |
} | |
} |
相对应
put()
方法,get()
方法显示相当简洁,key
有效的话直接通过ordinal
方法取索引,然后在值数组vals
里通过索引获取值返回。
# remove 方法实现分析
remove()
方法实现代码如下:
package java.util; | |
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable { | |
public V remove(Object key) { | |
// 判断 key 值是否有效 | |
if (!isValidKey(key)) | |
return null; | |
// 直接获取索引 | |
int index = ((Enum<?>)key).ordinal(); | |
Object oldValue = vals[index]; | |
// 对应下标元素值设置为 null | |
vals[index] = null; | |
if (oldValue != null) | |
size--; | |
return unmaskNull(oldValue); | |
} | |
} |
如果
key
值有效就通过key
获取下标索引值,把vals[]
对应的下标值设置为null
然后size
减一。
# containsValue&containsKey 方法实现分析
containsValue()
和containsKey()
方法实现代码如下:
package java.util; | |
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable { | |
// 判断是否包含 value | |
public boolean containsValue(Object value) { | |
value = maskNull(value); | |
// 遍历数组 | |
for (Object val : vals) | |
if (value.equals(val)) | |
return true; | |
return false; | |
} | |
// 判断是否包含 key | |
public boolean containsKey(Object key) { | |
return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null; | |
} | |
} |
判断
value
直接通过遍历数组实现,而判断key
就很简单了,判断key
是否有效和对应vals[]
中是否存在该值。
以上这就是EnumMap
的主要实现原理,即内部有两个数组,长度相同,一个表示所有可能的键 (枚举值),一个表示对应的值,不允许key
为null
,但允许value
为null
,键都有一个对应的索引,根据索引直接访问和操作其键数组和值数组,由于操作的都是数组,因此效率很高。
# EnumSet
EnumSet
是与枚举类型一起使用的专属Enum
的Set
集合,EnumSet
中所有元素都必须是枚举类型,与其它Set
接口的实现类HashSet
、TreeSet
其内部都是用对应的HashSet
、TreeMap
实现的,不同的是EnumSet
在内部实现是位向量
,它是一种极为高效的位运算操作,由于直接存储和操作都是bit
,因此EnumSet
空间和时间性能都十分可观,足以媲美传统上基于int
的位标志
的运算,重要的是我们可以像操作set
集合一般来操作为运算
,这样使用代码更简单易懂同时又具备类型安全的优势。
注意:EnumSet
不允许使用null
元素,试图插入null
元素将会抛出NullPointerException
,但试图测试判断是否存在null
元素或移除null
元素则不会抛出异常,与大多数collection
实现一样,EnumSet
不是线程安全的,因此在多线程环境下一个注意数据同步问题。
# EnumSet 基本用法
创建
EnumSet
并不能使用new
关键字,因为它是一个抽象类,而应该使用其提供的静态工厂方法,EnumSet
的静态工厂方法比较多,代码如下:
package java.util; | |
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E> implements Cloneable, java.io.Serializable { | |
// 创建一个具有指定元素类型的空 EnumSet | |
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) { | |
Enum<?>[] universe = getUniverse(elementType); | |
if (universe == null) | |
throw new ClassCastException(elementType + " not an enum"); | |
if (universe.length <= 64) | |
return new RegularEnumSet<>(elementType, universe); | |
else | |
return new JumboEnumSet<>(elementType, universe); | |
} | |
// 创建一个指定元素类型并包含所有枚举值的 EnumSet | |
public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) { | |
EnumSet<E> result = noneOf(elementType); | |
result.addAll(); | |
return result; | |
} | |
// 创建一个包括枚举值中指定范围元素的 EnumSet | |
public static <E extends Enum<E>> EnumSet<E> range(E from, E to) { | |
if (from.compareTo(to) > 0) | |
throw new IllegalArgumentException(from + " > " + to); | |
EnumSet<E> result = noneOf(from.getDeclaringClass()); | |
result.addRange(from, to); | |
return result; | |
} | |
// 创建一个包括参数中所有元素的 EnumSet | |
public static <E extends Enum<E>> EnumSet<E> of(E e) { | |
EnumSet<E> result = noneOf(e.getDeclaringClass()); | |
result.add(e); | |
return result; | |
} | |
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2) { | |
EnumSet<E> result = noneOf(e1.getDeclaringClass()); | |
result.add(e1); | |
result.add(e2); | |
return result; | |
} | |
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3) { | |
EnumSet<E> result = noneOf(e1.getDeclaringClass()); | |
result.add(e1); | |
result.add(e2); | |
result.add(e3); | |
return result; | |
} | |
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4) { | |
EnumSet<E> result = noneOf(e1.getDeclaringClass()); | |
result.add(e1); | |
result.add(e2); | |
result.add(e3); | |
result.add(e4); | |
return result; | |
} | |
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5) { | |
EnumSet<E> result = noneOf(e1.getDeclaringClass()); | |
result.add(e1); | |
result.add(e2); | |
result.add(e3); | |
result.add(e4); | |
result.add(e5); | |
return result; | |
} | |
@SafeVarargs | |
public static <E extends Enum<E>> EnumSet<E> of(E first, E... rest) { | |
EnumSet<E> result = noneOf(first.getDeclaringClass()); | |
result.add(first); | |
for (E e : rest) | |
result.add(e); | |
return result; | |
} | |
// 创建一个包含参数容器中的所有元素的 EnumSet | |
public static <E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s) { | |
return s.clone(); | |
} | |
public static <E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c) { | |
if (c instanceof EnumSet) { | |
return ((EnumSet<E>)c).clone(); | |
} else { | |
if (c.isEmpty()) | |
throw new IllegalArgumentException("Collection is empty"); | |
Iterator<E> i = c.iterator(); | |
E first = i.next(); | |
EnumSet<E> result = EnumSet.of(first); | |
while (i.hasNext()) | |
result.add(i.next()); | |
return result; | |
} | |
} | |
} |
代码示例如下:
package top.rem.rain; | |
import java.util.ArrayList; | |
import java.util.EnumSet; | |
import java.util.List; | |
/** | |
* @Author: LightRain | |
* @Description: EnumSet 用法示例 | |
* @DateTime: 2023-12-10 11:09 | |
* @Version:1.0 | |
**/ | |
public class EnumSetDemo { | |
public static void main(String[] args) { | |
// 创建一个空 Set 集合 | |
EnumSet<Color> enumSet = EnumSet.noneOf(Color.class); | |
System.out.println("添加前的EnumSet:" + enumSet); | |
enumSet.add(Color.BLUE); | |
enumSet.add(Color.BLACK); | |
enumSet.add(Color.GREEN); | |
enumSet.add(Color.YELLOW); | |
enumSet.add(Color.RED); | |
System.out.println("添加后的EnumSet:" + enumSet); | |
System.out.println("------------------------------------------------"); | |
// 使用 allOf 创建包含所有枚举类型的 enumSet,其内部根据 Class 对象初始化了所有枚举实例 | |
EnumSet<Color> enumSet1 = EnumSet.allOf(Color.class); | |
System.out.println("allOf直接填充:" + enumSet1); | |
System.out.println("------------------------------------------------"); | |
// 初始集合包括枚举值中指定范围的元素 | |
EnumSet<Color> enumSet2 = EnumSet.range(Color.RED, Color.BLUE); | |
System.out.println("指定初始化范围:" + enumSet2); | |
System.out.println("------------------------------------------------"); | |
// 指定补集,也就是从全部枚举类型中去除参数集合中的元素,如下去掉上述 enumSet2 的元素 | |
EnumSet<Color> enumSet3 = EnumSet.complementOf(enumSet2); | |
System.out.println("指定补集:" + enumSet3); | |
System.out.println("------------------------------------------------"); | |
// 初始化时直接指定元素 | |
EnumSet<Color> enumSet4 = EnumSet.of(Color.YELLOW); | |
System.out.println("指定Color.YELLOW元素:" + enumSet4); | |
EnumSet<Color> enumSet5 = EnumSet.of(Color.YELLOW, Color.RED); | |
System.out.println("指定Color.YELLOW和Color.RED元素:" + enumSet5); | |
System.out.println("------------------------------------------------"); | |
// 复制 enumSet5 容器的数据作为初始化数据 | |
EnumSet<Color> enumSet6 = EnumSet.copyOf(enumSet5); | |
System.out.println("enumSet6:" + enumSet6); | |
System.out.println("------------------------------------------------"); | |
List<Color> list = new ArrayList<>(); | |
list.add(Color.RED); | |
// 此处添加一个重复元素 | |
list.add(Color.RED); | |
list.add(Color.GREEN); | |
list.add(Color.YELLOW); | |
System.out.println("list:" + list); | |
// 使用 copyOf (Collection<E> c) | |
EnumSet<Color> enumSet7 = EnumSet.copyOf(list); | |
System.out.println("enumSet7:" + enumSet7); | |
/* | |
执行结果: | |
添加前的 EnumSet:[] | |
添加后的 EnumSet:[GREEN, RED, BLUE, BLACK, YELLOW] | |
------------------------------------------------ | |
allOf 直接填充:[GREEN, RED, BLUE, BLACK, YELLOW] | |
------------------------------------------------ | |
指定初始化范围:[RED, BLUE] | |
------------------------------------------------ | |
指定补集:[GREEN, BLACK, YELLOW] | |
------------------------------------------------ | |
指定 Color.YELLOW 元素:[YELLOW] | |
指定 Color.YELLOW 和 Color.RED 元素:[RED, YELLOW] | |
------------------------------------------------ | |
enumSet6:[RED, YELLOW] | |
------------------------------------------------ | |
list:[RED, RED, GREEN, YELLOW] | |
enumSet7:[GREEN, RED, YELLOW] | |
*/ | |
} | |
private enum Color { | |
/** | |
* 绿色 | |
*/ | |
GREEN, | |
/** | |
* 红色 | |
*/ | |
RED, | |
/** | |
* 蓝色 | |
*/ | |
BLUE, | |
/** | |
* 黑色 | |
*/ | |
BLACK, | |
/** | |
* 黄色 | |
*/ | |
YELLOW | |
} | |
} |
noneOf(Class<E> elementType)
静态方法,主要用于创建一个空的EnumSet
集合,传递参数elementType
代表的是枚举类型的类型信息,即Class
对象。EnumSet<E> allOf(Class<E> elementType)
静态方法则是创建一个填充了elementType
类型所代表的所有枚举实例,奇怪的是EnumSet
提供了多个重载形式的of()
方法,最后一个接受的是可变参数,其它重载方法则是固定参数个数,EnumSet
之所以这样设计是因为可变参数的运行效率低一些,所有在参数数据不多的情况下,强烈不建议使用传递参数为可变参数的of()
方法,即EnumSet<E> of(E first, E... rest)
。 至于EnumSet
的操作方法与set
集合一样,什么时候使用EnumSet
比较好呢?事实上当需要进行位域运算时就可以使用EnumSet
了。
# 位域运算
位域运算代码示例如下:
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: 位域运算代码示例 | |
* @DateTime: 2023-12-10 13:12 | |
* @Version:1.0 | |
**/ | |
public class BitFieldDemo { | |
// 定义位域变量 | |
/** | |
* 1 | |
*/ | |
public static final int TYPE_ONE = 1 << 0; | |
/** | |
* 2 | |
*/ | |
public static final int TYPE_TWO = 1 << 1; | |
/** | |
* 4 | |
*/ | |
public static final int TYPE_THREE = 1 << 2; | |
/** | |
* 8 | |
*/ | |
public static final int TYPE_FOUR = 1 << 3; | |
public static void main(String[] args) { | |
// 位域运算 | |
int type = TYPE_ONE | TYPE_TWO | TYPE_THREE | TYPE_FOUR; | |
System.out.println("type = " + type); | |
/* | |
执行结果: | |
type = 15 | |
*/ | |
} | |
} |
诸如上述情况,我们都可以将上述的类型定义成枚举然后采用
EnumSet
来装载,进行各种操作,这样不仅不用手动编写太多冗余代码,而且使用EnumSet
集合进行操作也将使代码更加简洁。
package top.rem.rain; | |
import java.util.EnumSet; | |
/** | |
* @Author: LightRain | |
* @Description: 枚举替代位域 | |
* @DateTime: 2023-12-10 13:23 | |
* @Version:1.0 | |
**/ | |
public class EnumSetDemo2 { | |
public static void main(String[] args) { | |
EnumSet<Type> set = EnumSet.of(Type.TYPE_ONE,Type.TYPE_FOUR); | |
} | |
private enum Type { | |
/** | |
* 1 | |
*/ | |
TYPE_ONE, | |
/** | |
* 2 | |
*/ | |
TYPE_TWO, | |
/** | |
* 3 | |
*/ | |
TYPE_THREE, | |
/** | |
* 4 | |
*/ | |
TYPE_FOUR | |
} | |
} |
EnumSet
其中最有价值的是其内部实现原理,采用的是位向量,它体现出来的是一种高效的数据处理方式,这点很值得我们去学习它。
# EnumSet 实现原理分析
关于
EnumSet
实现原理会有点绕脑,其内部执行几乎都是位运算。
# 理解位向量
分析
EnumSet
前有必要先了解一下位向量
,顾名思义位向量
就是用一个bit
位 (0
或1
) 标记一个元素的状态,用一组bit
位表示一个集合的状态,而每个位对应一个元素,每个bit
位的状态只可能有两种,即0
或1
。位向量
能表示的元素个数与向量的bit
位长度有关,如:一个int
类型能表示32
个元素,而应该long
类型则可以表示64
个元素,对于EnumSet
而言采用的就是long
类型或者long
类型数组。比如现在有一个文件中的数据,该文件存储了N = 1000000
个无序的整数,需要把这些整数读取到内存并排序再重新写回文件中,该如何解决?最简单的方式是用int
类型来存储每个数,并把其存入到数组中,再进行排序,但是这种方式将会导致存储空间异常地大,对数据操作起来效率也是问题,那么有没有更高效的方式呢?那就是运用位向量
,我们知道一个int
类型的数有4
个字节,也就是32
位,那么我们可以用N/32
个int
类型数组来表示N
个数。
a [0] 表示第 1~32 个数(0~31) | |
a [1] 表示第 33~64 个数(32~63) | |
a [2] 表示第 65~96 个数(64~95) | |
...... 以此类推 |
这样一来每当输入一个数字
m
后,我们应该先找到该数字在数组的第?
个元素,也就是a[?]
然后再确定在这个元素的第几个bit
位,找到后设置为1
,代表存在该数字。
比如:输入40
那么40/32
为1
余8
,则应该将a[1]
元素值的第9
个bit
位设置为1
(1
的二进制左移8
位后就是第9
个位置),表示该数字存在,40
数字的表示原理图过程如下:
在大概明白了位向量表示方式后,上述过程的计算方式,通过以下方式可以计算该数存储在数组的第
?
个元素和元素中第?
个bit
位置,为了方便演示,我们在这里假设第?
个元素中的?
为P
余数值为S
。
//m 除以 2^n 则商 (P) 表示为 m >> n | |
// 等同于 m / 2^5 取整数 即:40 / 32 = 1 | |
// 那么 P=1 就是数组中的第 2 个元素,即 a [1] | |
// 位操作过程如下,40 的二进制 | |
00000000 00000000 00000000 00101000 | |
// 右移 5 位即 n=5,m >> 5, 即结果转为 10 进制就是 P=1 | |
00000000 00000000 00000000 00000001 |
在这里我们使用
int
类型,即32
位,所以2^5=32
,因此n=5
, 由此计算出P
的值代表的是数组的第P
个元素,接着利用下述方式计算出余数 (S
), 以此设置该元素的第 (S+1
) 个bit
位为1
。
//m 除以 2^n 的余数 (S) 表示为 m & (2^n-1) | |
// 等同于: m % 2^5 取余数 即:40 % 32 = 8 | |
//m=40 的二进制 | |
00000000 00000000 00000000 00101000 | |
// 2^n-1 (31) 的二进制 | |
00000000 00000000 00000000 00011111 | |
//m & (2^n-1) 即 40 与 31 进行与操作得出余数 即 S=8 | |
00000000 00000000 00000000 00001000 | |
// 下面是将 a [1] 元素值的第 (8+1) 个 bit 设置为 1,为什么是 (8+1) 而不是 8?因为 1 左移 8 位就在第 9 个 bit 位了,过程如下: | |
// 1 的二进制如下: | |
00000000 00000000 00000000 00000001 | |
// 1 << 8 利用余数 8 对 1 进行左移动 | |
00000000 00000000 00000001 0000000 | |
// 然后再与 a [1] 执行或操作后就可以将对应的 bit 为设置为 1 | |
//a [P] |= 1 << S 下面将使用 Java 代码来实现 |
通过上述二进制位运算就可以计算出整数部分
P
和余数部分S
并成功设置bit
位为1
,下面是用Java
代码来实现运算过程,代码如下:
# 1️⃣定义位运算变量
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: 位运算示例:定义全局变量 | |
* @DateTime: 2023-12-10 16:15 | |
* @Version:1.0 | |
**/ | |
public class BitOperationDemo { | |
/** | |
* 存储元素的数组 | |
*/ | |
private int[] a; | |
/** | |
* 默认使用 int 类型 | |
*/ | |
private static final int BIT_LENGTH = 32; | |
/** | |
* 整数部分 | |
*/ | |
private static int P; | |
/** | |
* 余数 | |
*/ | |
private static int S; | |
/** | |
* 2^5 - 1 | |
*/ | |
private static final int MASK = 0x1F; | |
/** | |
* 2^n SHIFT=n=5 表示 2^5=32 即 bit 位长度 32 | |
*/ | |
private static final int SHIFT = 5; | |
} |
# 2️⃣位运算添加
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: 位运算示例 | |
* @DateTime: 2023-12-10 16:15 | |
* @Version:1.0 | |
**/ | |
public class BitOperationDemo { | |
/** | |
* 置位和添加操作 | |
* @param i | |
*/ | |
public void set(int i){ | |
// 1. 结果等同 P = i / BIT_LENGTH; 取整数 | |
P = i >> SHIFT; | |
// 2. 结果等同 S = i % BIT_LENGTH; 取余数 | |
S = i & MASK; | |
// 3. 赋值设置该元素 bit 位为 1 | |
a[P] |= 1 << S; | |
// 4. 将 int 型变量 j 的第 k 个比特位设置为 1, 即 j=j|(1<<k), 上述 3 句合并为一句 | |
//a[i >> SHIFT ] |= (1 << (i & MASK)); | |
} | |
} |
在计算出
P
和S
后就可以进行赋值了,其中a[P]
代表数组中第P
个元素,a[P] |= 1 << S
整句意思就是把a[P]
元素的第S+1
位设置为1
,注意从低位到高位设置 (即:从右到左),1.
2.
3.
合并为4.
,代码将更佳简洁。
既然有添加操作就会有删除操作,删除操作的过程与添加类似,只不过删除是把相对应的bit
位设置为0
,代表不存在该数值。
# 3️⃣位运算清除
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: 位运算示例 | |
* @DateTime: 2023-12-10 16:15 | |
* @Version:1.0 | |
**/ | |
public class BitOperationDemo { | |
/** | |
* 置 0 操作,相当于清除元素 | |
* | |
* @param i | |
*/ | |
public void clear(int i) { | |
// 1. 计算位于数组中第?个元素 P = i / BIT_LENGTH; | |
P = i >> SHIFT; | |
// 2. 计算余数 S = i % BIT_LENGTH; | |
S = i & MASK; | |
// 3. 把 a [P] 元素的第 S+1 个 (从低位到高位) bit 位设置为 0 | |
a[P] &= ~(1 << S); | |
// 4. 将 int 型变量 j 的第 k 个比特位设置为 0,即 j= j&~(1<<k) | |
//a[i>>SHIFT] &= ~(1<<(i &MASK)); | |
} | |
} |
与添加唯一不同的是,计算出余数
S
后利用1
左移S
位,再取反 (~
) 操作,最后进行与 (&
) 操作,即:将a[P]
元素的第S+1
个 (从低位到高位)bit
位设置为0
,表示删除该数字,这个计算过程可以自行推算一下,这就是位向量
表示法的添加和清除方法。
# 4️⃣位运算读取
package top.rem.rain; | |
/** | |
* @Author: LightRain | |
* @Description: 位运算示例 | |
* @DateTime: 2023-12-10 16:15 | |
* @Version:1.0 | |
**/ | |
public class BitOperationDemo { | |
/** | |
* 读取操作,返回 1 代表该 bit 位有值,返回 0 代表该 bit 位没值 | |
* @param i | |
* @return int | |
*/ | |
public int get(int i){ | |
//a[i>>SHIFT] & (1<<(i&MASK)); | |
P = i >> SHIFT; | |
S = i & MASK; | |
return Integer.bitCount(a[P] & (1 << S)); | |
} | |
} |
其中
Integer.bitCount()
是返回指定int
值的二进制补码 (计算机数字的二进制表示法都是使用补码表示) 表示形式的1
位的数量。
# 5️⃣位运算完整代码
package top.rem.rain; | |
import java.util.ArrayList; | |
import java.util.List; | |
import java.util.Random; | |
/** | |
* @Author: LightRain | |
* @Description: 位运算示例 | |
* @DateTime: 2023-12-10 16:15 | |
* @Version:1.0 | |
**/ | |
public class BitOperationDemo { | |
/** | |
* 存储元素的数组 | |
*/ | |
private int[] a; | |
/** | |
* 默认使用 int 类型 | |
*/ | |
private static final int BIT_LENGTH = 32; | |
/** | |
* 整数部分 | |
*/ | |
private static int P; | |
/** | |
* 余数 | |
*/ | |
private static int S; | |
/** | |
* 2^5 - 1 | |
*/ | |
private static final int MASK = 0x1F; | |
/** | |
* 2^n SHIFT=n=5 表示 2^5=32 即 bit 位长度 32 | |
*/ | |
private static final int SHIFT = 5; | |
private final int count; | |
/** | |
* 初始化位向量 | |
* | |
* @param count | |
*/ | |
public BitOperationDemo(int count) { | |
this.count = count; | |
a = new int[(count - 1) / BIT_LENGTH + 1]; | |
init(); | |
} | |
/** | |
* 将数组中元素 bit 位设置为 0 | |
*/ | |
public void init() { | |
for (int i = 0; i < count; i++) { | |
clear(i); | |
} | |
} | |
/** | |
* 置位和添加操作 | |
* | |
* @param i | |
*/ | |
public void set(int i) { | |
// 1. 结果等同 P = i / BIT_LENGTH; 取整数 | |
P = i >> SHIFT; | |
// 2. 结果等同 S = i % BIT_LENGTH; 取余数 | |
S = i & MASK; | |
// 3. 赋值设置该元素 bit 位为 1 | |
a[P] |= 1 << S; | |
// 4. 将 int 型变量 j 的第 k 个比特位设置为 1, 即 j=j|(1<<k), 上述 3 句合并为一句 | |
//a[i >> SHIFT ] |= (1 << (i & MASK)); | |
} | |
/** | |
* 置 0 操作,相当于清除元素 | |
* | |
* @param i | |
*/ | |
public void clear(int i) { | |
// 1. 计算位于数组中第?个元素 P = i / BIT_LENGTH; | |
P = i >> SHIFT; | |
// 2. 计算余数 S = i % BIT_LENGTH; | |
S = i & MASK; | |
// 3. 把 a [P] 元素的第 S+1 个 (从低位到高位) bit 位设置为 0 | |
a[P] &= ~(1 << S); | |
// 4. 将 int 型变量 j 的第 k 个比特位设置为 0,即 j= j&~(1<<k) | |
//a[i>>SHIFT] &= ~(1<<(i &MASK)); | |
} | |
/** | |
* 读取操作,返回 1 代表该 bit 位有值,返回 0 代表该 bit 位没值 | |
* | |
* @param i | |
* @return int | |
*/ | |
public int get(int i) { | |
//a[i>>SHIFT] & (1<<(i&MASK)); | |
P = i >> SHIFT; | |
S = i & MASK; | |
return Integer.bitCount(a[P] & (1 << S)); | |
} | |
/** | |
* 获取排序后的数组 | |
* @return | |
*/ | |
public List<Integer> getSortedArray(){ | |
List<Integer> sortedArray = new ArrayList<Integer>(); | |
for (int i = 0; i < count; i++) { | |
// 判断 i 是否存在 | |
if (get(i) == 1) { | |
sortedArray.add(i); | |
} | |
} | |
return sortedArray; | |
} | |
public static void main(String[] args) { | |
int count = 25; | |
List<Integer> randoms = getRandomsList(count); | |
System.out.println("排序前:"); | |
BitOperationDemo bitOperation = new BitOperationDemo(count); | |
for (Integer e : randoms) { | |
System.out.print(e+","); | |
bitOperation.set(e); | |
} | |
System.out.println(); | |
System.out.println(); | |
System.out.println("------------------------------------------------"); | |
List<Integer> sortedArray = bitOperation.getSortedArray(); | |
System.out.println(); | |
System.out.println("排序后:"); | |
for (Integer e : sortedArray) { | |
System.out.print(e+","); | |
} | |
/* | |
执行结果: | |
排序前: | |
24,12,14,8,5,9,17,11,10,22,23,7,21,19,13,15,6,1,20,18,16,2,3,4, | |
------------------------------------------------ | |
排序后: | |
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24, | |
*/ | |
} | |
private static List<Integer> getRandomsList(int count) { | |
Random random = new Random(); | |
List<Integer> randomsList = new ArrayList<Integer>(); | |
while(randomsList.size() < (count - 1)){ | |
// element ∈ [1,count) | |
int element = random.nextInt(count - 1) + 1; | |
if (!randomsList.contains(element)) { | |
randomsList.add(element); | |
} | |
} | |
return randomsList; | |
} | |
} |
# EnumSet 原理解析
有了前面
位向量
的分析,对于了解EnumSet
的实现原理就会简单一些了,EnumSet
内部使用位向量
来实现的,在前面说过EnumSet
只是一个抽象类,事实上它存在两个子类,RegularEnumSet
和JumboEnumSet
。RegularEnumSet
使用一个long
类型的变量作为位向量
,long
类型的位长度是64 bit
,因此可以存储64
个枚举实例的标志位,一般情况下够用的了,而JumboEnumSet
使用一个long
类型的数组,当枚举个数超过64
时,就会采用long
数组的方式存储。
# 1️⃣ EnumSet 内部数据结构
EnumSet
内部数据结构源码如下:
package java.util; | |
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E> implements Cloneable, java.io.Serializable { | |
/** | |
* 表示枚举类型 | |
* The class of all the elements of this set. -> 此集合的所有元素的类 | |
*/ | |
final transient Class<E> elementType; | |
/** | |
* 存储该类型信息所表示的所有可能的枚举实例 | |
* All of the values comprising E. (Cached for performance.) -> 包含 E 的所有值。(缓存以提高性能。) | |
*/ | |
final transient Enum<?>[] universe; | |
} |
EnumSet
中有两个变量:一个是elementType
用于表示枚举类型信息,另一个是universe
是数组类型用于存储该类型信息所表示的所有可能的枚举实例,EnumSet
是抽象类,因此具体实现由子类来完成。
# 2️⃣ noneOf () 静态方法
noneOf(Class<E> elementType)
静态构造方法源码如下:
package java.util; | |
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E> implements Cloneable, java.io.Serializable { | |
// 根据 EnumMap 中的一样,获取可能的枚举实例 | |
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) { | |
Enum<?>[] universe = getUniverse(elementType); | |
if (universe == null) | |
throw new ClassCastException(elementType + " not an enum"); | |
if (universe.length <= 64) | |
// 枚举个数小于 64 则创建 RegularEunmSet | |
return new RegularEnumSet<>(elementType, universe); | |
else | |
// 否则创建 JumboEnumSet | |
return new JumboEnumSet<>(elementType, universe); | |
} | |
} |
从源码可以看出如果枚举值个数小于等于
64
,则静态工厂方法中创建的就是RegularEnumSet
,否则大于64
的话就创建JumboEnumSet
。无论是RegularEnumSet
还是JumboEnumSet
,其构造函数内部都间接调用了EnumSet
的构造函数,因此最终的elementType
和universe
都传递给了父类EnumSet
的内部变量。
# 3️⃣ RegularEnumSet&JumboEnumSet
RegularEnumSet
和JumboEnumSet
构造方法源码如下:
package java.util; | |
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> { | |
// RegularEnumSet 构造 | |
RegularEnumSet(Class<E>elementType, Enum<?>[] universe) { | |
super(elementType, universe); | |
} | |
} |
package java.util; | |
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> { | |
// JumboEnumSet 构造 | |
JumboEnumSet(Class<E>elementType, Enum<?>[] universe) { | |
super(elementType, universe); | |
elements = new long[(universe.length + 63) >>> 6]; | |
} | |
} |
在
RegularEnumSet
和JumboEnumSet
类中都存在一个elements
变量,用于记录位向量
的操作。
package java.util; | |
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> { | |
@java.io.Serial | |
private static final long serialVersionUID = 3411599620347842686L; | |
// 通过 long 类型的 elements 记录位向量的操作 | |
private long elements = 0L; | |
} |
package java.util; | |
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> { | |
@java.io.Serial | |
private static final long serialVersionUID = 334349849919042784L; | |
// 通过 long 数组类型的 elements 记录位向量 | |
private long elements[]; | |
// 表示集合大小 | |
private int size = 0; | |
} |
在
RegularEnumSet
中elements
是一个long
类型的变量,共有64
个bit
位,因此可以记录64
个枚举常量,当枚举常量的数量超过64
个时,将使用JumboEnumSet
,elements
在该类中一个long
型的数组,每个数组元素都可以存储64
个枚举常量,这个过程其实与前面位向量
的分析是同样的道理,只不过前面使用的是32
位的int
类型,这里使用的是64
位的long
类型罢了。
# 4️⃣ RegularEnumSet - Add () 方法
下面来看
EnumSet
是如何添加数据的RegularEnumSet
中的add
源码如下:
package java.util; | |
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> { | |
public boolean add(E e) { | |
// 检测是否为枚举类型 | |
typeCheck(e); | |
// 记录旧 elements | |
long oldElements = elements; | |
// 执行位向量操作 | |
// 数组版:a [i>> SHIFT ] |= (1 << (i & MASK)) | |
elements |= (1L << ((Enum<?>)e).ordinal()); | |
return elements != oldElements; | |
} | |
} |
关于
elements |= (1L << ((Enum<?>)e).ordinal());
这句跟我们所分析的位向量
操作是相同的原理,只不过前面分析的是数组类型实现,这里用的long
类型单一变量实现,((Enum)e).ordinal()
通过该语句获取要添加的枚举实例的序号,然后通过1
左移再与long
类型的elements
进行或操作,就可以把对应位置上的bit
设置为1
了,也就代表该枚举实例的存在。
详细过程请看下图:注意universe
数组在EnumSet
创建时就初始化并填充了所有可能的枚举实例,而elements
值的第n
个bit
位为1
时代表枚举存在,而获取的则是从universe
数组中的第n
个元素值。
以上就是枚举实例的添加过程和获取原理,而对于
JumboEnumSet
的add
接着看下面。
# 5️⃣ JumboEnumSet - Add () 方法
接着来看
JumboEnumSet
的add
源码实现如下:
package java.util; | |
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> { | |
public boolean add(E e) { | |
typeCheck(e); | |
// 计算 ordinal 值 | |
int eOrdinal = e.ordinal(); | |
int eWordNum = eOrdinal >>> 6; | |
long oldElements = elements[eWordNum]; | |
// 与前面分析的位向量相同:a [i>> SHIFT ] |= (1 << (i & MASK)) | |
elements[eWordNum] |= (1L << eOrdinal); | |
boolean result = (elements[eWordNum] != oldElements); | |
if (result) | |
size++; | |
return result; | |
} | |
} |
关于
JumboEnumSet
的add
方法的实现与RegularEuumSet
区别是一个long
数组类型,一个long
变量,运算原理相同,数组的位向量
运算与前面的分析是相同的,。
# 6️⃣ RegularEnumSet&JumboEnumSet - remove () 方法
RegularEnumSet
和JumboEnumSet
的remove()
源码实现如下:
package java.util; | |
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> { | |
public boolean remove(Object e) { | |
if (e == null) | |
return false; | |
Class<?> eClass = e.getClass(); | |
if (eClass != elementType && eClass.getSuperclass() != elementType) | |
return false; | |
long oldElements = elements; | |
// 将 int 类型变量 j 的第 k 个比特位设置为 0,即:j = j&~(1<<k) | |
// 数组类型:a [i>>SHIFT] &= ~(1<<(i & MASK)); | |
elements &= ~(1L << ((Enum<?>)e).ordinal()); | |
return elements != oldElements; | |
} | |
} |
package java.util; | |
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> { | |
public boolean remove(Object e) { | |
if (e == null) | |
return false; | |
Class<?> eClass = e.getClass(); | |
if (eClass != elementType && eClass.getSuperclass() != elementType) | |
return false; | |
int eOrdinal = ((Enum<?>)e).ordinal(); | |
int eWordNum = eOrdinal >>> 6; | |
long oldElements = elements[eWordNum]; | |
// 与 a [i>>SHIFT] &= ~(1<<(i &MASK)); 相同 | |
elements[eWordNum] &= ~(1L << eOrdinal); | |
boolean result = (elements[eWordNum] != oldElements); | |
if (result) | |
size--; | |
return result; | |
} | |
} |
remove()
方法的实现,跟位向量
清空操作是同样的实现原理,请看下图:
至于
JumboEnumSet
的实现原理也是类似的,下面为了简洁起见,我们以RegularEnumSet
类的实现作为源码分析,毕竟JumboEnumSet
的内部实现原理可以说跟前面分析过的位向量
几乎一样。
# 7️⃣ contains ()&containsAll () 方法
接着来看如何判读是否包含某个元素,源码如下:
package java.util; | |
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> { | |
public boolean contains(Object e) { | |
if (e == null) | |
return false; | |
Class<?> eClass = e.getClass(); | |
if (eClass != elementType && eClass.getSuperclass() != elementType) | |
return false; | |
// 先左移再进行 & amp; 操作 | |
return (elements & (1L << ((Enum<?>)e).ordinal())) != 0; | |
} | |
public boolean containsAll(Collection<?> c) { | |
if (!(c instanceof RegularEnumSet<?> es)) | |
return super.containsAll(c); | |
if (es.elementType != elementType) | |
return es.isEmpty(); | |
// ~elements 取反相当于 elements 补集,再与 es.elements 进行 & amp; 操作,如果为 0 就说明 elements 补集与 es.elements 没有交集,也就是 es.elements 是 elements 的子集 | |
return (es.elements & ~elements) == 0; | |
} | |
} |
对于
contains(Object e)
方法,先左移再按位进行&
操作,不为0
则表示包含该元素,跟位向量
的get
操作实现原理差不多。对于containsAll(Collection<?> c)
则可能比较难懂,这里分析一下elements
的long
类型变量标记EnumSet
集合中已存在元素的bit
位,如果bit
位为1
则说明存在枚举实例,为0
则不存在,现在执行~elements
操作后,则说明~elements
的elements
的补集,那么只要传递进来的es.elements
与补集~elements
执行&
操作为0
,就可以证明es.elements
与补集~elements
没有交集的可能,也就说es.elements
只能是elements
的子集,这样就可以判断出当前EnumSet
集合中包含传递进来的集合c
了,详细请看下图:
上图中,
elements
代表A
,es.elements
代表 B,~elements
就是求A
的补集,(es.elements & ~elements) == 0
就是在验证A∩B
是不是空集,即B
是否为A
的子集。
# 8️⃣ retainAll () 方法
接着看
retainAll()
方法,求两个集合的交集,源码如下:
package java.util; | |
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> { | |
public boolean retainAll(Collection<?> c) { | |
if (!(c instanceof RegularEnumSet<?> es)) | |
return super.retainAll(c); | |
if (es.elementType != elementType) { | |
boolean changed = (elements != 0); | |
elements = 0; | |
return changed; | |
} | |
long oldElements = elements; | |
// 执行 & 操作,求交集比较简单 | |
elements &= es.elements; | |
return elements != oldElements; | |
} | |
} |
# 9️⃣ iterator () 方法取值
package java.util; | |
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> { | |
public Iterator<E> iterator() { | |
return new EnumSetIterator<>(); | |
} | |
private class EnumSetIterator<E extends Enum<E>> implements Iterator<E> { | |
// 记录 elements | |
long unseen; | |
// 记录最后一个返回值 | |
long lastReturned = 0; | |
EnumSetIterator() { | |
unseen = elements; | |
} | |
public boolean hasNext() { | |
return unseen != 0; | |
} | |
@SuppressWarnings("unchecked") | |
public E next() { | |
if (unseen == 0) | |
throw new NoSuchElementException(); | |
// 取值过程:先与本身负执行 & amp; 操作得出的就是二进制低位开始的第一个 1 的数值大小哦 | |
lastReturned = unseen & -unseen; | |
// 取值后减去已取得 lastReturned | |
unseen -= lastReturned; | |
// 返回在指定 long 值的二进制补码表示形式中最低位 (最右边) 的 1 位之后的零位的数量 | |
return (E) universe[Long.numberOfTrailingZeros(lastReturned)]; | |
} | |
public void remove() { | |
if (lastReturned == 0) | |
throw new IllegalStateException(); | |
elements &= ~lastReturned; | |
lastReturned = 0; | |
} | |
} | |
} |
比较难懂的应该是下面这些代码:
// 取值过程,先与本身负执行 & amp; 操作得出的就是二进制低位开始的第一个 1 的数值大小 | |
lastReturned = unseen & -unseen; | |
// 取值后减去已取得 lastReturned | |
unseen -= lastReturned; | |
// 返回在指定 long 值的二进制补码表示形式中最低位 (最右边) 的 1 位之后的零位的数量 | |
return (E) universe[Long.numberOfTrailingZeros(lastReturned)]; |
我们通过原理图来帮助理解,现在假设集合中已保存所有可能的枚举实例变量,我们需要把它们遍历出来,下面的第一个枚举元素的获取过程,显然通过
unseen & -unseen
操作,我们可以获取到二进制低位开始的第一个1
的数值,该计算的结果是要么全部都是0
要么就只有一个1
,然后赋值给lastReturned
通过Long.numberOfTrailingZeros(lastReturned)
获取到该bit
为1
在64
位的long
类型中的位置,即:从低位算起的第几个bit
。
详细看下图:该bit
的位置恰好是低位的第1
个bit
位置,也就指明了universe
数组的第一个元素就是要获取的枚举变量。执行unseen -= lastReturned
后继续进行第2
个元素的遍历,以此类推遍历出所有值,这就是EnumSet
的取值过程,真正存储枚举变量的是universe
数组,而通过long
类型变量的bit
位的0
或1
表示存储该枚举变量在universe
数组的那个位置,这样做的好处是任何操作都是执行long
类型变量的bit
位操作,这样执行效率将会特别高,毕竟是二进制直接执行,只有最终获取值时才会操作到数组universe
。
以上这些就是关于
EnumSet
的实现原理的主要部分,其内部使用位向量
来执行,存储结构简洁,节省空间,而大部分操作都是按位运算来执行,直接操作二进制数据效率极高,这就是本期的枚举全部内容。