**

《手册》第 3 、4 、39 页中有几段关于枚举类型的描述 1

  1. 【参考】枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。
  2. 说明:枚举其实就是特殊的类,域成员均为常量,且构造方法被默认强制是私有。
  3. 【推荐】如果变量值仅在一个固定范围内变化用 enum 类型来定义。
  4. 【强制】二方库里可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用 枚举类型或者包含枚举类型的 POJO 对象。


大多数 Java 程序员对枚举类型一知半解,大多数程序员对枚举的用法都非常简单。
本小节主要解决以下几个问题:

  • 那么枚举类究竟是怎样的?
  • 默认的构造方法为何是私有的?
  • 为什么接口不要返回枚举类型。
  • 枚举类还有哪些高级用法?

    **

**


我们学习一个框架,学习一个语言特性时,可以思考一下这个框架和语言特性出现的原因。
枚举一般用来表示一组相同类型的常量,比如月份、星期、颜色等。
枚举的主要使用场景是,当需要一组固定的常量,并且编译时成员就已能确定时就应该使用枚举。2
因此枚举类型没必要多例,如果能够保证单例,则可以减少内存开销。
另外枚举为数值提供了命名,更容易理解,而且枚举更加安全,功能更加强大。

**


前面介绍过,优先通过官方文档来学习 Java 的语言特性。
JLS 8.9 节 Enum Types 对枚举类型进行了详细地介绍 3。主要有以下几个要点:
如果枚举类如果被 abstract 或 final 修饰,枚举如果常量重复,如果尝试实例化枚举类型都会有编译错误。
枚举类除声明的枚举常量没有其他实例。
枚举类型的 E 是 Enum 的直接子类。

那么 Java 是如何保证除了定义的枚举常量外没有其他实例呢?
从手册中我们可以找到原因:

  • Enum 的 clone 方法被 final 修饰,保证 enum 常量不会被克隆。
  • 禁止对枚举类型的反射。
  • 序列化机制保证反序列化时枚举类型不允许构造多个相同实例。

通过这些提示,我们就明白为何枚举类的构造函数是私有的,
文档中还介绍了枚举的成员,枚举的迭代,枚举类型作为 switch 的条件,带抽象函数的枚举常量等。

**


我们选取 JLS 中的一个代码片段:

  1. public enum CoinEnum {
  2. PENNY(1), NICKEL(5), DIME(10), QUARTER(25);
  3. CoinEnum(int value) {
  4. this.value = value;
  5. }
  6. private final int value;
  7. public int value() { return value; }
  8. }

先编译:

  1. javac CoinEnum.java

然后再反汇编:

  1. javap -c CoinEnum

