**
《手册》第 3 、4 、39 页中有几段关于枚举类型的描述 1 :
【参考】枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。说明:枚举其实就是特殊的类,域成员均为常量,且构造方法被默认强制是私有。【推荐】如果变量值仅在一个固定范围内变化用 enum 类型来定义。【强制】二方库里可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用 枚举类型或者包含枚举类型的 POJO 对象。
大多数 Java 程序员对枚举类型一知半解,大多数程序员对枚举的用法都非常简单。
本小节主要解决以下几个问题:
**
我们学习一个框架,学习一个语言特性时,可以思考一下这个框架和语言特性出现的原因。
枚举一般用来表示一组相同类型的常量,比如月份、星期、颜色等。
枚举的主要使用场景是,当需要一组固定的常量,并且编译时成员就已能确定时就应该使用枚举。2
因此枚举类型没必要多例,如果能够保证单例,则可以减少内存开销。
另外枚举为数值提供了命名,更容易理解,而且枚举更加安全,功能更加强大。
**
前面介绍过,优先通过官方文档来学习 Java 的语言特性。
JLS 8.9 节 Enum Types 对枚举类型进行了详细地介绍 3。主要有以下几个要点:
如果枚举类如果被 abstract 或 final 修饰,枚举如果常量重复,如果尝试实例化枚举类型都会有编译错误。
枚举类除声明的枚举常量没有其他实例。
枚举类型的 E 是 Enum 的直接子类。
那么 Java 是如何保证除了定义的枚举常量外没有其他实例呢?
从手册中我们可以找到原因:
- Enum 的 clone 方法被 final 修饰,保证 enum 常量不会被克隆。
- 禁止对枚举类型的反射。
- 序列化机制保证反序列化时枚举类型不允许构造多个相同实例。
通过这些提示,我们就明白为何枚举类的构造函数是私有的,
文档中还介绍了枚举的成员,枚举的迭代,枚举类型作为 switch 的条件,带抽象函数的枚举常量等。
**
我们选取 JLS 中的一个代码片段:
public enum CoinEnum {PENNY(1), NICKEL(5), DIME(10), QUARTER(25);CoinEnum(int value) {this.value = value;}private final int value;public int value() { return value; }}
先编译:
javac CoinEnum.java
然后再反汇编:
javap -c CoinEnum
得到下面的反汇编后的代码:
public final class com.imooc.basic.learn_enum.CoinEnum extends java.lang.Enum<com.imooc.basic.learn_enum.CoinEnum> {public static final com.imooc.basic.learn_enum.CoinEnum PENNY;public static final com.imooc.basic.learn_enum.CoinEnum NICKEL;public static final com.imooc.basic.learn_enum.CoinEnum DIME;public static final com.imooc.basic.learn_enum.CoinEnum QUARTER;// 第 1 处代码public static com.imooc.basic.learn_enum.CoinEnum[] values();Code:0: getstatic #1 // Field $VALUES:[Lcom/imooc/basic/learn_enum/CoinEnum;3: invokevirtual #2 // Method "[Lcom/imooc/basic/learn_enum/CoinEnum;".clone:()Ljava/lang/Object;6: checkcast #3 // class "[Lcom/imooc/basic/learn_enum/CoinEnum;"9: areturn// 第 2 处代码public static com.imooc.basic.learn_enum.CoinEnum valueOf(java.lang.String);Code:0: ldc #4 // class com/imooc/basic/learn_enum/CoinEnum2: aload_03: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;6: checkcast #4 // class com/imooc/basic/learn_enum/CoinEnum9: areturnpublic int value();Code:0: aload_01: getfield #7 // Field value:I4: ireturnstatic {};Code:0: new #4 // class com/imooc/basic/learn_enum/CoinEnum3: dup4: ldc #8 // String PENNY6: iconst_07: iconst_18: invokespecial #9 // Method "<init>":(Ljava/lang/String;II)V11: putstatic #10 // Field PENNY:Lcom/imooc/basic/learn_enum/CoinEnum;14: new #4 // class com/imooc/basic/learn_enum/CoinEnum17: dup18: ldc #11 // String NICKEL20: iconst_121: iconst_522: invokespecial #9 // Method "<init>":(Ljava/lang/String;II)V25: putstatic #12 // Field NICKEL:Lcom/imooc/basic/learn_enum/CoinEnum;28: new #4 // class com/imooc/basic/learn_enum/CoinEnum31: dup32: ldc #13 // String DIME34: iconst_235: bipush 1037: invokespecial #9 // Method "<init>":(Ljava/lang/String;II)V40: putstatic #14 // Field DIME:Lcom/imooc/basic/learn_enum/CoinEnum;43: new #4 // class com/imooc/basic/learn_enum/CoinEnum46: dup47: ldc #15 // String QUARTER49: iconst_350: bipush 2552: invokespecial #9 // Method "<init>":(Ljava/lang/String;II)V55: putstatic #16 // Field QUARTER:Lcom/imooc/basic/learn_enum/CoinEnum;58: iconst_459: anewarray #4 // class com/imooc/basic/learn_enum/CoinEnum62: dup63: iconst_064: getstatic #10 // Field PENNY:Lcom/imooc/basic/learn_enum/CoinEnum;67: aastore68: dup69: iconst_170: getstatic #12 // Field NICKEL:Lcom/imooc/basic/learn_enum/CoinEnum;73: aastore74: dup75: iconst_276: getstatic #14 // Field DIME:Lcom/imooc/basic/learn_enum/CoinEnum;79: aastore80: dup81: iconst_382: getstatic #16 // Field QUARTER:Lcom/imooc/basic/learn_enum/CoinEnum;85: aastore86: putstatic #1 // Field $VALUES:[Lcom/imooc/basic/learn_enum/CoinEnum;89: return}
通过开头位置的继承关系
com.imooc.basic.learn_enum.Coin extends java.lang.Enum<com.imooc.basic.learn_enum.Coin>
,验证了官方手册描述的 “枚举类型的 E 是 Enum 的直接子类。” 的说法。
我们还看到枚举类编译后被被自动加上
final
关键字。
枚举常量也会被加上
public static final
修饰。
另外我们还注意到和源码相比多了两个函数:
其中一个为:
public static com.imooc.basic.learn_enum.CoinEnum valueOf(java.lang.String);
(见 “第 2 处代码” )
// 第 2 处代码public static com.imooc.basic.learn_enum.CoinEnum valueOf(java.lang.String);Code:0: ldc #4 // class com/imooc/basic/learn_enum/CoinEnum2: aload_03: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;6: checkcast #4 // class com/imooc/basic/learn_enum/CoinEnum9: areturn
这是怎么回事?干嘛用的呢?
通过第 2 处代码的 code 偏移为 3 处的代码,我们可以看出调用了
java.lang.Enum#valueOf
函数。
我们直接找到该函数的源码:
/*** Returns the enum constant of the specified enum type with the* specified name. The name must match exactly an identifier used* to declare an enum constant in this type. (Extraneous whitespace* characters are not permitted.)** <p>Note that for a particular enum type {@code T}, the* implicitly declared {@code public static T valueOf(String)}* method on that enum may be used instead of this method to map* from a name to the corresponding enum constant. All the* constants of an enum type can be obtained by calling the* implicit {@code public static T[] values()} method of that* type.** @param <T> The enum type whose constant is to be returned* @param enumType the {@code Class} object of the enum type from which* to return a constant* @param name the name of the constant to return* @return the enum constant of the specified enum type with the* specified name* @throws IllegalArgumentException if the specified enum type has* no constant with the specified name, or the specified* class object does not represent an enum type* @throws NullPointerException if {@code enumType} or {@code name}* is null* @since 1.5*/public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {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);}
根据注释我们可以知道:
- 该函数的功能时根据枚举名称和枚举类型找到对应的枚举常量。
- 所有的枚举类型有一个隐式的函数
public static T valueOf(String)
用来根据枚举名称来获取枚举常量。
- 如果想获取当前枚举的所有枚举常量可以通过调用隐式的
public static T[] values()
函数来实现。
另外一个就是上面提到的
public static com.imooc.basic.learn_enum.CoinEnum[] values();
函数。
我们回到上面反汇编的代码,偏移为 58 到 96 的指令转为 Java 代码效果和下面很类似:
private static CoinEnum[] $VALUES;static {$VALUES = new CoinEnum[4];$VALUES[0] = PENNY;$VALUES[1] = NICKEL;$VALUES[2] = DIME;$VALUES[3] = QUARTER;}
根据第 1 处代码
// 第 1 处代码public static com.imooc.basic.learn_enum.CoinEnum[] values();Code:0: getstatic #1 // Field $VALUES:[Lcom/imooc/basic/learn_enum/CoinEnum;3: invokevirtual #2 // Method "[Lcom/imooc/basic/learn_enum/CoinEnum;".clone:()Ljava/lang/Object;6: checkcast #3 // class "[Lcom/imooc/basic/learn_enum/CoinEnum;"9: areturn
我们可以大致还原成下面的代码:
public static CoinEnum[] values() {return $VALUES.clone();}
因此整体的逻辑就很清楚了。
结合前面拷贝章节讲到的内容,接下来大家思考下一个新问题:为什么返回克隆对象而不是属性里的枚举数组呢?
其实这样设计的主要原因是:避免枚举数组在外部进行修改,影响到下一次调用:
CoinEnum.values()
的结果。如:
@Testpublic void testValues(){CoinEnum[] values1 = CoinEnum.values();values1[0] = CoinEnum.QUARTER;CoinEnum[] values2 = CoinEnum.values();Assert.assertEquals(values2[0],CoinEnum.PENNY);}
通过上面代码片段可以看出:对通过 clone 函数构造的新的数组对象(values1)的某个元素重新赋值并不会影响到原数组。
因此再次调用
CoinEnum.values()
仍然会返回基于原始枚举数组创建的新的拷贝对象(values2)。
**
通过官方文档和反汇编,我们知道:枚举类都是
java.lang.Enum
的子类型。正因如此,我们可以通过查看
Enum
类的源码来学习枚举的一些知识。
*我们通过 IDEA 自带的 Diagrams -> Show Diagrams -> Java Class Diagram 可以看到 Enum 类的继承关系,以及属性和函数等信息。
可以看到实现了
Comparable<E>
和
Serializable
接口。
那么为什么要实现这两个接口?
- 实现
Comparable<E>
接口很好理解,是为了排序。
- 实现
Serializable
接口是为了序列化。
前面序列化的小节中讲到:“一个类实现序列化接口,那么其子类也具备序列化的能力。”
从这里大家就会明白,正是因为其父类
Enum
实现了序列化接口,我们的枚举类没有显式实现序列化接口,使用 Java 原生序列化也并不会报错。
其中
Enum
类有两个属性 **:
name
表示枚举的名称。
ordinal
表示枚举的顺序,其主要用在
java.util.EnumSet
和
java.util.EnumMap
这两种基于枚举的数据结构中。
感兴趣的同学可以继续研究这两个数据结构的用法。
*接下来我带大家重点看两个函数的源码:
java.lang.Enum#clone
函数和
java.lang.Enum#compareTo
函数。
我们查看
Enum
类的
clone
函数:
/*** Throws CloneNotSupportedException. This guarantees that enums* are never cloned, which is necessary to preserve their "singleton"* status.** @return (never returns)*/protected final Object clone() throws CloneNotSupportedException {throw new CloneNotSupportedException();}
通过注释和源码我们可以明确地学习到,枚举类不支持
clone
, 如果调用会报
CloneNotSupportedException
异常。
目的是为了保证枚举不能被克隆,维持单例的状态。
我们知道即使将构造方法设置为私有,也可以通过反射机制
setAccessible
为
true
后调用。普通的类可以通过
java.lang.reflect.Constructor#newInstance
来构造实例,这样就破坏了单例。
然而在该函数源码中对枚举类型会作判断并报
IllegalArgumentException
。
public T newInstance(Object ... initargs)throws InstantiationException, IllegalAccessException,IllegalArgumentException, InvocationTargetException{// 省略..if ((clazz.getModifiers() & Modifier.ENUM) != 0)throw new IllegalArgumentException("Cannot reflectively create enum objects");// 省略..return inst;}
这样就防止了通过反射来构造枚举实例的可能性。
接下来我们看
compareTo
函数源码:
/*** Compares this enum with the specified object for order. Returns a* negative integer, zero, or a positive integer as this object is less* than, equal to, or greater than the specified object.** Enum constants are only comparable to other enum constants of the* same enum type. The natural order implemented by this* method is the order in which the constants are declared.*/public final int compareTo(E o) {Enum<?> other = (Enum<?>)o;Enum<E> self = this;if (self.getClass() != other.getClass() && // optimizationself.getDeclaringClass() != other.getDeclaringClass())throw new ClassCastException();return self.ordinal - other.ordinal;}
根据注释和源码,我们可以看到:其排序的依据是 枚举常量在枚举类的声明顺序。
**
那么我们想想为啥《手册》中会有下面的这个规定呢?
【强制】二方库里可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的 POJO 对象。
注:
二方是指公司内部的其他部门;
二方库是指公司内部发布到中央仓库,可供公司内部其他应用依赖的库(jar 包)。
我们写一个测试函数来研究这个问题:
@Testpublic void serialTest() {CoinEnum[] values = CoinEnum.values();// 序列化byte[] serialize = SerializationUtils.serialize(values);log.info("序列化后的字符:{}",new String(serialize));// 反序列化CoinEnum[] values2 = SerializationUtils.deserialize(serialize);Assert.assertTrue(Objects.deepEquals(values, values2));}
我们在
java.lang.Enum#valueOf
函数第一行打断点。
大家一定要自己尝试双击左下角的调用栈部分,查看从顶层调用
org.apache.commons.lang3.SerializationUtils#deserialize(byte[])
到
java.lang.Enum#valueOf
的整个调用过程。大家还可以通过表达式来查看参数的各种属性。
可以看到枚举的反序列化是通过调用
java.lang.Enum#valueOf
来实现的 **。
另外我们可以查看序列化后的字节流的字符表示形式:
序列化后的字符:
��ur&[Lcom.imooc.basic.learn_enum.CoinEnum;ċ���>��xpr#com.imooc.basic.learn_enum.CoinEnum
xrjava.lang.Enum
xptPENNYqt
NICKELqtDIMEq~tQUARTER
大致可以看出,序列化后的数据中主要包含枚举的类型和枚举名称。
我们了解了枚举的序列化和反序列化的原理后我们再思考:为什么接口返回值不允许使用枚举类型或者包含枚举类型的 POJO 对象?
上面讲到反序列化枚举类会调用
java.lang.Enum#valueOf
:
/*** Returns the enum constant of the specified enum type with the* specified name. The name must match exactly an identifier used* to declare an enum constant in this type. (Extraneous whitespace* characters are not permitted.)** <p>Note that for a particular enum type {@code T}, the* implicitly declared {@code public static T valueOf(String)}* method on that enum may be used instead of this method to map* from a name to the corresponding enum constant. All the* constants of an enum type can be obtained by calling the* implicit {@code public static T[] values()} method of that* type.** @param <T> The enum type whose constant is to be returned* @param enumType the {@code Class} object of the enum type from which* to return a constant* @param name the name of the constant to return* @return the enum constant of the specified enum type with the* specified name* @throws IllegalArgumentException if the specified enum type has* no constant with the specified name, or the specified* class object does not represent an enum type* @throws NullPointerException if {@code enumType} or {@code name}* is null* @since 1.5*/public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {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);}
大家可以设想一下,如果将枚举当做 RPC 接口的返回值或者返回值对象的属性。如果己方接口新增枚举常量,而二方(公司的其他部门)没有及时升级 JAR 包,会出现什么情况?
此时,如果己方调用此接口时传入新的枚举常量,进行序列化。
反序列化时会调用到
java.lang.Enum#valueOf
函数, 此时参数
name
值为新的枚举名称。
T result = enumType.enumConstantDirectory().get(name);
此时
result = null
,从源码可以看出,将会抛出
IllegalArgumentException
。
通过查看该函数顶部的
@throws IllegalArgumentException
注释,我们也可以得知:
如果枚举类没有该常量,或者该反序列化的类对象并不是枚举类型则会抛出该异常。
因此,二方的枚举类添加新的常量后,如果使用方没有及时更新 JAR 包,使用 Java 反序列化时可能会抛出
IllegalArgumentException
。
除了 Java 序列化、反序列化外,其他的序列化框架对于枚举类处理也容易出现各种错误,因此请严格遵守这一条。
大家可以通过为
CoinEnum
枚举类新增一个枚举常量,并将新增的枚举常量通过 Java 序列化到文件中,然后在源码中注释掉新增的枚举常量,再反序列化,来复现这个 BUG。
有没有好的解决办法?
最常见的做法就是返回枚举的数值,并在返回的包中给出枚举类,在枚举类中提供通过根据值去获取枚举常量的方法(具体做法见下文)。
并通过使用
@see
或
{@link}
在该返回的枚举的数值注释中给出指向枚举类的快捷方式,如:
/*** 硬币值,对应的枚举参见{@link CoinEnum}*/private Integer coinValue;
**
偶尔会遇到有些团队实现通过枚举中的值获取枚举常量时,居然用 switch ,非常让人吃惊。
如上面的
CoinEnum
的根据值获取枚举的函数,有些人会这么写:
public static CoinEnum getEnum(int value) {switch (value) {case 1:return PENNY;case 5:return NICKEL;case 10:return DIME;case 25:return QUARTER;default:return null;}}
这样做不符合设计模式的六大原则之一的 “开闭原则”,因为如果删除、新增一个枚举常量等,也需要修改该函数。
开闭原则:对拓展开放,对修改关闭。
另外如果枚举常量较多,很容易映射错误,后期很难维护。
可以利用前面讲到的枚举的 values 函数实现该功能,参考写法如下:
public static CoinEnum getEnum(int value) {for (CoinEnum coinEnum : CoinEnum.values()) {if (coinEnum.value == value) {return coinEnum;}}return null;}
使用上面的写法,如果后面需要对枚举常量进行修改,该函数不需要改动,显然比之前好了很多。
实际工作中这种写法也很常见。
那么还有改进空间吗?
这种写法虽然挺不错,但是每次获取枚举对象都要遍历一次枚举数组,时间复杂度是 O (n)。
降低时间复杂度该怎么做?一个常见的思路就是空间换时间。
因此我们可以事先通过 Map 将映射关系存起来,使用时直接从 Map 中获取,参考代码如下:
@Getterpublic enum CoinEnum {PENNY(1), NICKEL(5), DIME(10), QUARTER(25)/*,NEWONE(50)*/;CoinEnum(int value) {this.value = value;}private final int value;public int value() {return value;}private static final Map<Integer, CoinEnum> cache = new HashMap<>();static {for (CoinEnum coinEnum : CoinEnum.values()) {cache.put(coinEnum.getValue(), coinEnum);}}public static CoinEnum getEnum(int value) {return cache.getOrDefault(value, null);}}
通过上面的优化,使用时时间复杂度为 O (1),性能有所提升。
那么还有改进的空间吗?
上面的代码还存在以下几个问题:
- 每个枚举类中都需要编写类似的代码,很繁琐。
- 引入提供上述工具的很多枚举类,如果仅使用枚举常量,也会触发静态代码块的执行。
可不可以不修改枚举就能具备这种功能?是不是可以抽取公共部分代码封装成工具类?
我们来试一试。
首先大家可以想想,如果我们要将这部分封装成工具函数,需要哪些参数?
显然需要枚举的类型,还需要知道枚举中哪个属性作为缓存的 key,还需要传入匹配的参数。
因此可以编写如下工具类封装获取枚举对象的方法:
mport java.util.Map;import java.util.Optional;import java.util.Set;import java.util.concurrent.ConcurrentHashMap;import java.util.function.Function;public class EnumUtils {private static final Map<Object, Object> key2EnumMap = new ConcurrentHashMap<>();private static final Set<Class> enumSet = ConcurrentHashMap.newKeySet();/*** 带缓存的获取枚举值方式** @param enumType 枚举类型* @param keyFunction 根据枚举类型获取key的函数* @param key 带匹配的Key* @param <T> 枚举泛型* @return 枚举类型*/public static <T extends java.lang.Enum<T>> Optional<T> getEnumWithCache(Class<T> enumType, Function<T, Object> keyFunction, Object key) {if (!enumSet.contains(enumType)) {// 不同的枚举类型相互不影响synchronized (enumType) {if (!enumSet.contains(enumType)) {// 添加枚举enumSet.add(enumType);// 缓存枚举键值对for (T enumThis : enumType.getEnumConstants()) {// 避免重复String mapKey = getKey(enumType, keyFunction.apply(enumThis));key2EnumMap.put(mapKey, enumThis);}}}}return Optional.ofNullable((T) key2EnumMap.get(getKey(enumType, key)));}/*** 获取key* 注:带上枚举路径避免不同枚举的Key 重复*/public static <T extends java.lang.Enum<T>> String getKey(Class<T> enumType, Object key) {return enumType.getName().concat(key.toString());}/*** 不带缓存的获取枚举值方式** @param enumType 枚举类型* @param keyFunction 根据枚举类型获取key的函数* @param key 带匹配的Key* @param <T> 枚举泛型* @return 枚举类型*/public static <T extends java.lang.Enum<T>> Optional<T> getEnum(Class<T> enumType, Function<T, Object> keyFunction, Object key) {for (T enumThis : enumType.getEnumConstants()) {if (keyFunction.apply(enumThis).equals(key)) {return Optional.of(enumThis);}}return Optional.empty();}}
注:上述的几种写法,仅适合枚举常量和对应的属性一对一的情况,其他场景可能要换一种写法。
另外建议大家再思考下此方案还有没有优化的空间?是否还有其他优雅解决方案?
使用也非常简单:
@Testpublic void test() {int key = 5;CoinEnum targetEnum = CoinEnum.NICKEL;CoinEnum anEnum = CoinEnum.getEnum(key);Assert.assertEquals(targetEnum, anEnum);// 使用缓存Optional<CoinEnum> enumWithCache = EnumUtils.getEnumWithCache(CoinEnum.class, CoinEnum::getValue, key);Assert.assertTrue(enumWithCache.isPresent());Assert.assertEquals(targetEnum, enumWithCache.get());// 不使用缓存(遍历)Optional<CoinEnum> enumResult = EnumUtils.getEnum(CoinEnum.class, CoinEnum::getValue, key);Assert.assertTrue(enumResult.isPresent());Assert.assertEquals(targetEnum, enumResult.get());}
使用上面封装的工具类,不仅能够满足功能要求,还能实现了代码的复用,同时也做到了性能的优化。
通过上面的讲解,希望大家明白 “尽信书不如无书” 的道理,不要因为看到某个博客、某本书给出一个不错的写法就认为是标准答案,要有自己的思考,要有一定的代码优化意识。
**
**
从官方文档中我们可以看到,枚举常量可以带类方法:
enum Operation {PLUS {double eval(double x, double y) { return x + y; }},MINUS {double eval(double x, double y) { return x - y; }},TIMES {double eval(double x, double y) { return x * y; }},DIVIDED_BY {double eval(double x, double y) { return x / y; }};// Each constant supports an arithmetic operationabstract double eval(double x, double y);public static void main(String args[]) {double x = Double.parseDouble(args[0]);double y = Double.parseDouble(args[1]);for (Operation op : Operation.values())System.out.println(x + " " + op + " " + y +" = " + op.eval(x, y));}}
可以在枚举类中定义抽象方法,在枚举常量中实现该方法来提供计算等功能.
JDK 源码中常见的枚举类:
java.util.concurrent.TimeUnit
类就有类似的用法。
这种策略枚举方式也是替代 if - else if - else 的一种解决方案。
**
假设业务开发中需要实现状态流转的功能。
活动有:申报 -> 批准 -> 报名 -> 开始 -> 结束几种状态,依次流转。
我们可以通过下面的代码实现:
public enum ActivityStatesEnum {/*** 活动状态* 申报-> 批准-> 报名 -> 开始 -> 结束*/DEACLARE(1) {@OverrideActivityStatesEnum nextState() {return APPROVE;}},APPROVE(2) {@OverrideActivityStatesEnum nextState() {return ENROLL;}},ENROLL(3) {@OverrideActivityStatesEnum nextState() {return START;}},START(4) {@OverrideActivityStatesEnum nextState() {return END;}},END(5) {@OverrideActivityStatesEnum nextState() {return this;}};private int status;abstract ActivityStatesEnum nextState();ActivityStatesEnum(int status) {this.status = status;}public ActivityStatesEnum getEnum(int status) {for (ActivityStatesEnum statesEnum : ActivityStatesEnum.values()) {if (statesEnum.status == status) {return statesEnum;}}return null;}}
这样做的好处是可以通过
getEnum
函数获取枚举,直接通过
nextState
来获取下一个状态,更容易封装状态流转的函数,不需要每个状态都通过
if
**
fastjson 的
com.alibaba.fastjson.parser.Feature
类,灵活使用
java.lang.Enum#ordinal
和位运算实现了灵活的特性组合。
源码如下:
public enum Feature {AutoCloseSource,// 省略了一部分代码Feature(){mask = (1 << ordinal());}public final int mask;public final int getMask() {return mask;}public static boolean isEnabled(int features, Feature feature) {return (features & feature.mask) != 0;}public static int config(int features, Feature feature, boolean state) {if (state) {features |= feature.mask;} else {features &= ~feature.mask;}return features;}public static int of(Feature[] features) {if (features == null) {return 0;}int value = 0;for (Feature feature: features) {value |= feature.mask;}return value;}}
我们知道
java.lang.Enum#ordinal
表示枚举序号。因此可以通过将 1 左移枚举序号个位置,构造各种特性的掩码。
各种特性的掩码可以任意组合,来表示不同的特征组合,也可以根据特性值反向解析出这些特性组合。
**
本节使用的学习方法有,思考技术的初衷,官方文档,读源码和反汇编。
主要要点如下:
- 枚举一般表示相同类型的常量。
- 枚举隐式继承自
Enum<E>
,实现了
Comparable<E>
和
Serializable
接口。java.util.EnumSet
和java.util.EnumMap
是两种关于Enum
的数据结构。枚举类可以使用其
ordinal
属性,通过定义抽象函数、实现接口等方式实现高级用法。
更多枚举进阶知识可参考《Effective Java》 第 6 章 枚举和注解。
下一节将讲述
ArrayList
类的
subList
函数和
Arrays
类的
asList
**
1、通过前几节介绍的
codota
来学习两种和
Enum
相关的数据结构 :
java.util.EnumSet
和
java.util.EnumMap
的用法。
2、请为
CoinEnum
枚举类新增一个枚举常量,并将新增的枚举常量通过 Java 序列化到文件中,然后注释掉源码中新增的枚举常量,再反序列化,观察效果。
**
- 阿里巴巴与 Java 社区开发者.《 Java 开发手册 1.5.0》华山版. 2019 ↩︎
- [美] Joshua Bloch.《Effective Java》[M]. 俞黎敏,译。背景:机械工业出版社,2019:131 ↩︎
- James Gosling, Bill Joy, Guy Steele, Gilad Bracha, Alex Buckley.《Java Language Specification: Java SE 8 Edition》. 2015 ↩︎
09 当switch遇到空指针
11 ArrayList的subList和Arrays的asList学习
精选留言 3
欢迎在这里发表留言,作者筛选后可公开显示
注释掉新增枚举常量会导致反序列化失败,报SerializationException: java.io.OptionalDataException 不是很理解OptionalDataException代表什么
1
回复
2019-12-08
回复letro
有些异常不常见,不明白代表什么意思的问题。 1 是否有主动进入这个异常的源码,去看它的注释?源码的注释给了很详细的介绍,看不懂可以用翻译软件翻译一下。 2 是否根据报错进入了报错的源码所在行数去断点调试?可以通过断点看看调用栈,看看上游从哪里调过来的 如果这两个问题都是否定的,那么希望你遇到类似问题要重点自己去这么做,我直接告诉你答案只能解决你一个问题,你的学习能力没有任何提升。
回复
2019-12-09 17:26:17
回复letro
注释明确写道: 以下两种情况会抛出此异常(OptionalDataException): 1 尝试从流中读取一个对象,但是下一个元素是基本类型。 2 试图使用类中自定义的readObject或readExternal方法来读取数据末尾的后面(通俗来讲,已经没数据了还要读数据) 再结合相关的例子或者你出现这个例子来理解就会好很多。
回复
2019-12-09 17:36:33
策略模式代替if else,是可以替代,但是实际工作中也不尽如人意
1
回复
2019-11-14
这个问题要看你怎么去看待。 任何技术都有适用的场景,如果代码非常简单,使用策略模式可能就没太大必要。 根据开闭原则,当代码可能被新增各种情况时,通过策略模式提高了代码的拓展性和可维护性。 另外使用姿势是否正确? 选择适合的场景使用适合的技术才是最重要的,不是所有 if else 都要用策略模式。 正如任何其他设计模式一样,策略模式本身就有一些缺点。 【我们要做的是在能够发挥它优势的地方,劣势可以容忍的地方,且没有更好方案的地方使用它】。 就像属性转换小节所讲的一样,往往在大型项目中在转换次数比较多,坑比较多的时候,才能真正体会到建议的价值。 总之多了解一些方法,选择最适合的方法。 另外大家描述问题时尽量清晰一些,比如“不尽如人意”具体指的是什么呢?
回复
2019-11-16 00:43:21
用枚举来编写单例也是特别好的用法
2
回复
2019-11-10
嗯,文中介绍了枚举保证单例的原因,因为这个相对大多数人都知道就没有专门提及。 另外真正用单例的场景,其实很少通过枚举来实现,因为单例的场景都是普通类为主。
