跳到主要内容

Kotlin 对象表达式和伴生对象

本篇文章将是 Kotlin 面向对象系列的最后一篇文章,这篇文章将会介绍几个特殊的对象语法,这是 Kotlin 语法中独有的。比如对象表达式 (object),天生的单例对象它会使写一个单例模式变得特别简单,而不是像 Java 那样声明一些语法模板。此外伴生对象 (companion object) 它将替代 Java 中的 static 静态成员。

1. 为什么需要对象表达式

1.1 对象表达式天生单例,会使得单例模式更简单

相信很多小伙伴都手写过 Java 中的单例模式,我们熟知单例模式必须满足几个条件:

  • 构造器私有化,private 修饰,主要为了防止外部私自创建该单例类的对象实例
  • 提供一个该实例对象全局访问点,在 Java 中一般是以公有的静态方法或者枚举返回单例类对象
  • 多线程环境下保证单例类有且只有一个对象实例,以及在多线程环境下获取单例类对象实例需要保证线程安全。
  • 反序列化时保证单例类有且只有一个对象实例

其实在 Java 中实现一个单例模式,上述条件需要写一些模板的代码,比如下面代码:

public class Singleton implements Serializable {
private Singleton() {//构造器私有化
}

private static final Singleton mInstance = new Singleton();

public static Singleton getInstance() {//提供公有获取单例对象的函数
return mInstance;
}

//防止单例对象在反序列化时重新生成对象
private Object readResolve() throws ObjectStreamException {
return mInstance;
}
}

//外部调用
public class TestMain {
public static void main(String[] args) {
Singleton.getInstance().doSomething();
}
}

然而上述近 15 行 Java 的代码,在 Kotlin 中使用单例,只需要简单声明一个 object 表达式,然后在表达式内部定义单例方法即可:

object KSingleton : Serializable {//实现Serializable序列化接口,通过私有、被实例化的readResolve方法控制反序列化
fun doSomething() {
println("do some thing")
}

private fun readResolve(): Any {//防止单例对象在反序列化时重新生成对象
return KSingleton//由于反序列化时会调用readResolve这个钩子方法,只需要把当前的KSingleton对象返回而不是去创建一个新的对象
}
}

为什么一行简单的 object 对象表达式就能实现单例,实际上这就是编译器魔法或者说是语法糖,本质上单例还是单例规则,这一点是无法改变的,而是编译器在编译 object 期间生成额外的单例代码,所以要想揭开这层语法糖衣还得将 Kotlin 代码反编译成 Java 代码。

public final class KSingleton implements Serializable {
public static final KSingleton INSTANCE;

public final void doSomething() {
String var1 = "do some thing";
System.out.println(var1);
}

private final Object readResolve() {
return INSTANCE;//可以看到readResolve方法直接返回了INSTANCE而不是创建新的实例
}

static {//静态代码块初始化KSingleton实例,不管有没有使用,只要KSingleton被加载了,
//静态代码块就会被调用,KSingleton实例就会被创建,并赋值给INSTANCE
KSingleton var0 = new KSingleton();
INSTANCE = var0;
}
}

可能会有人疑惑:没有看到构造器私有化,实际上这一点已经在编译器层面做了限制,不管你是在 Java 还是 Kotlin 中都无法私自去创建新的 object 单例对象。

1.2 替代 Java 中的匿名内部类

我们都知道在 Java 中有匿名内部类,一般情况直接通过 new 匿名内部类对象,然后重写内部抽象方法。但是在 Kotlin 使用 object 对象表达式来替代了匿名内部类,一般匿名内部类用在接口回调比较多。比如 Java 实现匿名内部类:

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

mButton.setOnClickListener(new OnClickListener() {//Java创建匿名内部类对象
@Override
public void onClick(View view) {
//do logic
}
});

然而在 Kotlin 并不是直接创建一个匿名接口对象,而是借助 object 表达式来声明的。

interface OnClickListener {
fun onClick()
}

mButton.setOnClickListener(object: OnClickListener{//Kotlin创建object对象表达式
override fun onClick() {
//do logic
}
})

2. 如何使用对象表达式

使用对象表达式很简单,只需要像声明类一样声明即可,只不过把 class 关键字换成了 object. 声明格式: object + 对象名 + : + 要实现 / 继承的接口或抽象类 (用做单例模式场景) 和 object + : + 要实现 / 继承的接口或抽象类 (用做匿名内部类场景)

//1、用做单例模式形式
object KSingleton : Serializable {//object关键字 + 对象名(KSingleton) + : + 要实现的接口(Serializable)
fun doSomething() {
println("do some thing")
}

private fun readResolve(): Any {
return KSingleton
}
}

//2、用做匿名内部类形式
mButton.setOnClickListener(object: OnClickListener{//object关键字 + : + 要实现的接口(OnClickListener)
override fun onClick() {
//do logic
}
})

3. 对象表达式使用场景