得到下面的反汇编后的代码:

  1. public final class com.imooc.basic.learn_enum.CoinEnum extends java.lang.Enum<com.imooc.basic.learn_enum.CoinEnum> {
  2. public static final com.imooc.basic.learn_enum.CoinEnum PENNY;
  3. public static final com.imooc.basic.learn_enum.CoinEnum NICKEL;
  4. public static final com.imooc.basic.learn_enum.CoinEnum DIME;
  5. public static final com.imooc.basic.learn_enum.CoinEnum QUARTER;
  6. // 第 1 处代码
  7. public static com.imooc.basic.learn_enum.CoinEnum[] values();
  8. Code:
  9. 0: getstatic #1 // Field $VALUES:[Lcom/imooc/basic/learn_enum/CoinEnum;
  10. 3: invokevirtual #2 // Method "[Lcom/imooc/basic/learn_enum/CoinEnum;".clone:()Ljava/lang/Object;
  11. 6: checkcast #3 // class "[Lcom/imooc/basic/learn_enum/CoinEnum;"
  12. 9: areturn
  13. // 第 2 处代码
  14. public static com.imooc.basic.learn_enum.CoinEnum valueOf(java.lang.String);
  15. Code:
  16. 0: ldc #4 // class com/imooc/basic/learn_enum/CoinEnum
  17. 2: aload_0
  18. 3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
  19. 6: checkcast #4 // class com/imooc/basic/learn_enum/CoinEnum
  20. 9: areturn
  21. public int value();
  22. Code:
  23. 0: aload_0
  24. 1: getfield #7 // Field value:I
  25. 4: ireturn
  26. static {};
  27. Code:
  28. 0: new #4 // class com/imooc/basic/learn_enum/CoinEnum
  29. 3: dup
  30. 4: ldc #8 // String PENNY
  31. 6: iconst_0
  32. 7: iconst_1
  33. 8: invokespecial #9 // Method "<init>":(Ljava/lang/String;II)V
  34. 11: putstatic #10 // Field PENNY:Lcom/imooc/basic/learn_enum/CoinEnum;
  35. 14: new #4 // class com/imooc/basic/learn_enum/CoinEnum
  36. 17: dup
  37. 18: ldc #11 // String NICKEL
  38. 20: iconst_1
  39. 21: iconst_5
  40. 22: invokespecial #9 // Method "<init>":(Ljava/lang/String;II)V
  41. 25: putstatic #12 // Field NICKEL:Lcom/imooc/basic/learn_enum/CoinEnum;
  42. 28: new #4 // class com/imooc/basic/learn_enum/CoinEnum
  43. 31: dup
  44. 32: ldc #13 // String DIME
  45. 34: iconst_2
  46. 35: bipush 10
  47. 37: invokespecial #9 // Method "<init>":(Ljava/lang/String;II)V
  48. 40: putstatic #14 // Field DIME:Lcom/imooc/basic/learn_enum/CoinEnum;
  49. 43: new #4 // class com/imooc/basic/learn_enum/CoinEnum
  50. 46: dup
  51. 47: ldc #15 // String QUARTER
  52. 49: iconst_3
  53. 50: bipush 25
  54. 52: invokespecial #9 // Method "<init>":(Ljava/lang/String;II)V
  55. 55: putstatic #16 // Field QUARTER:Lcom/imooc/basic/learn_enum/CoinEnum;
  56. 58: iconst_4
  57. 59: anewarray #4 // class com/imooc/basic/learn_enum/CoinEnum
  58. 62: dup
  59. 63: iconst_0
  60. 64: getstatic #10 // Field PENNY:Lcom/imooc/basic/learn_enum/CoinEnum;
  61. 67: aastore
  62. 68: dup
  63. 69: iconst_1
  64. 70: getstatic #12 // Field NICKEL:Lcom/imooc/basic/learn_enum/CoinEnum;
  65. 73: aastore
  66. 74: dup
  67. 75: iconst_2
  68. 76: getstatic #14 // Field DIME:Lcom/imooc/basic/learn_enum/CoinEnum;
  69. 79: aastore
  70. 80: dup
  71. 81: iconst_3
  72. 82: getstatic #16 // Field QUARTER:Lcom/imooc/basic/learn_enum/CoinEnum;
  73. 85: aastore
  74. 86: putstatic #1 // Field $VALUES:[Lcom/imooc/basic/learn_enum/CoinEnum;
  75. 89: return
  76. }

通过开头位置的继承关系

  1. com.imooc.basic.learn_enum.Coin extends java.lang.Enum<com.imooc.basic.learn_enum.Coin>


,验证了官方手册描述的 “枚举类型的 E 是 Enum 的直接子类。” 的说法。
我们还看到枚举类编译后被被自动加上

  1. final


关键字。
枚举常量也会被加上

  1. public static final


修饰。
另外我们还注意到和源码相比多了两个函数:
其中一个为:

  1. public static com.imooc.basic.learn_enum.CoinEnum valueOf(java.lang.String);


(见 “第 2 处代码” )

  1. // 第 2 处代码
  2. public static com.imooc.basic.learn_enum.CoinEnum valueOf(java.lang.String);
  3. Code:
  4. 0: ldc #4 // class com/imooc/basic/learn_enum/CoinEnum
  5. 2: aload_0
  6. 3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
  7. 6: checkcast #4 // class com/imooc/basic/learn_enum/CoinEnum
  8. 9: areturn

这是怎么回事?干嘛用的呢?
通过第 2 处代码的 code 偏移为 3 处的代码,我们可以看出调用了

  1. java.lang.Enum#valueOf


