object 关键字,有三种迥然不同的语义,分别可以定义:

  • 匿名内部类;
  • 单例模式;
  • 伴生对象。

Kotlin 的设计者认为,这三种语义本质上都是在定义一个类的同时还创建了对象。在这样的情况下,与其分别定义三种不同的关键字,还不如将它们统一成 object 关键字。

object:匿名内部类

1
2
3
4
5
image.setOnClickListener(object: View.OnClickListener {
override fun onClick(v: View?) {
gotoPreview()
}
})

在 Kotlin 中,匿名内部类还有一个特殊之处,就是我们在使用 object 定义匿名内部类的时候,其实还可以在继承一个抽象类的同时,来实现多个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface A {
fun funA()
}

interface B {
fun funB()
}

abstract class Man {
abstract fun findMan()
}

fun main() {
// 这个匿名内部类,在继承了Man类的同时,还实现了A、B两个接口
val item = object : Man(), A, B{
override fun funA() {
// do something
}
override fun funB() {
// do something
}
override fun findMan() {
// do something
}
}
}

这种写法,在 Java 当中其实是不被支持的。

object:单例模式

1
2
3
object UserManager {
fun login() {}
}

反编译对应的 Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public final class UserManager {

public static final UserManager INSTANCE;

static {
UserManager var0 = new UserManager();
INSTANCE = var0;
}

private UserManager() {}

public final void login() {}
}

Kotlin 编译器会将其转换成静态代码块的单例模式。因为static{}代码块当中的代码,由虚拟机保证它只会被执行一次,因此,它在保证了线程安全的前提下,同时也保证我们的 INSTANCE 只会被初始化一次。
这种方式定义的单例模式,虽然具有简洁的优点,但同时也存在两个缺点。

  • 不支持懒加载。这个问题很容易解决,我们在后面会提到。
  • 不支持传参构造单例。举个例子,在 Android 开发当中,很多情况下我们都需要用到 Context 作为上下文。另外有的时候,在单例创建时可能也需要 Context 才可以创建,那么如果这时候单纯只有 object 创建的单例,就无法满足需求了。

object:伴生对象

演变过程

先来看看 object 定义单例的一种特殊情况,看看它是如何演变成“伴生对象”的:

1
2
3
4
5
class Person {
object InnerSingleton {
fun foo() {}
}
}

反编译成 Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class Person {
public static final class InnerSingleton {

public static final Person.InnerSingleton INSTANCE;

public final void foo() {}

private InnerSingleton() {}

static {
Person.InnerSingleton var0 = new Person.InnerSingleton();
INSTANCE = var0;
}
}
}

使用“@JvmStatic”,实现类似 Java 静态方法的代码

1
2
3
4
5
6
class Person {
object InnerSingleton {
@JvmStatic
fun foo() {}
}
}

反编译

1
2
3
4
5
6
public final class Person {
public static final class InnerSingleton {
// 省略其他相同代码
public static final void foo() {}
}
}

对于 foo() 方法的调用,不管是 Kotlin 还是 Java,它们的调用方式都会变成一样的:

1
Person.InnerSingleton.foo()

上面的静态内部类“InnerSingleton”看起来有点多余,我们平时在 Java 当中写的静态方法,不应该是只有一个层级吗?如何实现?

加一个 companion 关键字

1
2
3
4
5
6
7
8
class Person {
// 改动在这里
// ↓
companion object InnerSingleton {
@JvmStatic
fun foo() {}
}
}

