Kotlin中object的多种用法

37 阅读11分钟

object 的三种语义,分别是匿名内部类、单例、伴生对象。这三种语义本质上都是在定义一个类的同时还创建了对象。所以它们被统一成 object 关键字。

1、object:定义匿名内部类

使用object定义匿名内部类的时候,还可以继承一个抽象类的同时,实现多个接口;

Java 当中其实也有匿名内部类的概念,这里我们可以通过跟 Java 的对比,来具体理解下 Kotlin 中对匿名内部类的定义。在 Java 开发当中,我们经常需要写类似这样的代码:

public interface OnClickListener {
    void onClick(View v);
}

image.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        gotoPreview();
    }
});

这就是典型的匿名内部类的写法,View.OnClickListener 是一个接口,因此我们在创建它的时候,必须实现它内部没有实现的方法。

在 Kotlin 当中,我们会使用 object 关键字来创建匿名内部类。同样,在它的内部,我们也必须要实现它内部未实现的方法。这种方式不仅可以用于创建接口的匿名内部类,也可以创建抽象类的匿名内部类。

image.setOnClickListener(object: View.OnClickListener {
    override fun onClick(v: View?) {
        gotoPreview()
    }
})

需要特殊说明的是,当 Kotlin 的匿名内部类只有一个需要实现的方法时,我们可以使用 SAM 转换,最终使用 Lambda 表达式来简化它的写法。

也就是说,Java 和 Kotlin 相同的地方就在于,它们的接口与抽象类,都不能直接创建实例。想要创建接口和抽象类的实例,我们必须通过匿名内部类的方式。

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

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
        }
    }
}

上面代码使用 object 定义了一个匿名内部类。这个匿名内部类,不仅继承了抽象类 Man,还同时实现了接口 A、接口 B。而这种写法,在 Java 当中其实是不被支持的。在日常的开发工作当中,我们有时会遇到这种情况:我们需要继承某个类,同时还要实现某些接口,为了达到这个目的,我们不得不定义一个内部类,然后给它取个名字。但这样的类,往往只会被用一次就再也没有其他作用了。所以针对这种情况,使用 object 的这种语法就正好合适。我们既不用再定义内部类,也不用想着该怎么给这个类取名字,因为用过一次后就不用再管了。

2、object:单例模式

object修饰类,替代class关键字;

object UserManager { 
    fun login() {}
}

反编译看看对应的 Java 代码:

public final class UserManager {

   public static final UserManager INSTANCE; 

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

   private UserManager() {}

   public final void login() {}
}

当使用 object 关键字定义单例类的时候,Kotlin 编译器会将其转换成静态代码块的单例模式。因为static{}代码块当中的代码,由虚拟机保证它只会被执行一次,因此,它在保证了线程安全的前提下,同时也保证我们的 INSTANCE 只会被初始化一次。

缺点:不支持懒加载、不支持传参构造单例;

3、object:伴生对象

类的嵌套单例前添加 companion object; 嵌套单例是单例模式的一种特殊情况,伴生对象是嵌套单例的一种特殊情况;

Kotlin 当中没有 static 关键字,所以我们没有办法直接定义静态方法和静态变量。不过,Kotlin 还是为我们提供了伴生对象,来帮助实现静态方法和变量。

如何才能实现类似 Java 静态方法的代码呢?我们可以使用“@JvmStatic”这个注解,如以下代码所示:

class Person {
    object InnerSingleton {
        @JvmStatic
        fun foo() {}
    }
}

反编译成Java代码可以发现foo()成为InnerSingleton静态内部类的静态方法。调用方式Person.InnerSingleton.foo(),明显有一个多余的层级。优化方式:加一个 companion 关键字即可。

class Person {
//  改动在这里
//     ↓
    companion object InnerSingleton {
        @JvmStatic
        fun foo() {}
    }
}

companion object,在 Kotlin 当中就被称作伴生对象,它其实是我们嵌套单例的一种特殊情况。也就是,在伴生对象的内部,如果存在“@JvmStatic”修饰的方法或属性,它会被挪到伴生对象外部的类当中,变成静态成员。

反编译成Java看看:

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 单例,演变出了嵌套的单例;嵌套的单例,演变出了伴生对象。也可以换个说法:嵌套单例,是 object 单例的一种特殊情况;伴生对象,是嵌套单例的一种特殊情况。

