② 如何保证克隆安全
而防克隆则是要来到父类**Enum
类**中,直接实现了clone()函数:
调用此函数直接返回 CloneNotSupportedException
异常。
③ 如何保证反射安全
将反射部分代码中的Singleton改成SingletonEnum,接着运行下,抛出下述异常
在获取构造函数时抛出的异常,没有此构造方法,呕吼,看回jad反编译的代码:
这里使用的不是无参构造方法,而是有两个参数,改下反射代码,往getDeclaredConstructor()传入这个两个参数:
再次运行,还是报异常:
定位到 Constructor类
的 newInstance()
反射通过newInstance()创建对象时,会检查该类是否**ENUM
**修饰,是则抛出异常,反射失败。
④ 如何保证序列化安全
Java规范中规定:每一个枚举类型及其定义的枚举变量在JVM中都是唯一的。
因此在枚举类型的序列化和反序列化上,Java做了特殊的规定:
- 序列化时 → 仅仅将枚举对象的name属性输出到结果中;
- 反序列化时 → 通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。
定位到Enum类的valueOf()方法:
调用enumType
(Class对象的引用)的enumConstantDirectory
获取一个Map集合,集合中存放的键值对:
枚举name : 枚举示例变量
根据name即可拿到枚举实例,所以枚举单例序列化并不会重新创建新实例!
0x5、Kotlin中的单例
说完Java的单例,顺带提提Kotlin中的单例,使用一行代码即可创建安全单例:
object KotlinSingleto
就是这么简短,依次点击 Tools
→ Kotlin
→ Show Kotlin Bytecode
→ Decompile
反编译下:
呕吼,static静态代码块,饿汉式变种,线程安全,Android项目不考虑其他三个问题的话,可以大胆放心使用。
0x6、单例存在哪些问题?
- 对OOP特性支持不友好 (将某个类设计成单例类,意味着放弃继承和多态两个特性);
- 会隐藏类之间的依赖关系 (单例不需要显式创建、依赖参数传递,在函数中直接调用);
- 对代码的扩展性不友好 (某一天,需要在代码中创建两个或多个实例,代码需要较大改动);
- 对代码的可测试性不友好 (单例类依赖较重外部资源,mock方式无法替换,持有成员变量相当于全局变量);
- 不支持有参数的构造函数 (解决思路:创建实例后再调init()函数传递参数、参数放到getInstance()方法中、将参数放到另一个全局变量中,里面的值可以静态常量定义或从配置文件中加载得到)
0x7、单例的替代方案
为了保证全局唯一,除了使用单例外,还可以用 静态方法
来实现,不过实际上它并不能解决上面提到的问题,而且没有懒加载。
只能另辟蹊径,用其他方式来保证类对象的全局唯一性:如工厂模式、IOC容器等(后面会讲),还可以通过程序员自己来保证(写代码时保证不要创建两个类对象)。
另外,如果单例类没有后续的扩展需求,且不依赖外部系统,设计单例类就没太大问题,对于一些全局的类,到处new,类之间传来传去,不如直接做成单例类,使用起来简洁方便。
0x8、如何实现一个多例
单例
指的是:一个类只能创建一个对象,对应的 多例
则是:
一个类可以创建多个对象,但个数是有限的,比如只能创建5个对象。
实现方式也比较简单,通过一个Map来存储对象类型及对象间的对应关系,来控制对象的个数。示例如下:
import java.util.HashMap; import java.util.Map; import java.util.Random; public class BikeServer { private final long bikeNo; // 共享单车编号 private final String address; // 共享单车地址 private static final int BIKE_COUNT = 5; // 单车数量 private static final Map<Long, BikeServer> bikeInstances = new HashMap<>(); // 单车实例集合 // 私有化构造方法 private BikeServer(long bikeNo, String address) { this.bikeNo = bikeNo; this.address = address; } // 静态代码块中初始化实例 static { bikeInstances.put(1L, new BikeServer(1L, "罗湖区")); bikeInstances.put(2L, new BikeServer(2L, "南山区")); bikeInstances.put(3L, new BikeServer(3L, "福田区")); bikeInstances.put(4L, new BikeServer(4L, "宝安区")); bikeInstances.put(5L, new BikeServer(5L, "龙华区")); } // 根据编号获取单车实例 public static BikeServer getInstance(long bikeNo) { return bikeInstances.get(bikeNo); } // 随机获取单车实例 public static BikeServer getRandomInstance() { Random r = new Random(); return bikeInstances.get(r.nextInt(BIKE_COUNT) + 1L); } @Override public String toString() { return "BikeServer{" + "bikeNo=" + bikeNo + ", address='" + address + '\'' + '}'; } }
调用下:
public class BikeTest { public static void main(String[] args) { System.out.println(BikeServer.getInstance(2L).toString()); System.out.println(BikeServer.getRandomInstance().toString()); } } // 输出结果: BikeServer{bikeNo=2, address='南山区'} BikeServer{bikeNo=5, address='龙华区'}
0x9、如何实现集群环境下的单例
上面介绍的单例、多例都是进程内唯一、进程间唯一,即不适用于多进程(集群),如果想实现集群环境下的单例:
要把单例对象序列化存储到外部共享存储区(如文件),进程用到单例对象时,先将它此从共享存储区将它读取内存,并反序列化为对象,然后再使用,使用完还需要把它序列化存储回外部共享存储区。
为了保证任何时刻进程中都只有一份对象存在,进程获取到对象后,需要对对象加锁,避免其他进程再获取,使用完后,还需要显式地将对象从内存中删除,并缩放对单例对象的加锁。
0xa、未解疑惑:饿汉式真的没有懒加载吗?
之前在群里看到有人发了这篇文章 《到底是用"静态类"还是单例》,其中说到:饿汉式本身就是延迟加载的,并附解释:
而在别的地方我又看到了:虚拟机规范严格规定了有且只有以下5种情况立即对类进行初始化,其中一条:
遇到new, getstatic, putstatic, invokestatic这4条字节码指令时,如果类没有进行过初始化,就需要先触发其初始化。
而初始化阶段是执行类构造器<clinit>() 方法的过程 → 由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生。所以 private static Singleton instance = new Singleton() 也会放入其中,所以还是在类加载的时候就完成了实例化。
欢迎有知道真相的的大佬评论区告知解惑,感谢~