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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/555350.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mysql挂载数据卷_记一次生产数据库数据文件进行分区转移

概述由于之前同事没有对磁盘分区做规划&#xff0c;可以看到数据和系统是在同个分区的&#xff0c;没有单独规划一个数据分区给数据库&#xff0c;还有个分区是640G没有用上。下面简单介绍一下mysql数据库数据文件的转移过程。1、新建数据分区篇幅需要&#xff0c;以下从简。。…

java计算一个多边形的重心_2D凸多边形碰撞检测算法(二) - GJK(上)

2D凸多边形碰撞检测算法&#xff08;二&#xff09; - GJK&#xff08;上&#xff09;原理在 Narrow Phase 精细碰撞检测中&#xff0c;除了 SAT &#xff0c;另外一个就是 GJK&#xff08;Gilbert–Johnson–Keerthi&#xff09;算法。它足够高效&#xff0c;且很容易了解它是…

高性能对象存储MinIO学习API使用使用api创建文件夹MinIO工具类

MinIO 是GlusterFS创始人之一Anand Babu Periasamy发布的开源项目&#xff0c;基于Apache V2 license 100% 开放源代码。MinIO采用Golang实现&#xff0c;客户端支持Java、Python、Javacript、Golang语言等。 其设计的主要目标是作为私有云对象存储的标准方案。非常适合于存储…

rmi远程反序列化rce漏洞_Apache Dubbo Provider默认反序列化远程代

背景近日&#xff0c;Apache Dubbo披露了Provider默认反序列化远程代码执行漏洞(CVE-2020-1948)&#xff0c;攻击者可构造恶意请求&#xff0c;从而执行任意代码。具体信息如上图所示。在官方邮件中&#xff0c;漏洞报告者还提供了官方的PoC脚本&#xff0c;感兴趣的读者可以自…

Java非对称加密KeyPairGenerator类

Java加密的常用的加密算法类型有三种 1单向加密&#xff1a; 也就是不可逆的加密&#xff0c;例如MD5,SHA,HMAC 2对称加密&#xff1a; 也就是加密方和解密方利用同一个秘钥对数据进行加密和解密&#xff0c;例如DES&#xff0c;PBE等等 3非对称加密&#xff1a; 非对称加…

操作痕迹包括那些_高级消防设施操作员专题之:走近气体灭火系统

按照《消防设施操作员职业技能标准》的规定&#xff0c;安装有气体灭火系统的单位&#xff0c;应当配置高级消防设施操作员。由于这些单位通常情况下都是消防安全重点单位、火灾高危单位&#xff0c;可以预见&#xff0c;高级消防设施操作员作为消防行业的高技能人才&#xff0…

请求头Content-Type:application/json,java后端如何接收数据

Content-Type的类型 1.application/x-www-form-urlencoded ​ 常用RequestParam(“参数名称”)也可以不写使用springMvc自己根据参数名称自动赋值 2.multipart/form-data ​ 这个和上个差不多吧&#xff0c;如果是multipart类型的文件&#xff0c;记得在后端接收参数是直接…

flutter不支持热更新_Flutter 在安卓上可以实现热更新了

本文由 句号君 授权投稿原文链接&#xff1a;https://blog.csdn.net/qizewei123/article/details/102963340Flutter 官方在 GitHub 上声明是暂时不支持热更新的&#xff0c;但是在 Flutter 的源码里&#xff0c;是有一部分预埋的热更新相关的代码&#xff0c;并且通过一些我们自…

jar包在windows后台运行,通过.bat文件

jar包在windows后台运行.bat 一、IDEA打成jar包 这里不再赘述 二、在windows后台运行jar包 在cmd中可以使用java -jar xxxxx.jar方式运行一个jar文件&#xff0c;这种方法运行一旦关闭该cmd界面就会停止运行。编辑.bat文件&#xff0c;使用javaw方式运行不用担心文件会在不小…

圆周分孔计算公式表图_在圆上分孔怎么计?

2017-05-17为什么中国多制造方孔圆钱?关于方孔圆钱外圆内方的形制&#xff0c;一直是钱币学中颇有争议的问题。有人认为秦始皇迷信方士而采用&#xff1b;有人说是为了穿绳成串&#xff0c;便于携带&#xff1b;有人认为为了减轻铜钱的重量。然而&#xff0c;有三种较为认同的…

