Java设计模式——为什么要用枚举实现单例模式(避免反射、序列化问题)
1、序言
相信如果能看到我这篇博客的小伙伴,肯定都看过Joshua Bloch大神说过的这句话:“单元素的枚举类型已经成为实现Singleton的最佳方法”。其实,第一次读到这句话,我连其中说的单元素指什么都不知道,尴尬。后来,网上看了搜索了好几篇文章,发现基本上都是转载自相同的一篇文章,而我的困惑是“为什么要用枚举类型实现单例模式呢?”,文章中都说的很笼统,于是决定自己结合Joshua Bloch的《effective java》写一篇总结下,给后来的同学做个参考。
2、什么是单例模式
关于什么是单例模式的定义,我之前的一篇文章《Java设计模式——单例模式的七种写法》中有写过,主要是讲饿汉懒汉、线程安全方面得问题,我就不再重复了,只是做下单例模式的总结。之前文章中实现单例模式三个主要特点: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));}
}
运行结果如下:
枚举序列化前读取其中的内容:枚举单例序列化
INSTANCE
INSTANCE
枚举序列化后读取其中的内容:枚举单例序列化
枚举序列化前后两个是否同一个:true
枚举类是JDK1.5才出现的,那之前的程序员面对反射攻击和序列化问题是怎么解决的呢?其实就是像Enum源码那样解决的,只是现在可以用enum可以使我们代码量变的极其简洁了。至此,相信同学们应该能明白了为什么Joshua Bloch说的“单元素的枚举类型已经成为实现Singleton的最佳方法”了吧,也算解决了我自己的困惑。既然能解决这些问题,还能使代码量变的极其简洁,那我们就有理由选枚举单例模式了。对了,解决序列化问题,要先懂transient和readObject,鉴于我的主要目的不在于此,就不在此写这两个原理了。
Java设计模式——为什么要用枚举实现单例模式(避免反射、序列化问题)相关推荐
- 【Java设计模式 - 创建型模式1】单例模式
java设计模式之单例模式 简介 场景 实现 单例模式的几种实现方式 懒汉式,线程不安全 懒汉式,线程安全 饿汉式 双检锁/双重校验锁(DCL,即 double-checked locking) 登记 ...
- Java设计模式(4 / 23):单例模式
文章目录 单例模式的应用场景 饿汉式单例模式 懒汉式单例模式 改进:synchronized 改进:双重检查锁 改进:静态内部类 破坏单例 用反射破坏单例 用序列化破坏单例 解密 注册式单例模式 枚举 ...
- Java设计模式(方法工厂类、单例模式、代理模式、策略模式、适配器、观察者、装饰类等)
目录 一.简单工厂模式(Factory Method) 二.工厂方法模式 三.抽象工厂模式(Abstract Factory) 3.1 三个工厂模式区别: 四.单例模式(Singleton) 1.饿汉 ...
- Java设计模式及应用场景之《单例模式》
文章目录 一.单例模式定义 二.单例模式的结构和说明 三.懒汉式和饿汉式的实现 1.懒汉式 2.饿汉式 四.懒汉式和饿汉式的优缺点 五.双重检查加锁方式的实现 六.类级内部类方式的实现 七.枚举方式的 ...
- JAVA设计模式是个什么玩意儿_03_单例模式
1. 思想 英文名叫Singleton,可以说是GoF的23种设计模式里最简单的一个. 单例模式:表示一个类只会生成一个唯一的对象. 分为两种方式:懒汉式和饿汉式. 2. 懒汉式实现方式 懒汉式是在你 ...
- 设计模式-单例模式-注册式单例模式-枚举式单例模式和容器式单例模式在Java中的使用示例
场景 设计模式-单例模式-饿汉式单例模式.懒汉式单例模式.静态内部类在Java中的使用示例: 设计模式-单例模式-饿汉式单例模式.懒汉式单例模式.静态内部类在Java中的使用示例_霸道流氓气质的博客- ...
- java/android 设计模式学习笔记(1)--- 单例模式
前段时间公司一些同事在讨论单例模式(我是最渣的一个,都插不上嘴 T__T ),这个模式使用的频率很高,也可能是很多人最熟悉的设计模式,当然单例模式也算是最简单的设计模式之一吧,简单归简单,但是在实际使 ...
- Java设计模式(七大原则和单例模式篇)
Java设计模式Ⅰ 1. Java设计模式的概述 1.1 设计模式的目的及重要性 1.2 设计模式的7大原则 1.2.1 单一职责原则 1.2.2 接口隔离原则 1.2.3 依赖倒置原则 1.2.4 ...
- Java 设计模式之单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一.这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式. 这种模式涉及到一个单一的类,该类负责创建自己的对 ...
最新文章
- 零门槛!手把手教你打造AI应用
- python绘图颜色代码_python matplotlib-颜色代码+ve和-ve值在绘图中
- 网页打开共享目录_你会做Excel文件目录吗?真的太太太太太简单了!
- struts+hibernate+oracle+easyui实现lazyout组件的简单案例——Emp实体类和对应的配置信息
- Inspeckage,安卓动态分析工具
- IOC操作Bean管理XML方式(外部属性文件)
- java多线程实例_多线程&高并发(全网最新:面试题+导图+笔记)面试手稳心不慌...
- 打开网页出现“安全沙箱冲突”的提示
- 广东鸿图:搭建业务报表,摆脱人工计数,工作效率提升150%
- docker swarm英文文档学习-11-上锁你的集群来保护你的加密密钥
- 语义噪声 | 语义网:重新发明轮子,创新者的窘境
- 《颠覆我认知的30篇文章 》阅读笔记(一)
- 普通人如何正确学习人工智能?
- Nat Commun:中国中医科学院黄璐琦院士/首都医科大学高伟教授团队联合解析雷公藤甲素生物合成关键C-14位羟化机制...
- 极路由 刷linux,极路由 刷uboot + openwrt , 以及连接校园网(netkeeper)
- VB如何自动保存_发酵鱼饵与果酸、VB小药的搭配,这才是夏季钓大鱼的必杀配方!...
- SHELL对接国际验证码接口
- text-align 和 align的区别
- iconfont在ionic中的使用(阿里图标库)
- win7 关闭防火墙
热门文章
- SAP HANA会代替BW吗?
- SAP系统上线支持维护制度
- “下沉市场”+“内容生态”,OTA的两道救命题?
- 赢得市值,失去人心,美团觉得划算吗?
- bat php 监控网站,HTML_进程监控实现代码[vbs+bat],运行后会在%windir%\system32\目录 - phpStudy...
- unity 平移图片_Unity 两张背景的切换平移
- redis 依赖_springboot|springboot集成redis缓存
- [VN2020 公开赛]CSRe
- Python 字典初始化dict()和{}
- python global和nonlocal用法解析