1 引言

​ 相信如果能看到我这篇博客的小伙伴,肯定都看过Joshua Bloch大神说过的这句话:“单元素的枚举类型已经成为实现Singleton的最佳方法”。其实,第一次读到这句话,我连其中说的单元素指什么都不知道,尴尬。后来,网上看了搜索了好几篇文章,发现基本上都是转载自相同的一篇文章,而我的困惑是为什么要用枚举类型实现单例模式呢”,文章中都说的很笼统,于是决定自己结合Joshua Bloch的《effective java》写一篇总结下,给后来的同学做个参考。

2 什么是单例模式

​ 关于什么是单例模式的定义,我之前的文章中讲过,主要是讲恶汉懒汉、线程安全方面得问题,我就不再重复了,只是做下单例模式的总结。之前文章中实现单例模式三个主要特点:1、构造方法私有化;2、实例化的变量引用私有化;3、获取实例的方法共有。

​ 如果不使用枚举,大家采用的一般都是“双重检查加锁”这种方式,如下,对单例模式还不了解的同学希望先大致看下这种思路,接下来的3.1和3.2都是针对这种实现方式进行探讨,了解过单例模式的同学可以跳过直接看3.1的内容:

public class Singleton {private volatile static Singleton uniqueInstance;private Singleton() {}public static Singleton getInstance() {if (uniqueInstance == null) {synchronized (Singleton.class){if(uniqueInstance == null){//进入区域后,再检查一次,如果仍是null,才创建实例uniqueInstance = new Singleton();}}}return uniqueInstance;}
}

3 为什么要用枚举单例

3.1 私有化构造器并不保险

​ 《effective java》中只简单的提了几句话:“享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要低于这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。”下面我以代码来演示一下,大家就能明白:

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {Singleton s=Singleton.getInstance();Singleton sUsual=Singleton.getInstance();Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();constructor.setAccessible(true);Singleton sReflection=constructor.newInstance();System.out.println(s+"\n"+sUsual+"\n"+sReflection);System.out.println("正常情况下,实例化两个实例是否相同:"+(s==sUsual));System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(s==sReflection));}

输出为:

com.lxp.pattern.singleton.Singleton@1540e19d
com.lxp.pattern.singleton.Singleton@1540e19d
com.lxp.pattern.singleton.Singleton@677327b6
正常情况下,实例化两个实例是否相同:true
通过反射攻击单例模式情况下,实例化两个实例是否相同:false

既然存在反射可以攻击的问题,就需要按照Joshua Bloch做说的,加个异常处理。这里我就不演示了,等会讲到枚举我再演示。

3.2 序列化问题

大家先看下面这个代码:

public class SerSingleton implements Serializable {private volatile static SerSingleton uniqueInstance;private  String content;public String getContent() {return content;}public void setContent(String content) {this.content = content;}private SerSingleton() {}public static SerSingleton getInstance() {if (uniqueInstance == null) {synchronized (SerSingleton.class) {if (uniqueInstance == null) {uniqueInstance = new SerSingleton();}}}return uniqueInstance;}public static void main(String[] args) throws IOException, ClassNotFoundException {SerSingleton s = SerSingleton.getInstance();s.setContent("单例序列化");System.out.println("序列化前读取其中的内容:"+s.getContent());ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));oos.writeObject(s);oos.flush();oos.close();FileInputStream fis = new FileInputStream("SerSingleton.obj");ObjectInputStream ois = new ObjectInputStream(fis);SerSingleton s1 = (SerSingleton)ois.readObject();ois.close();System.out.println(s+"\n"+s1);System.out.println("序列化后读取其中的内容:"+s1.getContent());System.out.println("序列化前后两个是否同一个:"+(s==s1));}}

先猜猜看输出结果:

序列化前读取其中的内容:单例序列化
com.lxp.pattern.singleton.SerSingleton@135fbaa4
com.lxp.pattern.singleton.SerSingleton@58372a00
序列化后读取其中的内容:单例序列化
序列化前后两个是否同一个:false

​ 可以看出,序列化前后两个对象并不想等。为什么会出现这种问题呢?这个讲起来,又可以写一篇博客了,简单来说“任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。”当然,这个问题也是可以解决的,想详细了解的同学可以翻看《effective java》第77条:对于实例控制,枚举类型优于readResolve。

3.3 枚举类详解

3.3.1 枚举单例定义

咱们先来看一下枚举类型单例:

public enum  EnumSingleton {INSTANCE;public EnumSingleton getInstance(){return INSTANCE;}
}

怎么样,是不是觉得好简单,只有这么点代码,其实也没这么简单啦,编译后相当于:

public final class  EnumSingleton extends Enum< EnumSingleton> {public static final  EnumSingleton  ENUMSINGLETON;public static  EnumSingleton[] values();public static  EnumSingleton valueOf(String s);static {};
}

咱们先来验证下会不会避免上述的两个问题,先看下枚举单例的优点,然后再来讲原理。

3.3.2 避免反射攻击

public enum  EnumSingleton {INSTANCE;public EnumSingleton getInstance(){return INSTANCE;}public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {EnumSingleton singleton1=EnumSingleton.INSTANCE;EnumSingleton singleton2=EnumSingleton.INSTANCE;System.out.println("正常情况下,实例化两个实例是否相同:"+(singleton1==singleton2));Constructor<EnumSingleton> constructor= null;constructor = EnumSingleton.class.getDeclaredConstructor();constructor.setAccessible(true);EnumSingleton singleton3= null;singleton3 = constructor.newInstance();System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(singleton1==singleton3));}
}