函数。
我们直接找到该函数的源码:

  1. /**
  2. * Returns the enum constant of the specified enum type with the
  3. * specified name. The name must match exactly an identifier used
  4. * to declare an enum constant in this type. (Extraneous whitespace
  5. * characters are not permitted.)
  6. *
  7. * <p>Note that for a particular enum type {@code T}, the
  8. * implicitly declared {@code public static T valueOf(String)}
  9. * method on that enum may be used instead of this method to map
  10. * from a name to the corresponding enum constant. All the
  11. * constants of an enum type can be obtained by calling the
  12. * implicit {@code public static T[] values()} method of that
  13. * type.
  14. *
  15. * @param <T> The enum type whose constant is to be returned
  16. * @param enumType the {@code Class} object of the enum type from which
  17. * to return a constant
  18. * @param name the name of the constant to return
  19. * @return the enum constant of the specified enum type with the
  20. * specified name
  21. * @throws IllegalArgumentException if the specified enum type has
  22. * no constant with the specified name, or the specified
  23. * class object does not represent an enum type
  24. * @throws NullPointerException if {@code enumType} or {@code name}
  25. * is null
  26. * @since 1.5
  27. */
  28. public static <T extends Enum<T>> T valueOf(Class<T> enumType,
  29. String name) {
  30. T result = enumType.enumConstantDirectory().get(name);
  31. if (result != null)
  32. return result;
  33. if (name == null)
  34. throw new NullPointerException("Name is null");
  35. throw new IllegalArgumentException(
  36. "No enum constant " + enumType.getCanonicalName() + "." + name);
  37. }

根据注释我们可以知道:

  • 该函数的功能时根据枚举名称和枚举类型找到对应的枚举常量。
  • 所有的枚举类型有一个隐式的函数
  1. public static T valueOf(String)


用来根据枚举名称来获取枚举常量。

  • 如果想获取当前枚举的所有枚举常量可以通过调用隐式的
  1. public static T[] values()


函数来实现。
另外一个就是上面提到的

  1. public static com.imooc.basic.learn_enum.CoinEnum[] values();


函数。
我们回到上面反汇编的代码,偏移为 58 到 96 的指令转为 Java 代码效果和下面很类似:

  1. private static CoinEnum[] $VALUES;
  2. static {
  3. $VALUES = new CoinEnum[4];
  4. $VALUES[0] = PENNY;
  5. $VALUES[1] = NICKEL;
  6. $VALUES[2] = DIME;
  7. $VALUES[3] = QUARTER;
  8. }

根据第 1 处代码

  1. // 第 1 处代码
  2. public static com.imooc.basic.learn_enum.CoinEnum[] values();
  3. Code:
  4. 0: getstatic #1 // Field $VALUES:[Lcom/imooc/basic/learn_enum/CoinEnum;
  5. 3: invokevirtual #2 // Method "[Lcom/imooc/basic/learn_enum/CoinEnum;".clone:()Ljava/lang/Object;
  6. 6: checkcast #3 // class "[Lcom/imooc/basic/learn_enum/CoinEnum;"
  7. 9: areturn

我们可以大致还原成下面的代码:

  1. public static CoinEnum[] values() {
  2. return $VALUES.clone();
  3. }

因此整体的逻辑就很清楚了。
结合前面拷贝章节讲到的内容,接下来大家思考下一个新问题:为什么返回克隆对象而不是属性里的枚举数组呢?
其实这样设计的主要原因是:避免枚举数组在外部进行修改,影响到下一次调用:

  1. CoinEnum.values()


的结果。如:

  1. @Test
  2. public void testValues(){
  3. CoinEnum[] values1 = CoinEnum.values();
  4. values1[0] = CoinEnum.QUARTER;
  5. CoinEnum[] values2 = CoinEnum.values();
  6. Assert.assertEquals(values2[0],CoinEnum.PENNY);
  7. }

通过上面代码片段可以看出:对通过 clone 函数构造的新的数组对象(values1)的某个元素重新赋值并不会影响到原数组。
因此再次调用

  1. CoinEnum.values()


仍然会返回基于原始枚举数组创建的新的拷贝对象(values2)。

**


通过官方文档和反汇编,我们知道:枚举类都是

  1. java.lang.Enum


的子类型。正因如此,我们可以通过查看

  1. Enum


类的源码来学习枚举的一些知识。
*我们通过 IDEA 自带的 Diagrams -> Show Diagrams -> Java Class Diagram 可以看到 Enum 类的继承关系,以及属性和函数等信息。
10 枚举类的正确学习方式 - 图1
可以看到实现了

  1. Comparable<E>


  1. Serializable


接口。
那么为什么要实现这两个接口?

  • 实现
  1. Comparable<E>


接口很好理解,是为了排序。

  • 实现
  1. Serializable