工厂模式

所谓的工厂模式,就是指当我们想要统一管理一个类的创建时,我们可以将这个类的构造函数声明成 private,然后用工厂模式来暴露一个统一的方法,以供外部使用。Kotlin 的伴生对象非常符合这样的使用场景:

伴生对象的应用实现工厂模式:

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

将 User 的构造函数声明成了 private 的,这样,外部的类就无法直接使用它的构造函数来创建实例了。与此同时,我们通过伴生对象,暴露出了一个 create() 方法。在这个 create() 方法当中,我们可以做一些统一的判断,比如敏感词过滤、判断用户的名称是否合法。另外,由于“伴生对象”本质上还是属于 User 的嵌套类,伴生对象仍然还算是在 User 类的内部,所以,我们是可以在 create() 方法内部调用 User 的构造函数的。

4、另外4种单例模式写法,分别是懒加载委托单例模式、Double Check 单例模式、抽象类模板单例,以及接口单例模板。

4.1 借助懒加载委托 关键字 by lazy

object UserManager {
    // 对外暴露的 user
    val user by lazy { loadUser() }

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

    fun login() {}
}

当UserManager 内部的 user 变量变成了懒加载,只要 user 变量没有被使用过,它就不会触发 loadUser() 的逻辑。这其实是一种简洁与性能的折中方案。

4.2 伴生对象 Double Check

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")

这种写法本质上就是 Java 的 Double Check。

首先,我们定义了一个伴生对象,然后在它的内部,定义了一个 INSTANCE,它是 private 的,这样就保证了它无法直接被外部访问。同时它还被注解“@Volatile”修饰了,这可以保证 INSTANCE 的可见性,而 getInstance() 方法当中的 synchronized,保证了 INSTANCE 的原子性。因此,这种方案还是线程安全的。同时,我们也能注意到,初始化情况下,INSTANCE 是等于 null 的。这也就意味着,只有在 getInstance() 方法被使用的情况下,我们才会真正去加载用户数据。这样,我们就实现了整个 UserManager 的懒加载,而不是它内部的某个参数的懒加载。另外,由于我们可以在调用 getInstance(name) 方法的时候传入初始化参数,因此,这种方案也是支持传参的。

其中also是T 的扩展函数,返回值与apply一致,直接返回T;

缺点:每次实现一个单例,都要重写Double Check的逻辑;

所以,以下两种写法复写部分逻辑。

4.3 抽象类模板

主要由两个部分组成:第一部分是 INSTANCE 实例,第二部分是 getInstance() 函数。

将单例当中通用的“INSTANCE 实例”和“getInstance() 函数”,抽象到 BaseSingleton 当中来

//  ①                          ②                      
//  ↓                           ↓                       
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,是在具体的单例子类当中才需要去实现的。

注释③:creator(param: P): T 是 instance 构造器,它是一个抽象方法,需要我们在具体的单例子类当中实现此方法。

注释④:creator(param) 是对 instance 构造器的调用。

以前面的UserManager、PersonManager 为例,用抽象类模板的方式来实现单例

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() 这个抽象方法。

不必重复去写“INSTANCE 实例”和“Double Check”这样的模板代码,只需要简单继承 BaseSingleton 这个抽象类,按照要求传入泛型参数、实现 creator 这个抽象方法即可。

4.4 接口模板

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 }
        }
}

缺点: 1、instance无法使用private修饰,这并不符合单例的规范。正常情况下的单例模式,我们内部的 instance 必须是 private 的,这是为了防止它被外部直接修改。

2、instance 无法使用 @Volatile 修饰。这也是受限于接口的特性,这会引发多线程同步的问题。

具体使用

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 定义的单例即可。

如果我们的单例占用内存很小,不需要传参,但它内部的属性会触发消耗资源的网络请求和数据库查询,我们可以使用 object 搭配 by lazy 懒加载。

如果我们的工程很简单,只有一两个单例场景,同时我们有懒加载需求,并且 getInstance() 需要传参,我们可以直接手写 Double Check。

如果我们的工程规模大,对内存敏感,单例场景比较多,那我们就很有必要使用抽象类模板 BaseSingleton 了。