kotlin中使用了 object
、companion object
关键字用来表示java中的静态成员(类似静态成员)。
在实现双重校验锁单例模式时,我尝试了object
和companion 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
的单例 - 后者是在
ObjectExpression
的companion 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官方文档中有这样一段话描述object
与companion 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
其实问题就是牵涉到类的加载与过程,虚拟机定义了以下六种情况,如果类未被初始化,则会进行初始化:
- 创建类的实例
- 访问类的静态变量(除常量【被final修辞的静态变量】原因:常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(constant variable),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种很有用的优化,但是如果你需要改变final域的值那么每一块用到那个域的代码都需要重新编译。
- 访问类的静态方法
- 反射如(Class.forName("my.xyz.Test"))
- 当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化
- 虚拟机启动时,定义了main()方法的那个类先初始化
那么我们来分析以下上述代码的执行情况:
-
main()
方法Test
类初始化 -
main()
方法第一句:访问Singleton
的getInstance()
静态方法Singleton
类初始化,此时按照代码执行顺序进行静态成员的初始化默认值-
instance
= null -
count1
= 0 -
count2
= 0
-
-
按照代码执行顺序为类的静态成员赋值:
-
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
-
-
main()
方法第二句:访问Singleton
的count1
变量,由于count1
没有赋初始值,所以count1 = 1 -
main()
方法第三局:访问Singleton
的count2
变量,由于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