接口是为了序列化。
前面序列化的小节中讲到:“一个类实现序列化接口,那么其子类也具备序列化的能力。”
从这里大家就会明白,正是因为其父类

  1. Enum


实现了序列化接口,我们的枚举类没有显式实现序列化接口,使用 Java 原生序列化也并不会报错。
其中

  1. Enum


类有两个属性 **:

  1. name


表示枚举的名称。

  1. ordinal


表示枚举的顺序,其主要用在

  1. java.util.EnumSet


  1. java.util.EnumMap


这两种基于枚举的数据结构中。
感兴趣的同学可以继续研究这两个数据结构的用法。
*接下来我带大家重点看两个函数的源码:

  1. java.lang.Enum#clone


函数和

  1. java.lang.Enum#compareTo


函数。
我们查看

  1. Enum


类的

  1. clone


函数:

  1. /**
  2. * Throws CloneNotSupportedException. This guarantees that enums
  3. * are never cloned, which is necessary to preserve their "singleton"
  4. * status.
  5. *
  6. * @return (never returns)
  7. */
  8. protected final Object clone() throws CloneNotSupportedException {
  9. throw new CloneNotSupportedException();
  10. }

通过注释和源码我们可以明确地学习到,枚举类不支持

  1. clone


, 如果调用会报

  1. CloneNotSupportedException


异常。
目的是为了保证枚举不能被克隆,维持单例的状态。
我们知道即使将构造方法设置为私有,也可以通过反射机制

  1. setAccessible


  1. true


后调用。普通的类可以通过

  1. java.lang.reflect.Constructor#newInstance


来构造实例,这样就破坏了单例。
然而在该函数源码中对枚举类型会作判断并报

  1. IllegalArgumentException



  1. public T newInstance(Object ... initargs)
  2. throws InstantiationException, IllegalAccessException,
  3. IllegalArgumentException, InvocationTargetException
  4. {
  5. // 省略..
  6. if ((clazz.getModifiers() & Modifier.ENUM) != 0)
  7. throw new IllegalArgumentException("Cannot reflectively create enum objects");
  8. // 省略..
  9. return inst;
  10. }

这样就防止了通过反射来构造枚举实例的可能性。
接下来我们看

  1. compareTo


函数源码:

  1. /**
  2. * Compares this enum with the specified object for order. Returns a
  3. * negative integer, zero, or a positive integer as this object is less
  4. * than, equal to, or greater than the specified object.
  5. *
  6. * Enum constants are only comparable to other enum constants of the
  7. * same enum type. The natural order implemented by this
  8. * method is the order in which the constants are declared.
  9. */
  10. public final int compareTo(E o) {
  11. Enum<?> other = (Enum<?>)o;
  12. Enum<E> self = this;
  13. if (self.getClass() != other.getClass() && // optimization
  14. self.getDeclaringClass() != other.getDeclaringClass())
  15. throw new ClassCastException();
  16. return self.ordinal - other.ordinal;
  17. }

根据注释和源码,我们可以看到:其排序的依据是 枚举常量在枚举类的声明顺序。

**


那么我们想想为啥《手册》中会有下面的这个规定呢?
【强制】二方库里可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的 POJO 对象。
注:
二方是指公司内部的其他部门;
二方库是指公司内部发布到中央仓库,可供公司内部其他应用依赖的库(jar 包)。

我们写一个测试函数来研究这个问题:

  1. @Test
  2. public void serialTest() {
  3. CoinEnum[] values = CoinEnum.values();
  4. // 序列化
  5. byte[] serialize = SerializationUtils.serialize(values);
  6. log.info("序列化后的字符:{}",new String(serialize));
  7. // 反序列化
  8. CoinEnum[] values2 = SerializationUtils.deserialize(serialize);
  9. Assert.assertTrue(Objects.deepEquals(values, values2));
  10. }

我们在

  1. java.lang.Enum#valueOf


函数第一行打断点。
10 枚举类的正确学习方式 - 图2
大家一定要自己尝试双击左下角的调用栈部分,查看从顶层调用

  1. org.apache.commons.lang3.SerializationUtils#deserialize(byte[])



  1. java.lang.Enum#valueOf


的整个调用过程。大家还可以通过表达式来查看参数的各种属性。
可以看到枚举的反序列化是通过调用

  1. 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 对象?
