把书读薄 | 《设计模式之美》设计模式与范式(创建型-单例模式)(下)

简介: 之前做组内分享写过一篇 《重学设计模式 | 单例模式(Singleton Pattern)》,部分参考了《设计模式之美》,故直接搬运,且对此进行一些内容补充,对应 设计模式与范式:创建型(41-43),单例模式是日常开发中是用得最多的模式~ 二手知识加工难免有所纰漏,感兴趣有时间的可自行查阅原文,谢谢。

② 如何保证克隆安全


而防克隆则是要来到父类**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


就是这么简短,依次点击 ToolsKotlinShow Kotlin BytecodeDecompile 反编译下:


网络异常,图片无法展示
|


呕吼,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() 也会放入其中,所以还是在类加载的时候就完成了实例化。


欢迎有知道真相的的大佬评论区告知解惑,感谢~

相关文章
|
29天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
27 2
|
10天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
18天前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入理解与应用
【10月更文挑战第22天】 在软件开发中,设计模式是解决特定问题的通用解决方案。本文将通过通俗易懂的语言和实例,深入探讨PHP中单例模式的概念、实现方法及其在实际开发中的应用,帮助读者更好地理解和运用这一重要的设计模式。
14 1
|
1月前
|
设计模式 安全 Java
C# 一分钟浅谈:设计模式之单例模式
【10月更文挑战第9天】单例模式是软件开发中最常用的设计模式之一,旨在确保一个类只有一个实例,并提供一个全局访问点。本文介绍了单例模式的基本概念、实现方式(包括饿汉式、懒汉式和使用 `Lazy&lt;T&gt;` 的方法)、常见问题(如多线程和序列化问题)及其解决方案,并通过代码示例详细说明了这些内容。希望本文能帮助你在实际开发中更好地应用单例模式,提高代码质量和可维护性。
29 1
|
24天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
24 0
|
27天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
本教程详细讲解了Kotlin中的单例模式实现,包括饿汉式、懒汉式、双重检查锁、静态内部类及枚举类等方法,适合需要深入了解Kotlin单例模式的开发者。快速学习者可参考“简洁”系列教程。
27 0
|
27天前
|
设计模式 存储 数据库连接
Python编程中的设计模式之美:单例模式的妙用与实现###
本文将深入浅出地探讨Python编程中的一种重要设计模式——单例模式。通过生动的比喻、清晰的逻辑和实用的代码示例,让读者轻松理解单例模式的核心概念、应用场景及如何在Python中高效实现。无论是初学者还是有经验的开发者,都能从中获得启发,提升对设计模式的理解和应用能力。 ###
|
30天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
30天前
|
设计模式 存储 安全
PHP中的设计模式:单例模式的深入解析与实践
在PHP开发中,设计模式是提高代码可维护性、扩展性和重用性的关键技术之一。本文将深入探讨单例模式(Singleton Pattern)的原理、实现方式及其在PHP中的应用,同时通过实例展示如何在具体的项目场景中有效利用单例模式来管理和组织对象,确保全局唯一性的实现和最佳实践。
|
1月前
|
设计模式 传感器 运维
Harmony设计模式-单例模式
Harmony设计模式-单例模式
58 0