Kotlin object单例到底是懒汉式还是饿汉式

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 中 已经没有静态字段 这个说法了!

所有静态内容都必须写在 objectcompanion 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的情况,欢迎留言。

Kotlin 社区的一些讨论:

相关推荐
coderlin_1 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
2501_915918411 小时前
Fiddler中文版全面评测:功能亮点、使用场景与中文网资源整合指南
android·ios·小程序·https·uni-app·iphone·webview
wen's3 小时前
React Native安卓刘海屏适配终极方案:仅需修改 AndroidManifest.xml!
android·xml·react native
编程乐学4 小时前
网络资源模板--基于Android Studio 实现的聊天App
android·android studio·大作业·移动端开发·安卓移动开发·聊天app
没有了遇见6 小时前
Android 通过 SO 库安全存储敏感数据,解决接口劫持问题
android
hsx6666 小时前
使用一个 RecyclerView 构建复杂多类型布局
android
hsx6666 小时前
利用 onMeasure、onLayout、onDraw 创建自定义 View
android
守城小轩6 小时前
Chromium 136 编译指南 - Android 篇:开发工具安装(三)
android·数据库·redis
whysqwhw6 小时前
OkHttp平台抽象机制分析
android
hsx6667 小时前
Android 内存泄漏避坑
android