上面讲到反序列化枚举类会调用

  1. java.lang.Enum#valueOf



  1. /**
  2. * Returns the enum constant of the specified enum type with the
  3. * specified name. The name must match exactly an identifier used
  4. * to declare an enum constant in this type. (Extraneous whitespace
  5. * characters are not permitted.)
  6. *
  7. * <p>Note that for a particular enum type {@code T}, the
  8. * implicitly declared {@code public static T valueOf(String)}
  9. * method on that enum may be used instead of this method to map
  10. * from a name to the corresponding enum constant. All the
  11. * constants of an enum type can be obtained by calling the
  12. * implicit {@code public static T[] values()} method of that
  13. * type.
  14. *
  15. * @param <T> The enum type whose constant is to be returned
  16. * @param enumType the {@code Class} object of the enum type from which
  17. * to return a constant
  18. * @param name the name of the constant to return
  19. * @return the enum constant of the specified enum type with the
  20. * specified name
  21. * @throws IllegalArgumentException if the specified enum type has
  22. * no constant with the specified name, or the specified
  23. * class object does not represent an enum type
  24. * @throws NullPointerException if {@code enumType} or {@code name}
  25. * is null
  26. * @since 1.5
  27. */
  28. public static <T extends Enum<T>> T valueOf(Class<T> enumType,
  29. String name) {
  30. T result = enumType.enumConstantDirectory().get(name);
  31. if (result != null)
  32. return result;
  33. if (name == null)
  34. throw new NullPointerException("Name is null");
  35. throw new IllegalArgumentException(
  36. "No enum constant " + enumType.getCanonicalName() + "." + name);
  37. }

大家可以设想一下,如果将枚举当做 RPC 接口的返回值或者返回值对象的属性。如果己方接口新增枚举常量,而二方(公司的其他部门)没有及时升级 JAR 包,会出现什么情况?
此时,如果己方调用此接口时传入新的枚举常量,进行序列化。
反序列化时会调用到

  1. java.lang.Enum#valueOf


函数, 此时参数

  1. name


值为新的枚举名称。

  1. T result = enumType.enumConstantDirectory().get(name);

此时

  1. result = null


,从源码可以看出,将会抛出

  1. IllegalArgumentException



通过查看该函数顶部的

  1. @throws IllegalArgumentException


注释,我们也可以得知:
如果枚举类没有该常量,或者该反序列化的类对象并不是枚举类型则会抛出该异常。

因此,二方的枚举类添加新的常量后,如果使用方没有及时更新 JAR 包,使用 Java 反序列化时可能会抛出

  1. IllegalArgumentException



除了 Java 序列化、反序列化外,其他的序列化框架对于枚举类处理也容易出现各种错误,因此请严格遵守这一条。
大家可以通过为

  1. CoinEnum


枚举类新增一个枚举常量,并将新增的枚举常量通过 Java 序列化到文件中,然后在源码中注释掉新增的枚举常量,再反序列化,来复现这个 BUG。
有没有好的解决办法?
最常见的做法就是返回枚举的数值,并在返回的包中给出枚举类,在枚举类中提供通过根据值去获取枚举常量的方法(具体做法见下文)。
并通过使用

  1. @see


  1. {@link}


在该返回的枚举的数值注释中给出指向枚举类的快捷方式,如:

  1. /**
  2. * 硬币值,对应的枚举参见{@link CoinEnum}
  3. */
  4. private Integer coinValue;


**


偶尔会遇到有些团队实现通过枚举中的值获取枚举常量时,居然用 switch ,非常让人吃惊。
如上面的

  1. CoinEnum


的根据值获取枚举的函数,有些人会这么写:

  1. public static CoinEnum getEnum(int value) {
  2. switch (value) {
  3. case 1:
  4. return PENNY;
  5. case 5:
  6. return NICKEL;
  7. case 10:
  8. return DIME;
  9. case 25:
  10. return QUARTER;
  11. default:
  12. return null;
  13. }
  14. }

这样做不符合设计模式的六大原则之一的 “开闭原则”,因为如果删除、新增一个枚举常量等,也需要修改该函数。
开闭原则:对拓展开放,对修改关闭。

另外如果枚举常量较多,很容易映射错误,后期很难维护。
可以利用前面讲到的枚举的 values 函数实现该功能,参考写法如下:

  1. public static CoinEnum getEnum(int value) {
  2. for (CoinEnum coinEnum : CoinEnum.values()) {
  3. if (coinEnum.value == value) {
  4. return coinEnum;
  5. }
  6. }
  7. return null;
  8. }