在伴生对象的内部,如果存在“@JvmStatic”修饰的方法或属性,它会被挪到伴生对象外部的类当中,变成静态成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final class Person {

public static final Person.InnerSingleton InnerSingleton = new Person.InnerSingleton((DefaultConstructorMarker)null);

// 注意这里
public static final void foo() {
InnerSingleton.foo();
}

public static final class InnerSingleton {
public final void foo() {}

private InnerSingleton() {}

public InnerSingleton(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}

被挪到外部的静态方法 foo(),它最终还是调用了单例 InnerSingleton 的成员方法 foo(),所以它只是做了一层转接而已。

普通的 object 单例,演变出了嵌套的单例;嵌套的单例,演变出了伴生对象。

换个说法:嵌套单例,是 object 单例的一种特殊情况;伴生对象,是嵌套单例的一种特殊情况。

伴生对象的实战应用

工厂模式

1
2
3
4
5
6
7
8
9
10
11
//  私有的构造函数,外部无法调用
// ↓
class User private constructor(name: String) {
companion object {
@JvmStatic
fun create(name: String): User? {
// 统一检查,比如敏感词过滤
return User(name)
}
}
}

另外 4 种单例模式的写法

借助懒加载委托

1
2
3
4
5
6
7
8
9
10
11
object UserManager {
// 对外暴露的 user
val user by lazy { loadUser() }

private fun loadUser(): User {
// 从网络或者数据库加载数据
return User.create("tom")
}

fun login() {}
}

伴生对象 Double Check

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class UserManager private constructor(name: String) {
companion object {
@Volatile private var INSTANCE: UserManager? = null

fun getInstance(name: String): UserManager =
// 第一次判空
INSTANCE?: synchronized(this) {
// 第二次判空
INSTANCE?:UserManager(name).also { INSTANCE = it }
}
}
}

// 使用
UserManager.getInstance("Tom")

定义了一个伴生对象,然后在它的内部,定义了一个 INSTANCE,它是 private 的,这样就保证了它无法直接被外部访问。同时它还被注解“@Volatile”修饰了,这可以保证 INSTANCE 的可见性,而 getInstance() 方法当中的 synchronized,保证了 INSTANCE 的原子性。因此,这种方案还是线程安全的。

以上的实现方式仍然存在一个问题:不同的单例当中,我们必须反复写 Double Check 的逻辑

抽象类模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//  ①                          ②                      
// ↓ ↓
abstract class BaseSingleton<in P, out T> {
@Volatile
private var instance: T? = null

// ③
// ↓
protected abstract fun creator(param: P): T

fun getInstance(param: P): T =
instance ?: synchronized(this) {
// ④
// ↓
instance ?: creator(param).also { instance = it }
}
}
  • 注释①:abstract 关键字,代表了我们定义的 BaseSingleton 是一个抽象类。我们以后要实现单例类,就只需要继承这个 BaseSingleton 即可。
  • 注释②:in P, out T 是 Kotlin 当中的泛型,P 和 T 分别代表了 getInstance() 的参数类型和返回值类型。注意,这里的 P 和 T,是在具体的单例子类当中才需要去实现的。如果你完全不知道泛型是什么东西,可以先看看泛型的介绍,我们在第 10 讲会详细介绍 Kotlin 泛型。
  • 注释③:creator(param: P): T 是 instance 构造器,它是一个抽象方法,需要我们在具体的单例子类当中实现此方法。
  • 注释④:creator(param) 是对 instance 构造器的调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PersonManager private constructor(name: String) {
// ① ②
// ↓ ↓
companion object : BaseSingleton<String, PersonManager>() {
// ③
// ↓
override fun creator(param: String): PersonManager = PersonManager(param)
}
}

class UserManager private constructor(name: String) {
companion object : BaseSingleton<String, UserManager>() {
override fun creator(param: String): UserManager = UserManager(param)
}
}
  • 注释①:companion object : BaseSingleton,由于伴生对象本质上还是嵌套类,也就是说,它仍然是一个类,那么它就具备类的特性“继承其他的类”。因此,我们让伴生对象继承 BaseSingleton 这个抽象类。
  • 注释②:String, PersonManager,这是我们传入泛型的参数 P、T 对应的实际类型,分别代表了 creator() 的“参数类型”和“返回值类型”。
  • 注释③:override fun creator,我们在子类当中实现了 creator() 这个抽象方法。

    接口模板(不推荐)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    interface 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
2
3
4
5
6
7
8
class Singleton private constructor(name: String) {
companion object: ISingleton<String, Singleton> {
// ① ②
// ↓ ↓
@Volatile override var instance: Singleton? = null
override fun creator(param: String): Singleton = Singleton(param)
}
}
  • 注释①:@Volatile,这个注解虽然可以在实现的时候添加,但实现方可能会忘记,这会导致隐患。
  • 注释②:我们在实现 instance 的时候,仍然无法使用 private 来修饰。

小结

Kotlin 的匿名内部类和 Java 的类似,只不过它多了一个功能:匿名内部类可以在继承一个抽象类的同时还实现多个接口。

object 的单例和伴生对象,这两种语义从表面上看是没有任何联系的。“单例”演变出了“嵌套单例”,而“嵌套单例”演变出了“伴生对象”。

借助 Kotlin 伴生对象这个语法,研究了伴生对象的实战应用,比如可以实现工厂模式、懒加载 + 带参数的单例模式。

单例模式使用场景:

  • 如果我们的单例占用内存很小,并且对内存不敏感,不需要传参,直接使用 object 定义的单例即可。
  • 如果我们的单例占用内存很小,不需要传参,但它内部的属性会触发消耗资源的网络请求和数据库查询,我们可以使用 object 搭配 by lazy 懒加载。
  • 如果我们的工程很简单,只有一两个单例场景,同时我们有懒加载需求,并且 getInstance() 需要传参,我们可以直接手写 Double Check。
  • 如果我们的工程规模大,对内存敏感,单例场景比较多,那我们就很有必要使用抽象类模板 BaseSingleton 了。

关注我