Kotlin 静态内部类单例模式的正确实现方式

article/2025/9/16 20:14:00

本篇是对现网上流传的 Kotlin 实现静态内部类单例模式的纠正,为了把原理说清楚,文章前奏可能会有些长,熟悉静态内部类单例模式原理的朋友,可以直接跳转到文章最后,直接看结果即可。

最近在整理基础库的时候,需要一个基础类来存储初始化的数据,例如应用的 Application Context,用户的登录 token 等等信息,这些基本都是应用全局类的信息,在应用的整个生命周期都会用到,因此我将这个基础类设计为单例模式来优化性能。

我知道的单例模式就有6钟,饿汉式、懒汉式、线程安全的懒汉式,volatile + 双重校验锁试,静态内部类式,,枚举式。

我挑哪种来用呢?那肯定是性能最好的呀!

一个单例模式性能的好坏,主要考究其在复杂的生产环境下,还能否保证实例的唯一性

可能会碰到的生产环境有以下几种,如果在以下情况下不能保证实例的唯一,那么该单例模式就是有瑕疵的。

  1. 多线程情况下是否会被多次创建。

  2. 反射调用单例类的构造方法时,能否重新创建实例。

  3. 如果单例类实现了 Serializable 接口,其反序列化时生成的实例是否与序列化时的实例是同一个。

枚举式据说是最安全,性能最好的单例模式,Java 不允许反射调用枚举类的构造方法,对枚举类的序列化过程也做了特殊处理,同时枚举类利用语言特性保证了多线程安全。

不过枚举式我用的非常少,个人不太习惯,我挑了使用最顺手的静态内部类式,这种单例模式我用的最多。

然而静态内部类单例模式是有瑕疵的,静态内部类利用了类的加载机制保证了多线程安全,但其构造方法仍然可以通过反射的方式被外部调用。如果类实现了 Serializable 接口,那么其默认的反序列化过程生成的对象与序列化时的对象也是不同的。

虽然有瑕疵,但都是有解决办法滴。

解决反射调用构造方式时可能会重新生成对象的问题,我们只需要在类的构造方法里添加一个标志位校验就可以解决:

class Common private constructor(){companion object {private var flag = false}// 防止反射破坏单例init {if (!flag) {flag = true} else {throw Throwable("SingleTon is being attacked.")}}
}

而反序列化的问题,可以通过在类中声明 readResolve()方法 ,在反序列化返回对象前替换成我们的单例解决。

fun readResolve(): Any {return Common.getInstance()
}

更多解决单例模式瑕疵问题的知识,这里就不展开啦,有兴趣的同学可以自己去网上冲浪一下。

考虑到我的单例类使用场景,不需要实现序列化接口,因此只需要解决反射调用的问题就行了。

瑕疵解决了,那么现在就来给我的单例类实现一个静态内部类的单例模式吧~

静态内部类单例模式,作为一个 Java 老手,用 Java 写我可谓是信手拈来,直接用文档编辑器莽着敲,都不带看的:

// java 实现静态内部类单例
class Common {private static boolean flag = false;// 解决反射调用问题private Common() {if (!flag) {flag = true} else {throw new Throwable("SingleTon is being attachked.")}}public static final Common getInstance() {return CommonSingleTonHolder.sInstance}private static final class CommonSingleTonHolder {private static Common sInstance = new Common();}
}

抬手之间还解决了反射调用的问题~

为什么说静态内部类的单例模式是线程安全的呢? 这里要简单提一下类的加载机制了

简单的说,类加载过程包括五个过程:加载、校验、准备、解析、初始化。

  1. 加载:虚拟机通过类的全限定类名获取类的二进制字节流,通过这个字节流代表的静态存储结构转换为方法区中的运行时数据存储结构,并在堆中生成一个class对象,来作为访问这个运行时数据存储结构的入口。
  2. 校验:虚拟机校验类的字节流文件是否符合虚拟机的规范,是否会对虚拟机的安全造成影响。主要包括文件格式校验,元数据校验,字节码校验,符号引用校验。
  3. 准备:为类中的静态变量分配堆内存,并将其初始化为默认值
  4. 解析:将Class文件中的符号引用转化为指向内存的直接引用。
  5. 初始化:执行类构造器的clinit方法,clinit方法里面包含类的静态变量的赋值操作和静态语句块。

类的静态变量会在准备阶段分配内存,并被初始化为默认值。在初始化阶段,会执行类的<clinit>()方法,执行静态变量的赋值操作和静态语句块。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。因此说静态内部类的单例在多线程访问时也是线程安全的。