java 枚举(enum) 全面解读

枚举类型是单例模式的。你需要实例化一次&#xff0c;然后再整个程序之中就可以调用他的方法和成员变量了。 枚举类型使用单例模式是因为他的值是固定的&#xff0c;不需要发生改变。 简介 枚举是Java1.5引入的新特性&#xff0c;通过关键字enum来定义枚举类。枚举类是一种特殊…

修改表名_面试官:如何批量修改mysql表字段、表、数据库字符集和排序规则

概述目前数据库字符集统一用的utf8&#xff0c;由于项目需要&#xff0c;引进了表情&#xff0c;但是utf8mb5才支持表情字符&#xff0c;所以需统一修改数据库字符集&#xff0c;下面介绍批量修改数据库字符集的办法。修正顺序是字段级别>表级别>库级别。一、批量修改整个…

Linux - nohup - 实现后台运行程序及查看(nohup与)

1. 后台执行 一般运行linux上的程序都是执行 .sh 文件&#xff08;./sh文件&#xff09;&#xff0c;那如果不影响当前CMD窗口的操作&#xff0c;需要后台运行怎么办呢&#xff1f; 这时就需要借助 nohup 和 & 命令来实现。 nohup java -server -Xms128M -Xmx512M -XX:M…

量化评价和质化评价举例_量化评价和质性评价异同点

量化评价和质性评价在理论上有分歧&#xff0c;但它们不是两种对立的方法&#xff0c;在课程评价中是非常必要和不可缺少的。它们的分歧能在课程评价实践中统一起来&#xff0c;互相弥补各自的缺点。1.量化评价的特点 量化评价的优点是逻辑性强&#xff0c;标准化和精确化程度…

Maven命令 install 和 package的区别

Maven命令 install 和 package的区别 Maven是目前十分流行的项目构建工具以及依赖解决工具&#xff0c;其提供的常用指令中有两个很容易引起使用者的疑惑&#xff0c; 那就是 install 和 package &#xff0c; 那么这两个命令到底有啥区别呢&#xff1f; Maven install 安装…

如何重启_消费市场按下重启键,企业该如何提前布局

2020广发卡携手企业和消费者&#xff0c;共同按下重启键&#xff0c;让我们放下包袱&#xff0c;轻松前行。当疫情结束后&#xff0c;你想做什么&#xff1f;也许是去见想见的人&#xff0c;和他一起去吃想吃的美食&#xff1b;也许是约上三五好友&#xff0c;或带着最亲的家人…

Linux中使用netstat命令的基本操作,排查端口号的占用情况

Linux中netstat命令详解 Netstat是控制台命令,是一个监控TCP/IP网络的非常有用的工具&#xff0c;它可以显示路由表、实际的网络连接以及每一个网络接口设备的状态信息。Netstat用于显示与IP、TCP、UDP和ICMP协议相关的统计数据&#xff0c;一般用于检验本机各端口的网络连接情…

与context的关系_Android-Context

一.简介Context 翻译为上下文环境&#xff0c;是一个应用程序环境信息的接口。如果以 Android 系统角度来看可以理解为某一与操作系统的交互的具体场景&#xff0c;比如 Activity 的具体功能&#xff0c;Service 的后台运行等。如果以程序的角度看&#xff0c;Context 是一个抽…

Linux中sudo、su和su -命令的区别

Linux中sudo、su和su -命令的区别小结 我们知道&#xff0c;在Linux下对很多文件进行修改都需要有root&#xff08;管理员&#xff09;权限&#xff0c;比如对/ect/profile等文件的修改。下面这篇文章主要给大家总结介绍了关于Linux中sudo、su和su -命令的区别的相关资料&…

mybatis可以生成不重复的id吗_Mybatis面试题吐血总结

标签&#xff0c;逐一定义数据库列名和对象属性名之间的映射关系。第二种是使用 sql 列的别名功能&#xff0c;将列的别名书写为对象属性名。有了列名与属性名的映射关系后&#xff0c;mybatis 通过反射创建对象&#xff0c;同时使用反射给对象的属性逐一赋值并返回&#xff0c…