使用上面的写法,如果后面需要对枚举常量进行修改,该函数不需要改动,显然比之前好了很多。
实际工作中这种写法也很常见。
那么还有改进空间吗?
这种写法虽然挺不错,但是每次获取枚举对象都要遍历一次枚举数组,时间复杂度是 O (n)。
降低时间复杂度该怎么做?一个常见的思路就是空间换时间。
因此我们可以事先通过 Map 将映射关系存起来,使用时直接从 Map 中获取,参考代码如下:

  1. @Getter
  2. public enum CoinEnum {
  3. PENNY(1), NICKEL(5), DIME(10), QUARTER(25)/*,NEWONE(50)*/;
  4. CoinEnum(int value) {
  5. this.value = value;
  6. }
  7. private final int value;
  8. public int value() {
  9. return value;
  10. }
  11. private static final Map<Integer, CoinEnum> cache = new HashMap<>();
  12. static {
  13. for (CoinEnum coinEnum : CoinEnum.values()) {
  14. cache.put(coinEnum.getValue(), coinEnum);
  15. }
  16. }
  17. public static CoinEnum getEnum(int value) {
  18. return cache.getOrDefault(value, null);
  19. }
  20. }

通过上面的优化,使用时时间复杂度为 O (1),性能有所提升。
那么还有改进的空间吗?
上面的代码还存在以下几个问题:

  • 每个枚举类中都需要编写类似的代码,很繁琐。
  • 引入提供上述工具的很多枚举类,如果仅使用枚举常量,也会触发静态代码块的执行。

可不可以不修改枚举就能具备这种功能?是不是可以抽取公共部分代码封装成工具类?
我们来试一试。
首先大家可以想想,如果我们要将这部分封装成工具函数,需要哪些参数?
显然需要枚举的类型,还需要知道枚举中哪个属性作为缓存的 key,还需要传入匹配的参数。
因此可以编写如下工具类封装获取枚举对象的方法:

  1. mport java.util.Map;
  2. import java.util.Optional;
  3. import java.util.Set;
  4. import java.util.concurrent.ConcurrentHashMap;
  5. import java.util.function.Function;
  6. public class EnumUtils {
  7. private static final Map<Object, Object> key2EnumMap = new ConcurrentHashMap<>();
  8. private static final Set<Class> enumSet = ConcurrentHashMap.newKeySet();
  9. /**
  10. * 带缓存的获取枚举值方式
  11. *
  12. * @param enumType 枚举类型
  13. * @param keyFunction 根据枚举类型获取key的函数
  14. * @param key 带匹配的Key
  15. * @param <T> 枚举泛型
  16. * @return 枚举类型
  17. */
  18. public static <T extends java.lang.Enum<T>> Optional<T> getEnumWithCache(Class<T> enumType, Function<T, Object> keyFunction, Object key) {
  19. if (!enumSet.contains(enumType)) {
  20. // 不同的枚举类型相互不影响
  21. synchronized (enumType) {
  22. if (!enumSet.contains(enumType)) {
  23. // 添加枚举
  24. enumSet.add(enumType);
  25. // 缓存枚举键值对
  26. for (T enumThis : enumType.getEnumConstants()) {
  27. // 避免重复
  28. String mapKey = getKey(enumType, keyFunction.apply(enumThis));
  29. key2EnumMap.put(mapKey, enumThis);
  30. }
  31. }
  32. }
  33. }
  34. return Optional.ofNullable((T) key2EnumMap.get(getKey(enumType, key)));
  35. }
  36. /**
  37. * 获取key
  38. * 注:带上枚举路径避免不同枚举的Key 重复
  39. */
  40. public static <T extends java.lang.Enum<T>> String getKey(Class<T> enumType, Object key) {
  41. return enumType.getName().concat(key.toString());
  42. }
  43. /**
  44. * 不带缓存的获取枚举值方式
  45. *
  46. * @param enumType 枚举类型
  47. * @param keyFunction 根据枚举类型获取key的函数
  48. * @param key 带匹配的Key
  49. * @param <T> 枚举泛型
  50. * @return 枚举类型
  51. */
  52. public static <T extends java.lang.Enum<T>> Optional<T> getEnum(Class<T> enumType, Function<T, Object> keyFunction, Object key) {
  53. for (T enumThis : enumType.getEnumConstants()) {
  54. if (keyFunction.apply(enumThis).equals(key)) {
  55. return Optional.of(enumThis);
  56. }
  57. }
  58. return Optional.empty();
  59. }
  60. }