静态内部类单例模式也是一种懒汉模式,只有在执行 Common.getInstance()时才会去加载 CommonSingleTonHolder 类,为 sInstance 静态属性初始化。

Kotlin 实现静态内部类单例模式

前文介绍了这么多都是有关于 Java 的实现方式,用 Kotlin 怎么实现呢?

本着伸手就有,决不自己动手的原则,网上搜索一下,有能直接拿来用的,直接 copy copy~

1_kotlin_search.png

现网上传的 Kotlin 静态内部类单例模式
// google 第一页搜索内容,截止到 2022 年 1 月 14 日
class SingletonDemo private constructor() {companion object {val instance = SingletonHolder.holder}private object SingletonHolder {val holder = SingletonDemo()}
}

搜索完点击第一条,返回了上面的搜索结果。

喔,看着挺简单的,看看有没其他实现,陆续点击第二条第三条搜索内容,发现一整页的静态内部类单例都是返回这样的结果。

大家都一样,得,就你了。ctrl + c,ctrl + v,改个类名,nice,搞定。

class Common private constructor(){// 防止反射破坏单例init {if (!flag) {flag = true} else {throw Throwable("SingleTon is being attacked.")}}companion object {private var flag = false// 单例val instance = CommonSingletonHolder.holder}/*** 静态内部类单例*/private object CommonSingletonHolder {val holder = Common()}}

通过 Common.instance就可以访问我们的单例了。完美完美,对比一下 Kotlin 与 Java 的实现方式,非常像!

把上面的 Kotlin 代码反编译成 Java,再看,我却觉得有些不对劲。

为了让 Kotlin 反编译生成的代码与 Java 原生的调用方式尽量相同,反编译前,我给 instance 属性加了 @JvmField 注解。

companion object {@JvmFieldval instance = CommonSingletonHolder.holder
}

然后再反编译成 Java 代码,省去与分析无关的代码后:

public final class Common {@JvmField@NotNull// 单例public static final Common instance;private Common() {}// 单例的赋值static {instance = Common.CommonSingletonHolder.INSTANCE.getHolder();}// $FF: synthetic methodpublic Common(DefaultConstructorMarker $constructor_marker) {this();}private static final class CommonSingletonHolder {@NotNullprivate static final Common holder;@NotNullpublic static final Common.CommonSingletonHolder INSTANCE;@NotNullpublic final Common getHolder() {return holder;}static {Common.CommonSingletonHolder var0 = new Common.CommonSingletonHolder();INSTANCE = var0;holder = new Common((DefaultConstructorMarker)null);}}...
}

好像不太对喔!仔细看单例的赋值那一块代码,它是在 static 静态代码块里进行初始赋值的喔。

前文说了,静态内部类本质上也是一种懒汉单例模式,如果 instance 在 静态代码块被初始化,那么 instance 就会在 Common 类加载的过程中就完成初始化,那本质上,它不就变成一种饿汉单例模式了吗?

写个 test case 验证一下 instance 是在什么时候初始化的:

class Common private constructor(){init {JLog.d("Common", "init Common.")}companion object {private var flag = false@JvmFieldval instance = CommonSingletonHolder.holderfun test() {JLog.d("Common", "Common test called.")}}/*** 静态内部类单例*/private object CommonSingletonHolder {val holder = Common()}}// 执行 test() 方法
Common.test()

调用 Common.test() 跑跑看,如果是懒汉模式,那么只要Common没有执行到 CommonSingletonHolder.holder 就不会触发 CommonSingletonHolder 类的加载,更不会走到 val holder = Common() 完成 instance 的初始化。

2022-01-14 14:29:11.330 3500-3500/com.jamgu.common D/Common: init Common.
2022-01-14 14:29:11.330 3500-3500/com.jamgu.common D/Common: Common test called.

Bingo!日志首先打印了 init Common. Instance 在 Common 类加载的时候就被初始化了!,我们实现的居然是一种静态内部类的饿汉模式

啊这,饿汉模式和静态内部类模式都沾点的单例模式,应该叫什么模式??饿静式?听着好像也还说得过去。。

哈哈,开个玩笑,言归正传回来,我们实现的代码既不是静态内部类模式,也不是饿汉模式,那应该怎么修改让它符合一个正宗的静态内部类模式呢?

Easy,类的静态属性和静态代码块,会在类加载的初始化阶段赋值和执行,但类的静态方法不会呀,把单例作为静态方法的返回值就可以完美地让单例懒加载了。

companion object {@JvmStatic// 修改此处fun getInstance() = CommonSingletonHolder.holderfun test() {JLog.d("Common", "Common test called.")}
}

再执行 Common.test(),看看结果:

2022-01-14 14:46:34.914 3862-3862/com.jamgu.common D/Common: Common test called.

没有执行单例的初始化,搞定~

Kotlin 静态内部类单例模式的正确实现方式

最后,一个用 Kotlin 实现的,安全的静态内部类单例模式就崭新出炉了。

class Common private constructor(){// 防止反射破坏单例init {if (!flag) {flag = true} else {throw Throwable("SingleTon is being attacked.")}}companion object {private var flag = false@JvmStaticfun getInstance() = CommonSingletonHolder.holder}/*** 静态内部类单例*/private object CommonSingletonHolder {val holder = Common()}}

现网上传的静态内部类版本不准确噢,仅需要做一些修改,就可以实现真正的静态内部类单例模式了~

@JvmField
val instance = CommonSingletonHolder.holder
// 改成 ----->>>>>
@JvmStatic
fun getInstance() = CommonSingletonHolder.holder

兄dei,如果觉得我写的还不错,麻烦帮个忙呗 😃