在 Kotlin 中 object 对象表达式使用场景主要就是单例模式和替代匿名内部类场景。

3.1 object 用于单例模式场景

单例场景很简单,如果有需要使用单例模式,只要声明一个 object 对象表达式即可:

object KSingleton : Serializable {//实现Serializable序列化接口,通过私有、被实例化的readResolve方法控制反序列化
fun doSomething() {
println("do some thing")
}

private fun readResolve(): Any {//防止单例对象在反序列化时重新生成对象
return KSingleton//由于反序列化时会调用readResolve这个钩子方法,只需要把当前的KSingleton对象返回而不是去创建一个新的对象
}
}

//在Kotlin中使用KSingleton
fun main(args: Array<String>) {
KSingleton.doSomething()//像调用静态方法一样,调用单例类中的方法
}
//在Java中使用KSingleton
public class TestMain {
public static void main(String[] args) {
KSingleton.INSTANCE.doSomething();//通过拿到KSingleton的公有单例类静态实例INSTANCE, 再通过INSTANCE调用单例类中的方法
}
}

3.2 object 用于匿名内部类场景

object 使用匿名内部场景在开发中还是比较多的,对于需要写一些接口回调方法时,一般都离不开 object 对象表达式。

interface OnClickListener {
fun onClick()
}

mButton.setOnClickListener(object: OnClickListener{//Kotlin创建object对象表达式
override fun onClick() {
//do logic
}
})

3.3 object 匿名内部类场景和 lambda 表达式场景如何选择

其实我们知道在 Kotlin 中对于匿名内部类场景,除了可以使用 object 对象表达式场景还可以使用 lambda 表达式,但是需要注意的是能使用 lambda 表达式替代匿名内部场景必须是匿名内部类使用的类接口中只能有一个抽象方法超过一个都不能使用 lambda 表达式来替代。所以对于 object 表达式和 lambda 表达式替换匿名内部类场景就一目了然了。

interface OnClickListener {
fun onClick()
}

mButton.setOnClickListener{//因为OnClickListener中只有一个抽象方法onClick,所以可以直接使用lambda表达式的简写形式
//do logic
}

interface OnLongClickListener {
fun onLongClick()

fun onClickLog()
}

mButton.setOnLongClickListener(object : OnLongClickListener {//因为OnLongClickListener中有两个抽象方法,所以只能使用object表达式这种形式
override fun onLongClick() {

}

override fun onClickLog() {

}
})

4. 伴生对象

在 Kotlin 中其实已经看不到任何的 static 静态关键字的字眼,我们都知道在 Java 中 static 是一个非常重要的特性,它可以用来修饰类、方法或属性。然而,static 修饰的内容是属于类级别的,而不是具体的对象级别的,但是很奇怪的是在定义的时候却与普通类成员变量和方法混合在一起。站在 Kotlin 视角来看就觉得代码结构很混乱,所以 Kotlin 希望尽管属于类级别的,定义在类的内部也希望把所有类似静态方法、属性都定义一个叫做 companion object 局部作用域内,这样一看就知道在 companion object 中是静态的。一起来看下对比例子:

package com.imooc.test;

public class Teacher {
private String name;
private String sex;
private int age;

public Teacher(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}

//可以看到在Java中static方法和属性都和普通类Teacher中普通成员方法属性混在一起的
static final String MALE = "male";
static final String FEMALE = "FEMALE";

public static boolean isWomen(Teacher teacher) {
return teacher.sex.equals(FEMALE);
}
}

Kotlin 希望能找到一种方式能够将两部分代码分开,但是又不失语义,所以 Kotlin 在 object 对象表达式基础上,引入了伴生对象的概念,伴生对象故名思义就是伴随某个类的对象,它属于这个类所有,因此伴生对象和 Java 中 static 修饰效果性质一样,全局只有一个单例。它声明在类的内部,在类被装载的时候初始化。

package com.imooc.test

class KTeacher(private var name: String, private var sex: String, private var age: Int) {
companion object {//在KTeacher类内部,提出companion object作用域将所有static相关的成员属性和方法放在一起,这样就可以和普通成员属性和方法分隔开了
private const val MALE = "male"
private const val FEMALE = "female"

fun isWoman(teacher: KTeacher): Boolean {
return teacher.sex == FEMALE
}
}
}

54. 总结

到这里有关 Kotlin 中面向对象相关系列知识就介绍完毕了,总的体会下来会发现 Kotlin 的面向对象有很多地方还是和 Java 保持一样,但是也引入很多自己特有的语法,然而在这些语法的背后依然还是回归到 Java 上了。但是不得不说这语法糖是真甜,写代码的效率将是大大提高,这也是 Kotlin 这门语言初衷,减少不必要模板代码,让开发者更加专注于自己业务逻辑。下篇文章将进入 Kotlin 比较复杂且不易理解的泛型系列,会由浅入深地带你认识 Kotlin 泛型的使用场景以及它背后的原理。