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

你可能感兴趣的:(Kotlin:由object和companion object创建的单例模式引发的思考)