Kotlin object
单例到底是懒汉式还是饿汉式?
Kotlin 中的
object
,一个关键字就能实现单例。但是它到底是懒汉式还是饿汉式?网上众说纷纭,有人说是饿汉式,有人说是懒汉式。
我查阅了众多文档和资料,实际验证了代码,然而最终的结果却出人意料......
object
的单例写法
Kotlin 中实现单例只需要一个关键字 object
,如下:
kotlin
object Singleton {
fun doSomething() {
println("doing something...")
}
}
使用:
kotlin
Singleton.doSomething()
编译后的字节码
那它是怎么实现单例的呢?可以看编译后的字节码(点击 Tool → Kotlin → Show Kotlin Bytecode):
java
// ================Singleton.class =================
public final class Singleton {
// access flags 0x19
public final static LSingleton; INSTANCE
// invisible
// access flags 0x2
private <init>()V
L0
LINENUMBER 1 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this LSingleton; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x11
public final doSomething()V
...
// access flags 0x8
static <clinit>()V
NEW Singleton
DUP
INVOKESPECIAL Singleton.<init> ()V
PUTSTATIC Singleton.INSTANCE : LSingleton;
RETURN
MAXSTACK = 2
MAXLOCALS = 0
}
反编译后的 Java 代码
点击 Bytecode 面板上的 "Decompile" 按钮,可查看反编译后的 Java 代码如下:
java
public final class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public final void doSomething() {
System.out.println("doing something...");
}
}
关于"饿汉"与"懒汉"的争论
这不就是Java中静态代码块的写法,看到这里,很多人会下结论(如 Kotlin下的5种单例模式):
- 这是 Java 中标准的饿汉式单例写法:使用静态代码块创建单例,线程安全,但类加载时就会初始化,可能造成不必要的资源浪费。
- 甚至还给出了 Kotlin 写法的懒汉式、双重检查版本。
但也有一部分人反驳说(如 重学 Kotlin ------ object,史上最 "快" 单例 ?):
- 不会造成资源浪费,因为 JVM 并不是一启动就加载所有类,只有当使用类时才会触发类加载和初始化。
- 所以这是 懒汉式单例。
而 Kotlin 官方文档 的说法如下,似乎暗指这是懒汉式单例:
java
//对象声明的初始化是线程安全的,并在第一次访问时完成。)
//The initialization of an object declaration is thread-safe and done on first access.
静态代码块、双重检测、类加载机制、饿汉式、懒汉式......这些背过的八股文在此刻混乱交织,曾经明确而清晰的定义,似乎已经无法解释
object
。也许我们该回到技术的本质,重新思考一下饿汉和懒汉的意义。
为什么要有懒汉式?
首先看一下 饿汉式单例(Eager Initialization) :
- 在类加载阶段就直接创建好唯一实例,无论这个实例是否真的被用到。
而 懒汉式(Lazy Initialization) 则是为了解决类加载时就创建对象带来的不必要的资源消耗。
那为什么会有"加载类但不使用类"的情况?都调用了这个类还会不使用这个类?
经过实验,有以下场景会出现:
场景 1:调用类的其他静态字段或静态方法
java
public class Tool {
public static final String NAME = "TOOL";
private static final Tool instance = new Tool();
private Tool() {
System.out.println("Tool 单例被初始化");
}
public static Tool getInstance() {
return instance;
}
}
调用:
java
System.out.println(Tool.NAME);
虽然只是访问了 NAME
,但 JVM 会加载类并执行初始化,从而实例化 Tool
,即使并没有调用 getInstance()
。
场景 2:Class.forName("xxx")
java
public static void main(String[] args) {
System.out.println("🟢 程序开始");
try {
// LazySingleton 是静态内部类实现的懒汉式单例
Class.forName("LazySingleton", true, ClassLoader.getSystemClassLoader());
// EagerSingleton 是饿汉式写法
Class.forName("EagerSingleton", true, ClassLoader.getSystemClassLoader());
// ObjectSingleton 是 Kotlin 的 object 写法
Class.forName("ObjectSingleton", true, ClassLoader.getSystemClassLoader());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
System.out.println("🔴 程序结束");
}
输出:
java
🟢 程序开始
🟥 EagerSingleton 静态代码块执行
🛠 构造函数
Singleton object 被初始化
🔴 程序结束
可以看到 Java 饿汉式 和 Kotlin 的 object 写法都执行了初始化,而懒汉式并没有。
小结
饿汉和懒汉式的真正区别在于:
是否会出现"未真正使用该类却仍创建了实例"的情况。
因为这种情况可能导致意外的或者不必要的初始化,这才是懒汉式存在的根本理由。
Kotlin 中的 object
再回到 Kotlin 中来看看,Kotlin中会有这种加载了却不使用的情况吗:
1. 静态成员访问?
Java 中访问静态成员会触发类加载和初始化。但 Kotlin 中 已经没有静态字段 这个说法了!
所有静态内容都必须写在 object
或 companion object
中,也就避免了"误触初始化"的风险。
这个在 Java 中最容易误触的场景,在 Kotlin 中根本不存在!
2. Class.forName("xxx")
?
确实也能在 Kotlin 中使用,不过一般业务代码很少用这种方式。
而且也可以用不触发初始化的写法:
java
// 第二个参数设置为false表示不初始化
Class.forName("com.xxx.MyClass", false, classLoader)
最终结论
所以弄明白了懒汉式的本质之后,再来看 object
关键字,会发现 Kotlin 时代,已经无所谓懒汉式、饿汉式了,object
单例只有在访问并使用它的时候才会加载并执行初始化。
所以,我的结论是:
- 绝大部分情况下,直接使用
object
关键字即可 object
不支持传参数,这点可以另外讨论- 如果真遇到了"加载类但不想初始化"的场景,用
Class.forName(xxx, false, classLoader)
即可。 - 如果有其他"加载了类,但是并不需要使用这个类"或者不适合用object的情况,欢迎留言。