结果就报异常了:

Exception in thread "main" java.lang.NoSuchMethodException: com.lxp.pattern.singleton.EnumSingleton.<init>()at java.lang.Class.getConstructor0(Class.java:3082)at java.lang.Class.getDeclaredConstructor(Class.java:2178)at com.lxp.pattern.singleton.EnumSingleton.main(EnumSingleton.java:20)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
正常情况下,实例化两个实例是否相同:true

然后debug模式,可以发现是因为EnumSingleton.class.getDeclaredConstructors()获取所有构造器,会发现并没有我们所设置的无参构造器,只有一个参数为(String.class,int.class)构造器,然后看下Enum源码就明白,这两个参数是name和ordial两个属性:

public abstract class Enum<E extends Enum<E>>implements Comparable<E>, Serializable {private final String name;public final String name() {return name;}private final int ordinal;public final int ordinal() {return ordinal;}protected Enum(String name, int ordinal) {this.name = name;this.ordinal = ordinal;}//余下省略

​ 枚举Enum是个抽象类,其实一旦一个类声明为枚举,实际上就是继承了Enum,所以会有(String.class,int.class)的构造器。既然是可以获取到父类Enum的构造器,那你也许会说刚才我的反射是因为自身的类没有无参构造方法才导致的异常,并不能说单例枚举避免了反射攻击。好的,那我们就使用父类Enum的构造器,看看是什么情况:

public enum  EnumSingleton {INSTANCE;public EnumSingleton getInstance(){return INSTANCE;}public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {EnumSingleton singleton1=EnumSingleton.INSTANCE;EnumSingleton singleton2=EnumSingleton.INSTANCE;System.out.println("正常情况下,实例化两个实例是否相同:"+(singleton1==singleton2));Constructor<EnumSingleton> constructor= null;
//        constructor = EnumSingleton.class.getDeclaredConstructor();constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);//其父类的构造器constructor.setAccessible(true);EnumSingleton singleton3= null;//singleton3 = constructor.newInstance();singleton3 = constructor.newInstance("testInstance",66);System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(singleton1==singleton3));}
}

然后咱们看运行结果:

正常情况下,实例化两个实例是否相同:true
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objectsat java.lang.reflect.Constructor.newInstance(Constructor.java:417)at com.lxp.pattern.singleton.EnumSingleton.main(EnumSingleton.java:25)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

​ 继续报异常。之前是因为没有无参构造器,这次拿到了父类的构造器了,只是在执行第17行(我没有复制import等包,所以行号少于我自己运行的代码)时候抛出异常,说是不能够反射,我们看下Constructor类的newInstance方法源码:

@CallerSensitivepublic T newInstance(Object ... initargs)throws InstantiationException, IllegalAccessException,IllegalArgumentException, InvocationTargetException{if (!override) {if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {Class<?> caller = Reflection.getCallerClass();checkAccess(caller, clazz, null, modifiers);}}if ((clazz.getModifiers() & Modifier.ENUM) != 0)throw new IllegalArgumentException("Cannot reflectively create enum objects");ConstructorAccessor ca = constructorAccessor;   // read volatileif (ca == null) {ca = acquireConstructorAccessor();}@SuppressWarnings("unchecked")T inst = (T) ca.newInstance(initargs);return inst;}

请看第12行源码,说明反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。

3.3.3 避免序列化问题

我按照3.2中方式来写,作为对比,方面大家看的更清晰些:

public enum  SerEnumSingleton implements Serializable {INSTANCE;private  String content;public String getContent() {return content;}public void setContent(String content) {this.content = content;}private SerEnumSingleton() {}public static void main(String[] args) throws IOException, ClassNotFoundException {SerEnumSingleton s = SerEnumSingleton.INSTANCE;s.setContent("枚举单例序列化");System.out.println("枚举序列化前读取其中的内容:"+s.getContent());ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));oos.writeObject(s);oos.flush();oos.close();FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");ObjectInputStream ois = new ObjectInputStream(fis);SerEnumSingleton s1 = (SerEnumSingleton)ois.readObject();ois.close();System.out.println(s+"\n"+s1);System.out.println("枚举序列化后读取其中的内容:"+s1.getContent());System.out.println("枚举序列化前后两个是否同一个:"+(s==s1));}
}

运行结果如下:

1 枚举序列化前读取其中的内容:枚举单例序列化
2 INSTANCE
3 INSTANCE
4 枚举序列化后读取其中的内容:枚举单例序列化
5 枚举序列化前后两个是否同一个:true

​ 枚举类是JDK1.5才出现的,那之前的程序员面对反射攻击和序列化问题是怎么解决的呢?其实就是像Enum源码那样解决的,只是现在可以用enum可以使我们代码量变的极其简洁了。至此,相信同学们应该能明白了为什么Joshua Bloch说的“单元素的枚举类型已经成为实现Singleton的最佳方法”了吧,也算解决了我自己的困惑。既然能解决这些问题,还能使代码量变的极其简洁,那我们就有理由选枚举单例模式了。对了,解决序列化问题,要先懂transient和readObject,鉴于我的主要目的不在于此,就不在此写这两个原理了。

Java transient关键字使用可以参考这个:https://www.cnblogs.com/lanxuezaipiao/p/3369962.html

参考:

1、《Effective Java》(第2版):p14-15,p271-274

为什么要用枚举实现单例模式(避免反射、序列化问题)相关推荐