注:上述的几种写法,仅适合枚举常量和对应的属性一对一的情况,其他场景可能要换一种写法。
另外建议大家再思考下此方案还有没有优化的空间?是否还有其他优雅解决方案?

使用也非常简单:

  1. @Test
  2. public void test() {
  3. int key = 5;
  4. CoinEnum targetEnum = CoinEnum.NICKEL;
  5. CoinEnum anEnum = CoinEnum.getEnum(key);
  6. Assert.assertEquals(targetEnum, anEnum);
  7. // 使用缓存
  8. Optional<CoinEnum> enumWithCache = EnumUtils.getEnumWithCache(CoinEnum.class, CoinEnum::getValue, key);
  9. Assert.assertTrue(enumWithCache.isPresent());
  10. Assert.assertEquals(targetEnum, enumWithCache.get());
  11. // 不使用缓存(遍历)
  12. Optional<CoinEnum> enumResult = EnumUtils.getEnum(CoinEnum.class, CoinEnum::getValue, key);
  13. Assert.assertTrue(enumResult.isPresent());
  14. Assert.assertEquals(targetEnum, enumResult.get());
  15. }

使用上面封装的工具类,不仅能够满足功能要求,还能实现了代码的复用,同时也做到了性能的优化。
通过上面的讲解,希望大家明白 “尽信书不如无书” 的道理,不要因为看到某个博客、某本书给出一个不错的写法就认为是标准答案,要有自己的思考,要有一定的代码优化意识。

**

**


从官方文档中我们可以看到,枚举常量可以带类方法:

  1. enum Operation {
  2. PLUS {
  3. double eval(double x, double y) { return x + y; }
  4. },
  5. MINUS {
  6. double eval(double x, double y) { return x - y; }
  7. },
  8. TIMES {
  9. double eval(double x, double y) { return x * y; }
  10. },
  11. DIVIDED_BY {
  12. double eval(double x, double y) { return x / y; }
  13. };
  14. // Each constant supports an arithmetic operation
  15. abstract double eval(double x, double y);
  16. public static void main(String args[]) {
  17. double x = Double.parseDouble(args[0]);
  18. double y = Double.parseDouble(args[1]);
  19. for (Operation op : Operation.values())
  20. System.out.println(x + " " + op + " " + y +
  21. " = " + op.eval(x, y));
  22. }
  23. }

可以在枚举类中定义抽象方法,在枚举常量中实现该方法来提供计算等功能.
JDK 源码中常见的枚举类:

  1. java.util.concurrent.TimeUnit


类就有类似的用法。
这种策略枚举方式也是替代 if - else if - else 的一种解决方案。

**


假设业务开发中需要实现状态流转的功能。
活动有:申报 -> 批准 -> 报名 -> 开始 -> 结束几种状态,依次流转。
我们可以通过下面的代码实现:

  1. public enum ActivityStatesEnum {
  2. /**
  3. * 活动状态
  4. * 申报-> 批准-> 报名 -> 开始 -> 结束
  5. */
  6. DEACLARE(1) {
  7. @Override
  8. ActivityStatesEnum nextState() {
  9. return APPROVE;
  10. }
  11. },
  12. APPROVE(2) {
  13. @Override
  14. ActivityStatesEnum nextState() {
  15. return ENROLL;
  16. }
  17. },
  18. ENROLL(3) {
  19. @Override
  20. ActivityStatesEnum nextState() {
  21. return START;
  22. }
  23. },
  24. START(4) {
  25. @Override
  26. ActivityStatesEnum nextState() {
  27. return END;
  28. }
  29. },
  30. END(5) {
  31. @Override
  32. ActivityStatesEnum nextState() {
  33. return this;
  34. }
  35. };
  36. private int status;
  37. abstract ActivityStatesEnum nextState();
  38. ActivityStatesEnum(int status) {
  39. this.status = status;
  40. }
  41. public ActivityStatesEnum getEnum(int status) {
  42. for (ActivityStatesEnum statesEnum : ActivityStatesEnum.values()) {
  43. if (statesEnum.status == status) {
  44. return statesEnum;
  45. }
  46. }
  47. return null;
  48. }
  49. }

