还有你不知道的Java枚举特性(下篇)
简介
这篇是博文 还有你不知道的Java枚举特性(上篇) 的下篇,可以点击下面的链接前往。
本篇主要内容:
- Java 枚举是一个特殊的类,聊聊其方法的重写
- 如何使用接口来组织 Java 枚举?
- 如何使用枚举实现 Java 的单例模式
- JDK 数据结构中关于枚举的集合
EnumSet
和字典EnumMap
重写枚举的方法
所有的枚举类都继承自 Enum
,在这个父类当中 toString
、equals
和hashCode
的三个方法,可以看一下,源码如下:
1 | public String toString() { |
可以看出在这三个方法当中,我们只能重写 toString
方法,另外两个方法都是 final
修饰的方法,不可以被子类重写。
我们在自定义的枚举中,可以重写 toString
方法的,示例如下:
1 | public enum Color { |
关于 Enum
的源码,可以在博文 Java 枚举的本质 中的文末翻阅。
对于 Java 中所有枚举都是继承自 Enum
,大家可以去使用 javap
命令反编译看看,如下代码是 javap
后的简单示例:
1 | public final class com.veryitman.Color extends java.lang.Enum<com.veryitman.Color> { |
可以看到 Color
是继承自 java.lang.Enum
的,Enum
是一个抽象类并实现了 Comparable
和 Serializable
这两个接口,如下:
1 | public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable |
使用接口组织枚举
当我们定义的枚举过多且又有很多嵌套,可以使用接口来组织这些枚举,将其归类,这样一来不仅代码看起来很规范,并且也很好管理代码。
如下示例,使用接口 MobileTool
来组织两个枚举。
1 | public interface MobileTool { |
简单的可以这样使用,示例如下:
1 | MobileTool mphone = MobileTool.Phone.HUAWEI; |
枚举实现单例模式
在 Effective Java
这本书籍中,作者有个这样下面的描述:
1 | "This approach is functionally equivalent to the public field approach, except that it is more concise, provides the serialization machinery for free, and provides an ironclad guarantee against multiple instantiation, even in the face of sophisticated serialization or reflection attacks. While this approach has yet to be widely adopted, a single-element enum type is the best way to implement a singleton." |
核心的意思是
1 | 使用枚举实现单例的方法虽然还没有广泛采用,但单元素的枚举类型已经成为实现 Java 单例模式的最佳方法。 |
我们用枚举实现一下单例模式,示例代码如下:
1 | public enum Foo { |
相比 Java 中其他的单例实现方式,此时此刻你会发现,枚举实现单例的代码会精简很多。
那么,枚举实现单例模式到底有哪些优势呢?或者换句话说,就这样实现单例靠谱吗?
经过大量例子和 Java 编程专家的讲解,枚举实现单例模式相当靠谱,它具有以下一些特点:
1、枚举实现的单例模式是线程安全的
本质上面来讲,枚举实现的单例之所以是线程安全的,这个跟 Java 的类加载机制有关。从上面反编译的代码来看,枚举是 final class
并且每个枚举值都是 static
的,这里牵扯到 ClassLoader
的相关知识,如果有兴趣建议大家去研究一下。
总之,对于我们任何一个枚举在第一次被真正用到之时,会被 Java 虚拟机加载并且完成初始化,而这个初始化过程是线程安全的,所以你需要记住枚举实现的单例模式是多线程安全的就可以了。
2、枚举可解决反射/反序列化问题
我们知道,一般的单例模式都存在两个问题,一个是可以通过反射调用,另一个就是可以通过序列化和反序列化来破坏单例。
一般解决反射调用可以通过私有构造方法中做处理,示例代码如下:
1 | private static boolean flag = false; |
了解序列化原理的同学,可以通过在单例类中实现 readResolve
方法就可以避免反序列化攻击这个问题了。示例代码如下:
1 | private Object readResolve() { |
Java 的枚举的反序列化实现并不是通过反射实现的,也就是说枚举的序列化和反序列化是有经过特殊定制和处理的,这就可以避免反序列化过程中由于反射而导致的单例被破坏问题。
总之,枚举实现的单例模式不仅可以防止反射破坏,还可以防止序列化破坏单例。
除枚举实现单这种方式以外,我一般使用下面两种方式来实现单例模式。
饿汉式的单例模式写法
1 | public class Foo { |
静态内部类实现单例模式
1 | public class Foo { |
枚举集合
EnumSet
是一个专为枚举设计的集合类,EnumSet
中的所有元素都必须是指定枚举类型的枚举值。
EnumSet
类结构图如下:
EnumSet
是一个抽象类,无法被实例化,但是可以通过静态方法获取该类的实例。
1 | public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E> |
EnumMap
类结构图如下:
EnumMap
定义如下:
1 | public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> |
EnumSet
保证集合中的元素不重复;EnumMap
中的 key 是 enum 类型,而 value 则可以是任意类型。
关于这两个数据结构的使用方法,大家可以参考 JDK 手册。
赠人玫瑰,手留余香~