  1. 设计模式-单例模式-注册式单例模式-枚举式单例模式和容器式单例模式在Java中的使用示例

    场景 设计模式-单例模式-饿汉式单例模式.懒汉式单例模式.静态内部类在Java中的使用示例: 设计模式-单例模式-饿汉式单例模式.懒汉式单例模式.静态内部类在Java中的使用示例_霸道流氓气质的博客- ...

  2. 单例模式与反射的攻防之【 道高一尺,魔高一丈 】

    文章目录 饿汉模式 没有保护的饿汉模式(可以被反射破坏) 反射破坏普通饿汉模式 带有保护的饿汉模式(构造函数加锁防反射) 反射尝试破坏带有保护的饿汉模式 懒汉模式 基本的懒汉模式(线程不安全) 基本的 ...

  3. java 枚举与单例模式_今天我们来聊聊单例模式和枚举

    ㊣java编程核心第2版多线程的图书 75.5元 包邮 (需用券) 去购买 > 曾经面试的时候,在说到单例模式的时候,我还是含含糊糊的,我觉得自己懂,但是又在某个结点被卡死了,所以决定写篇文章来 ...

  4. java枚举类及面试题为什么枚举实现单例模式是安全的?

    目录 枚举类 为什么会有枚举类? 枚举类的使用 枚举类的常用方法 枚举的构造 枚举类型能被反射吗? 为什么枚举实现单例模式是安全的? 枚举类的优缺点 枚举类 为什么会有枚举类? 假如说程序中有一些颜色 ...