这样做的好处是可以通过

  1. getEnum


函数获取枚举,直接通过

  1. nextState


来获取下一个状态,更容易封装状态流转的函数,不需要每个状态都通过

  1. if


判断再指定下一个状态,也降低出错的概率。

**


fastjson 的

  1. com.alibaba.fastjson.parser.Feature


类,灵活使用

  1. java.lang.Enum#ordinal


和位运算实现了灵活的特性组合。
源码如下:

  1. public enum Feature {
  2. AutoCloseSource,
  3. // 省略了一部分代码
  4. Feature(){
  5. mask = (1 << ordinal());
  6. }
  7. public final int mask;
  8. public final int getMask() {
  9. return mask;
  10. }
  11. public static boolean isEnabled(int features, Feature feature) {
  12. return (features & feature.mask) != 0;
  13. }
  14. public static int config(int features, Feature feature, boolean state) {
  15. if (state) {
  16. features |= feature.mask;
  17. } else {
  18. features &= ~feature.mask;
  19. }
  20. return features;
  21. }
  22. public static int of(Feature[] features) {
  23. if (features == null) {
  24. return 0;
  25. }
  26. int value = 0;
  27. for (Feature feature: features) {
  28. value |= feature.mask;
  29. }
  30. return value;
  31. }
  32. }

我们知道

  1. java.lang.Enum#ordinal


表示枚举序号。因此可以通过将 1 左移枚举序号个位置,构造各种特性的掩码。
各种特性的掩码可以任意组合,来表示不同的特征组合,也可以根据特性值反向解析出这些特性组合。

**


本节使用的学习方法有,思考技术的初衷,官方文档,读源码和反汇编。
主要要点如下:

  1. 枚举一般表示相同类型的常量。
  2. 枚举隐式继承自
  1. Enum<E>


,实现了

  1. Comparable<E>
    1. Serializable


    接口。

    1. java.util.EnumSet


    1. java.util.EnumMap


    是两种关于

    1. Enum


    的数据结构。

  1. 枚举类可以使用其

  1. ordinal


属性,通过定义抽象函数、实现接口等方式实现高级用法。
更多枚举进阶知识可参考《Effective Java》 第 6 章 枚举和注解。
下一节将讲述

  1. ArrayList


类的

  1. subList


函数和

  1. Arrays


类的

  1. asList


函数。

**


1、通过前几节介绍的

  1. codota


来学习两种和

  1. Enum


相关的数据结构 :

  1. java.util.EnumSet


  1. java.util.EnumMap


的用法。
2、请为

  1. CoinEnum


枚举类新增一个枚举常量,并将新增的枚举常量通过 Java 序列化到文件中,然后注释掉源码中新增的枚举常量,再反序列化,观察效果。

**


  1. 阿里巴巴与 Java 社区开发者.《 Java 开发手册 1.5.0》华山版. 2019 ↩︎
  2. [美] Joshua Bloch.《Effective Java》[M]. 俞黎敏,译。背景:机械工业出版社,2019:131 ↩︎
  3. 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

回复慕粉3543028

这个问题要看你怎么去看待。 任何技术都有适用的场景,如果代码非常简单,使用策略模式可能就没太大必要。 根据开闭原则,当代码可能被新增各种情况时,通过策略模式提高了代码的拓展性和可维护性。 另外使用姿势是否正确? 选择适合的场景使用适合的技术才是最重要的,不是所有 if else 都要用策略模式。 正如任何其他设计模式一样,策略模式本身就有一些缺点。 【我们要做的是在能够发挥它优势的地方,劣势可以容忍的地方,且没有更好方案的地方使用它】。 就像属性转换小节所讲的一样,往往在大型项目中在转换次数比较多,坑比较多的时候,才能真正体会到建议的价值。 总之多了解一些方法,选择最适合的方法。 另外大家描述问题时尽量清晰一些,比如“不尽如人意”具体指的是什么呢?
回复
2019-11-16 00:43:21


用枚举来编写单例也是特别好的用法
2
回复
2019-11-10

回复慕粉3543028

嗯,文中介绍了枚举保证单例的原因,因为这个相对大多数人都知道就没有专门提及。 另外真正用单例的场景,其实很少通过枚举来实现,因为单例的场景都是普通类为主。