Kotlin笔记 05 - object
object 关键字,有三种迥然不同的语义,分别可以定义:
- 匿名内部类;
- 单例模式;
- 伴生对象。
Kotlin 的设计者认为,这三种语义本质上都是在定义一个类的同时还创建了对象。在这样的情况下,与其分别定义三种不同的关键字,还不如将它们统一成 object 关键字。
object:匿名内部类
1 | image.setOnClickListener(object: View.OnClickListener { |
在 Kotlin 中,匿名内部类还有一个特殊之处,就是我们在使用 object 定义匿名内部类的时候,其实还可以在继承一个抽象类的同时,来实现多个接口。
1 | interface A { |
这种写法,在 Java 当中其实是不被支持的。
object:单例模式
1 | object UserManager { |
反编译对应的 Java 代码:
1 | public final class UserManager { |
Kotlin 编译器会将其转换成静态代码块的单例模式。因为static{}代码块当中的代码,由虚拟机保证它只会被执行一次,因此,它在保证了线程安全的前提下,同时也保证我们的 INSTANCE 只会被初始化一次。
这种方式定义的单例模式,虽然具有简洁的优点,但同时也存在两个缺点。
- 不支持懒加载。这个问题很容易解决,我们在后面会提到。
- 不支持传参构造单例。举个例子,在 Android 开发当中,很多情况下我们都需要用到 Context 作为上下文。另外有的时候,在单例创建时可能也需要 Context 才可以创建,那么如果这时候单纯只有 object 创建的单例,就无法满足需求了。
object:伴生对象
演变过程
先来看看 object 定义单例的一种特殊情况,看看它是如何演变成“伴生对象”的:
1 | class Person { |
反编译成 Java
1 | public final class Person { |
使用“@JvmStatic”,实现类似 Java 静态方法的代码
1 | class Person { |
反编译
1 | public final class Person { |
对于 foo() 方法的调用,不管是 Kotlin 还是 Java,它们的调用方式都会变成一样的:
1 | Person.InnerSingleton.foo() |
上面的静态内部类“InnerSingleton”看起来有点多余,我们平时在 Java 当中写的静态方法,不应该是只有一个层级吗?如何实现?
加一个 companion 关键字
1 | class Person { |
在伴生对象的内部,如果存在“@JvmStatic”修饰的方法或属性,它会被挪到伴生对象外部的类当中,变成静态成员
1 | public final class Person { |
被挪到外部的静态方法 foo(),它最终还是调用了单例 InnerSingleton 的成员方法 foo(),所以它只是做了一层转接而已。
普通的 object 单例,演变出了嵌套的单例;嵌套的单例,演变出了伴生对象。
换个说法:嵌套单例,是 object 单例的一种特殊情况;伴生对象,是嵌套单例的一种特殊情况。
伴生对象的实战应用
工厂模式
1 | // 私有的构造函数,外部无法调用 |
另外 4 种单例模式的写法
借助懒加载委托
1 | object UserManager { |
伴生对象 Double Check
1 | class UserManager private constructor(name: String) { |
定义了一个伴生对象,然后在它的内部,定义了一个 INSTANCE,它是 private 的,这样就保证了它无法直接被外部访问。同时它还被注解“@Volatile”修饰了,这可以保证 INSTANCE 的可见性,而 getInstance() 方法当中的 synchronized,保证了 INSTANCE 的原子性。因此,这种方案还是线程安全的。
以上的实现方式仍然存在一个问题:不同的单例当中,我们必须反复写 Double Check 的逻辑
抽象类模板
1 | // ① ② |
- 注释①:abstract 关键字,代表了我们定义的 BaseSingleton 是一个抽象类。我们以后要实现单例类,就只需要继承这个 BaseSingleton 即可。
- 注释②:in P, out T 是 Kotlin 当中的泛型,P 和 T 分别代表了 getInstance() 的参数类型和返回值类型。注意,这里的 P 和 T,是在具体的单例子类当中才需要去实现的。如果你完全不知道泛型是什么东西,可以先看看泛型的介绍,我们在第 10 讲会详细介绍 Kotlin 泛型。
- 注释③:creator(param: P): T 是 instance 构造器,它是一个抽象方法,需要我们在具体的单例子类当中实现此方法。
- 注释④:creator(param) 是对 instance 构造器的调用。
1 | class PersonManager private constructor(name: String) { |
- 注释①:companion object : BaseSingleton,由于伴生对象本质上还是嵌套类,也就是说,它仍然是一个类,那么它就具备类的特性“继承其他的类”。因此,我们让伴生对象继承 BaseSingleton 这个抽象类。
- 注释②:String, PersonManager,这是我们传入泛型的参数 P、T 对应的实际类型,分别代表了 creator() 的“参数类型”和“返回值类型”。
- 注释③:override fun creator,我们在子类当中实现了 creator() 这个抽象方法。
接口模板(不推荐)
1
2
3
4
5
6
7
8
9
10
11interface ISingleton<P, T> {
// ①
var instance: T?
fun creator(param: P): T
fun getInstance(p: P): T =
instance ?: synchronized(this) {
instance ?: creator(p).also { instance = it }
}
}
缺陷:
- instance 无法使用 private 修饰。 这是接口特性规定的,而这并不符合单例的规范。正常情况下的单例模式,我们内部的 instance 必须是 private 的,这是为了防止它被外部直接修改。
- instance 无法使用 @Volatile 修饰。 这也是受限于接口的特性,这会引发多线程同步的问题。
除了 ISingleton 接口有这样的问题,我们在实现 ISingleton 接口的类当中,也会有类似的问题。
1 | class Singleton private constructor(name: String) { |
- 注释①:@Volatile,这个注解虽然可以在实现的时候添加,但实现方可能会忘记,这会导致隐患。
- 注释②:我们在实现 instance 的时候,仍然无法使用 private 来修饰。
小结
Kotlin 的匿名内部类和 Java 的类似,只不过它多了一个功能:匿名内部类可以在继承一个抽象类的同时还实现多个接口。
object 的单例和伴生对象,这两种语义从表面上看是没有任何联系的。“单例”演变出了“嵌套单例”,而“嵌套单例”演变出了“伴生对象”。
借助 Kotlin 伴生对象这个语法,研究了伴生对象的实战应用,比如可以实现工厂模式、懒加载 + 带参数的单例模式。
单例模式使用场景:
- 如果我们的单例占用内存很小,并且对内存不敏感,不需要传参,直接使用 object 定义的单例即可。
- 如果我们的单例占用内存很小,不需要传参,但它内部的属性会触发消耗资源的网络请求和数据库查询,我们可以使用 object 搭配 by lazy 懒加载。
- 如果我们的工程很简单,只有一两个单例场景,同时我们有懒加载需求,并且 getInstance() 需要传参,我们可以直接手写 Double Check。
- 如果我们的工程规模大,对内存敏感,单例场景比较多,那我们就很有必要使用抽象类模板 BaseSingleton 了。