  5. java 单例模式 泛型_设计模式之架构设计实例(工厂模式、单例模式、反射、泛型等)...

    设计模式, 架构设计实例, 使用到了工厂模式.单例模式.反射.泛型等 项目包结构如下图: 1.bean包 (1)Base.java父类 package test.bean; public class ...

  6. linux 单例模式改密码,Java 利用枚举实现单例模式

    引言 单例模式比较常见的实现方法有懒汉模式,DCL模式公有静态成员等,从Java 1.5版本起,单元素枚举实现单例模式成为最佳的方法. Java枚举 基本用法 枚举的用法比较多,本文主要旨在介绍利用枚 ...

  7. 为什么enum枚举实现单例模式天然线程安全?

    定义枚举时使用 enum 和 class 一样, 是 Java 中的一个关键字. 就像 class 对应一个 Class 类一样, enum 也对应有一个 Enum 类. 通过将定义好的枚举反编译, ...

  8. 枚举是如何实现的?(枚举的线程安全性及序列化问题)

    枚举是如何实现的?(枚举的线程安全性及序列化问题) 枚举是如何保证线程安全的 举例源码 1 public enum t { 2 SPRING,SUMMER,AUTUMN,WINTER; 3 } 反编译 ...

  9. Java中枚举的线程安全性及序列化问题

    转载自  Java中枚举的线程安全性及序列化问题 Java SE5提供了一种新的类型-Java的枚举类型,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序 ...

  10. Java设计模式——为什么要用枚举实现单例模式(避免反射、序列化问题)

    1.序言   相信如果能看到我这篇博客的小伙伴,肯定都看过Joshua Bloch大神说过的这句话:"单元素的枚举类型已经成为实现Singleton的最佳方法".其实,第一次读到这 ...

最新文章

  1. 自定义UISearchBar外观
  2. 贝叶斯定理的实际应用
  3. oracle元数据存储在表空间,[Oracle] dbms_metadata.get_ddl 的使用方法总结
  4. 【网络安全】一次实战中对tp5网站getshell方式的测试
  5. Python配置-virtualenv和conda的区别
  6. 别小看这不起眼的电阻,里面有很多学问!
  7. SpringBoot实用小技巧之动态设置SpringBoot日志级别 1
  8. 「3.4w字」超保姆级教程带你实现Promise的核心功能
  9. linux c 网络事件 通知,深入理解Linux网络技术内幕—通知链
  10. 如何让ul的符号隐藏_如何对文件进行加密?分享一下我对文件进行加密的方法(菜鸟级)...
  11. css 怎么设置盒子水平居中,用一段css实现盒子垂直水平居中方法(8种)-案例
  12. filter在CSS中的效果
  13. 拜山贴(COM+中的连接复用)
  14. IOUtils常用方法的使用
  15. android设备当广告屏使用方法,Android手机与电视无线HDMI同屏器使用教程
  16. 二项分布期望与方差的证明
  17. 计算机sci多少字,一篇sci综述一般写多少字
  18. 有关南怀瑾、朱熹周易断卦法的个人观点
  19. 基于51单片机的555定时器测电容proteus仿真
  20. java输出精度到0.1_【java】浮点数精度问题,为什么0.1===0.1 5+0.1 = 5.1?

热门文章

  1. 单链表遍历_单链表及其遍历实现的基本操作
  2. scala中循环守卫_Scala中的循环
  3. 程序代码错误检测_错误检测代码
  4. client netty 主动发数据_netty案例,netty4.1基础入门篇十一《netty udp通信方式案例Demo》...
  5. vue 修改div宽度_Vue 组件通信方式及其应用场景总结(1.5W字)
  6. python中的数字类型格式与运算,python数字数据类型
  7. 字符串乘一个数_【思维拓展】三位数乘两位数,构造最大积和最小积
  8. node js 开发网站_使用Node JS开发网站
  9. Java Long类shortValue()方法与示例
  10. MySQL 事务的面试题总结