Kotlin:由object和companion object创建的单例模式引发的思考

kotlin中使用了 objectcompanion object 关键字用来表示java中的静态成员(类似静态成员)。
在实现双重校验锁单例模式时,我尝试了objectcompanion object,在网上想查询这两者的单例有什么区别,但好像也没查到什么资料。
先贴单例代码:

/**
 * kotlin双重校验锁单例模式
 */
object Singleton {

  @Volatile
  private var instance: ObjectExpression? = null

  fun getInstance() = instance ?: synchronized(this) {
    instance ?: ObjectExpression().apply {
        instance = this
    }
  }
}

class ObjectExpression {

  companion object {
    @Volatile
    private var instance: ObjectExpression? = null

    fun getInstance() = instance ?: synchronized(this) {
        instance ?: ObjectExpression().apply {
            instance = this
        }
    }
  }
}
  • 前者是在object中声明ObjectExpression的单例
  • 后者是在ObjectExpressioncompanion object声明的单例

反编译为java看看两者区别:

public final class Singleton {
  private static volatile ObjectExpression instance;
  public static final Singleton INSTANCE;

  @NotNull
  public final ObjectExpression getInstance() {
    ObjectExpression var10000 = instance;
    if (instance == null) {
     synchronized(this){}

     ObjectExpression var3;
     try {
        var10000 = instance;
        if (instance == null) {
           ObjectExpression var2 = new ObjectExpression();
           instance = var2;
           var10000 = var2;
        }

        var3 = var10000;
     } finally {
        ;
     }

     var10000 = var3;
    }

    return var10000;
  }
  //通过静态代码块生成INSTANCE实例
  static {
    Singleton var0 = new Singleton();
    INSTANCE = var0;
   }
}

public final class ObjectExpression {
  private static volatile ObjectExpression instance;
  //相应类加载时生成Companion对象
  public static final ObjectExpression.Companion Companion = new ObjectExpression.Companion((DefaultConstructorMarker)null);

  public static final class Companion {
    @NotNull
    public final ObjectExpression getInstance() {
     ObjectExpression var10000 = ObjectExpression.instance;
     if (var10000 == null) {
        synchronized(this){}

        ObjectExpression var3;
        try {
           var10000 = ObjectExpression.instance;
           if (var10000 == null) {
              ObjectExpression var2 = new ObjectExpression();
              ObjectExpression.instance = var2;
              var10000 = var2;
           }

           var3 = var10000;
        } finally {
           ;
        }

        var10000 = var3;
      }

      return var10000;
    }

  private Companion() {
  }

  // $FF: synthetic method
  public Companion(DefaultConstructorMarker $constructor_marker) {
     this();
  }
 }
}

实际调用的时候:

Singleton.getInstance()
ObjectExpression.getInstance()

反编译为java看看两者的区别:

  Singleton.INSTANCE.getInstance();
  ObjectExpression.Companion.getInstance();
  • 前者调用的是Singleton当中的INSTANCE
  • 后者调用的是ObjectExpression当中给的Companion

所以object内部是使用静态代码块来进行INSTANCE的初始化,而companion object内部是使用静态变量来进行Companion的初始化。
不过kotlin官方文档中有这样一段话描述objectcompanion object

对象表达式和对象声明之间的语义差异
对象表达式和对象声明之间有一个重要的语义差别:

  • 对象表达式是在使用他们的地方立即执行(及初始化)的;
  • 对象声明是在第一次被访问到时延迟初始化的;
  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配。

经过一晚上的研究,终于明白了官方文档的解释,同时也发现自己基础知识的欠缺,路漫漫呀...
那么就来说一说官方文档所说的第二句话:

  • 对象声明是在第一次被访问到时延迟初始化的;

其实指的意思是由于object是一个独立的类(可以通过反编译java查看),因此object当中的方法第一次访问时,此时object类加载,静态代码块初始化,INSTANCE完成创建。

而第三句话:

  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配。

指的意思是companion object的外部类加载时,由于companion是静态变量,外部类加载的时候,会进行初始化,所以等同于外部类的静态成员。

而自己知识的欠缺体现在关于类加载的时机与过程上,贴几个问题,思考一下输出结果是什么:

public class Singleton {

  private static Singleton instance = new Singleton();
  public static int count1;
  public static int count2 = 0;
  private Singleton(){
    count1 ++;
    count2 ++;
  }

  public static Singleton getInstance(){
    return instance;
  }
}

public class Test {
  public static void main(String[] args){
    Singleton singleton = Singleton.getInstance();
    System.out.println("count1 = " + Singleton.count1);
    System.out.println("count2 = " + Singleton.count2);
  }
}

count1 = 1
count2 = 1

错×

正确答案是:

count1 = 1
count2 = 0

其实问题就是牵涉到类的加载与过程,虚拟机定义了以下六种情况,如果类未被初始化,则会进行初始化:

  1. 创建类的实例
  2. 访问类的静态变量(除常量【被final修辞的静态变量】原因:常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(constant variable),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种很有用的优化,但是如果你需要改变final域的值那么每一块用到那个域的代码都需要重新编译。
  3. 访问类的静态方法
  4. 反射如(Class.forName("my.xyz.Test"))
  5. 当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化
  6. 虚拟机启动时,定义了main()方法的那个类先初始化

那么我们来分析以下上述代码的执行情况:

  1. main()方法 Test类初始化
  2. main()方法第一句:访问SingletongetInstance()静态方法 Singleton类初始化,此时按照代码执行顺序进行静态成员的初始化默认值
    • instance = null
    • count1 = 0
    • count2 = 0
  3. 按照代码执行顺序为类的静态成员赋值
    • private static Singleton instance = new Singleton(); instance调用Singleton的构造方法,调用构造方法后 count1 = 1,count2 = 1
    • public static int count1; count1没有进行赋值操作,所以count1 = 1
    • public static int count2 = 0; count2进行赋值操作,所以count2 = 0
  4. main()方法第二句:访问Singletoncount1变量,由于count1没有赋初始值,所以count1 = 1
  5. main()方法第三局:访问Singletoncount2变量,由于count2赋了初始值 0,所以count2 = 0

所以如果我们把Singleton代码执行顺序变化一下:

public class Singleton {

  public static int count1;
  public static int count2 = 0;
  private static Singleton instance = new Singleton();

  private Singleton() {
    count1++;
    count2++;
  }

  public static Singleton getInstance() {
    return instance;
  }

}

那么此时输出结果就为:

count1 = 1
count2 = 1

如果改为如下代码,那么运行情况又是怎样:

public class Singleton {

  Singleton(){
    System.out.println("Singleton construct");
  }

  static {
    System.out.println("Singleton static block");
  }

  public static final int COUNT = 1;

}

public class Test {
  public static void main(String[] args) {
    System.out.println("count = " + Singleton.COUNT);
  }
}

运行结果为:

count = 1

由于常量在编译阶段会存入相应类的常量池当中,所以在实际调用中Singleton.COUNT并没有直接引用到Singleton类,因此不会进行Singleton类的初始化,所以输出结果为 count = 1

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,966评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,170评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,909评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,959评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,851评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,583评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,956评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,590评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,878评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,892评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,719评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,501评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,957评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,124评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,440评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,003评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,211评论 2 339

推荐阅读更多精彩内容