  1. 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#.#)
  2. 不用点收藏,诶别点啊,你怎么点了?这多不好意思!
  3. 噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!

拜托拜托,谢谢各位同学!


http://chatgpt.dhexx.cn/article/gQmr3yB9.shtml

相关文章

设计模式3:单例模式:静态内部类单例模式

单例模式最简单的写法就是静态内部类单例模式&#xff0c;如下&#xff1a; public class Manager {private static class ManagerHolder {private static Manager instance new Manager();}private Manager() {}public static Manager getInstance() {return ManagerHolder.i…

单例模式详解

微信搜索【程序员囧辉】&#xff0c;关注这个坚持分享技术干货的程序员。 概述 单例模式&#xff0c;是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中&#xff0c;使用单例模式的类只有一个对象实例。 单例应用场景 Wi…

MATLAB中的结构体数组(struct)学习笔记

不要失却热情&#xff0c;不要丢掉冠军的心&#xff01; MALAB中的结构体&#xff08;struct&#xff09;数组学习笔记 前言1. 版本2. 关键词 一、Struct结构体数组概述二、Struct结构体数组基本用法1. 结构体的创建2. 结构体中的筛选操作 前言 MATLAB中结构体数组基本用法笔…

MATLAB学习——结构体类型

前言 MATLAB 中的数据类型主要包括数值类型、 逻辑类型、 字符串、函数句柄、 结构体和单元数组类型。 这6种基本的数据类型都是按照数组形式存储和操作的。 一、结构体类型 MATLAB中的结构体与C语言中的结构体类似&#xff0c; 一个结构体可以通过字段存储多个不同类型的数…

Matlab将结构体struct字段内的数据转化到矩阵中

假设structure1,为一结构体&#xff0c;structure1.name为100个字符串 怎么将这些字符串不用循环一次性赋值到矩阵A?? Astructure1.name 为什么只是将第一个赋值过去&#xff1f; 答案是可以使用cat函数&#xff1a; 可以用cat函数&#xff0c; A cat(1,structur1.name)是…

Matlab:结构体数组

Matlab:结构体数组 创建标量结构体访问字段中的值对非标量结构体数组进行索引当您有要按名称组织的数据时,可以使用结构体来存储这些数据。结构体将数据存储在名为字段的容器中,然后您可以按指定的名称访问这些字段。使用圆点表示法创建、分配和访问结构体字段中的数据。如果…

Matlab-结构数组

1 认识结构数组 结构也是一种数据类型&#xff0c;它的每一个元素都有一个名字。称结构中的元素为域。 类似与C语言中的结构体。 2 创建 两种方法&#xff1a; &#xff08;1&#xff09;用赋值语句创建 &#xff08;2&#xff09;用函数 struct 函数进行创建 2.1 赋值语…

Matlab遇到结构体内容引用自非结构体数组对象

原因&#xff1a; 未初始化下一级结构体的值&#xff0c;如果直接调用就会报错。 举例&#xff1a; a.p[]; a.p.x解决方法&#xff1a; 需要先初始化&#xff0c;再调用 a.p.x[] or a.p.x0

Matlab:结构体Struct

Matlab中创建一个结构体数组的方式有两种&#xff0c;分别为直接引用方式和使用struct函数。 1、使用直接引用方式创建结构体 与建立数值型数组一样&#xff0c;建立新struct对象不需要事先申明&#xff0c;可以直接引用&#xff0c;而且可以动态扩充。比如建立一个复数变量x…

Matlab 结构体(struct)使用

转自http://blog.sina.com.cn/s/blog_468651400100c6c0.html 要在MALTAB中实现比较复杂的编程&#xff0c;就不能不用struct类型。而且在 MATLAB中实现struct比C中更为方便。 4. 3.1 结构数组的创建 MATLAB提供了两种定义结构的方式&#xff1a;直接应用和使用struct函数。 1. …

MATALB-结构体

结构体 结构体的生成一、 直接输入二、使用结构体生成函数struct 结构体的操作添加成员变量删除成员变量调用成员变量 结构体是另一种可以将不同类型数据组合在一起的数据类型。 MATLAB结构体变量类似于C语言结构体变量,且比C语音更加直观。 结构体与单元数组的区别为,结构体…

matlab学习-结构体变量

matlab结构体学习 matlab结构体学习 matlab结构体与C语言数据结构结构体或java语言类的定义有异曲同工之妙&#xff0c;其主要知识点如下 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 matlab结构体学习前言一…

matlab-结构体struct

在前面&#xff0c;有用到一个cell2struct&#xff08;&#xff09;函数&#xff0c;cell是单元数组&#xff0c;而struct就是结构体。 1.建立结构体&#xff0c;在matlab中建立结构体有两种方式&#xff0c; eg&#xff1a;第一种为直接赋值 >> stu(1).namezhangsan;…

matlab中结构体使用方法

转自 http://hi.baidu.com/dess2211/blog/item/bb9b80185a7f940334fa417c.html matlab中使用结构体 2008-01-15 14:23 结构(struct)数组 要在MALTAB中实现比较复杂的编程&#xff0c;就不能不用struct类型。而且在 MATLAB中实现struct比C中更为方便。 4. 3.1 结构数组的创建 MA…

Matlab中结构体struct创建和使用

在项目上遇见了调用api接口&#xff0c;接口返回的信息为struct&#xff0c;故探讨一下matlab的struct结构体 1、struct结构体创建 创建结构体数组有两种方式&#xff0c;分别为直接创建和使用struct函数 1.1 直接创建 直接定义字段&#xff0c;像使用一般matlab变量一样&…

matlab 结构体

在前面&#xff0c;有用到一个cell2struct&#xff08;&#xff09;函数&#xff0c;cell是单元数组&#xff0c;而struct就是结构体。 1.建立结构体&#xff0c;在matlab中建立结构体有两种方式&#xff0c; eg&#xff1a;第一种为直接赋值 >> stu(1).namezhangsan; …

单片机实例4——广告灯的左移右移(硬件电路图+汇编程序+C语言程序)

4&#xff0e; 广告灯的左移右移 1&#xff0e; 实验任务 做单一灯的左移右移&#xff0c;硬件电路如图4.4.1所示&#xff0c;八个发光二极管L1&#xff0d;L8分别接在单片机的P1.0&#xff0d;P1.7接口上&#xff0c;输出“0”时&#xff0c;发光二极管亮&#xff0c;开始时P…

蓝桥杯-左移右移(2022国赛)

蓝桥杯-左移右移 1、问题描述2、解题思路与代码实现2.1 方法一&#xff1a;使用LinkedList双向链表实现(50%)2.2 方法二&#xff1a;使用HashMap左右临界值实现(100%) 1、问题描述 小蓝有一个长度为 N 的数组, 初始时从左到右依次是 1,2,3,…N 。 之后小蓝对这个数组进行了 M 次…

第十三届蓝桥杯大赛软件类决赛Java大学B组C题——左移右移

【问题描述】 小蓝有一个长度为 N 的数组&#xff0c;初始时从左到右依次是 1, 2, 3, . . . N。 之后小蓝对这个数组进行了 M 次操作&#xff0c;每次操作可能是以下 2 种之一&#xff1a; 左移 x&#xff0c;即把 x 移动到最左边。右移 x&#xff0c;即把 x 移动到最右边。 …

算术逻辑左移右移

左移&#xff08;<<&#xff09;&#xff1a; 算术左移和逻辑左移的规则相同&#xff0c;都是整体左移&#xff0c;高位舍去&#xff0c;低位补零。 而且我们知道在计算机内部中的数都是用补码来进行存储的&#xff0c;这样便可以把减法运算也转化为加法运算。 1110的原码…