前言
这里是专栏 重学 Kotlin ,灵感来自于 Medium 上 Android Developers 团队的 Kotlin Vocabulary 。
作为一名 Kotlin 老铁粉,我可能在博客里不止一次的表达过对 Kotlin 的态度。
都 2020 了,作为一名安卓开发者,再不会 Kotlin ,真的说不过去了!
介绍 Kotlin 语法的文章很多,那么,在这个系列中,我会写一些什么呢?
Kotlin 再强大,也逃脱不了在 JVM 上运行。经过 kotlinc
编译之后,生成的依旧是 .class
文件。
所以,学习 Kotlin 的最佳方式其实就是查看字节码。Android Studio 直接提供了插件,按如下方式即可查看:
Tools -> Kotlin -> Show Kotlin Bytecode
当然,字节码可读性太差,IDE 提供了 Decompile ,将字节码转换成 Java 代码。
这样,我们就可以轻松掌握 Kotlin 各种语法的本质。
本系列的每一篇文章都会选择一个关键字或者知识点,剖析本质,帮助大家快速深入理解 Kotlin 。
下面就进入今天的主角 object 。
目录
object 有哪些用法?
对象声明 —— 一个关键字实现单例 ?
伴生对象 —— static 的代替者 ?
对象表达式 —— Kotlin 的匿名内部类 ?
这到底是哪种用法 ?
正文
object 的三种用法
Kotlin 的 object
关键字有三种用法:
对象声明 ,一般用来实现单例伴生对象 ,类似 Java 的 static 关键字,也可以用于工厂方法模式对象表达式 ,一般用来代替 Java 的匿名内部类
下面就逐个来看看这三种用法的本质。
对象声明
object
的语义是这样的: 定义一个类并创建一个实例 。不管是对象声明,还是下面会说到的另外两种用法,都是遵循这一语义的。
作为对象声明,它可以直接用来实现单例模式:
object Singleton{
fun xxx(){}
}
话不多说,直接 Decompile 看 Java 代码:
public final class Singleton {
public static final Singleton INSTANCE;
public final void xxx() {
}
private Singleton() {
}
static {
Singleton var0 = new Singleton();
INSTANCE = var0;
}
}
从 Java 代码中可以看出来,显然这是一个单例模式。
私有构造函数 通过静态字段对外提供实例 静态代码块中直接初始化,线程安全 。
这里插播一个问题,static 代码块在何时执行?
首先类加载阶段可以分为加载 、验证 、准备 、解析 、初始化 、使用 、卸载 七个步骤 。static 代码块就是在 初始化 阶段执行的。那么,哪些场景会触发类的初始化呢?有如下几种场景:
通过 new
实例化对象 读写一个类的静态字段 调用一个类的静态方法 对类进行反射调用
按照上面反编译出来的 Java 代码,获得单例对象的方法是 Singleton.INSTANCE
,即调用 Singleon
类的静态字段 INSTANCE
,就会触发类的初始化阶段,也就触发了 static 代码块的执行,从而完成了单例对象的实例化。同时,由于类加载过程天生线程安全,所以 Kotlin 的 object 单例活脱脱的就是一个线程安全的懒汉式单例(访问时初始化)。
此外,object 声明的单例类和普通类一样,可以实现接口,继承类,也可以包含属性,方法。但是它不能由开发者手动声明构造函数,从反编译出来的 Java 代码可以看到,它只有一个 private
构造函数。
所以,这对实际的业务场景是有一定限制的。对于需要携带参数的单例类,object 就有点力不从心了。当然也不难解决,模仿 Java 的写法就行了,这里以 DCL 模式为例。
class Singleton private constructor(private val param: Int) {
companion object {
@Volatile
private var instance: Singleton? = null
fun getInstance(property: Int) =
instance ?: synchronized(this) {
instance ?: Singleton(property).also { instance = it }
}
}
}
说到这,你应该了解了 object
实现单例模式的本质。下面来看看 伴生对象 。
伴生对象
你可以回想一下,你在 Kotlin 中使用过 static
关键字吗?答案肯定是没有。通常我们可以在顶层文件中直接定义常量和顶层函数,但有的时候我们的确需要在类中定义静态常量或函数,这样显得更加直观。这就是 伴生对象 的应用场景。
伴生对象 ,顾名思义,就是伴随着类而存在的对象,在类加载的时候初始化。
class User(val male: Int){
companion object {
val MALE = 0
fun isMale(male:Int) = male == MALE
}
}
这样就可以像调用 static 一样调用伴生对象中的属性和函数,而无需创造类实例。
User.MALE
User.isMale(1)
还是直接看 Java 代码。
public final class User {
private final int male;
private static final int MALE = 0;
public static final User.Companion Companion = new User.Companion((DefaultConstructorMarker)null);
public final int getMale() {
return this.male;
}
public User(int male) {
this.male = male;
}
public static final class Companion {
public final int getMALE() {
return User.MALE;public static final User.Companion Companion = new User.Companion((DefaultConstructorMarker)null);
}
public final boolean isMale(int male) {
return male == ((User.Companion)this).getMALE();
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
编译器为我们生成了一个叫做 Companion
的静态内部类,注意它的 getMale()
和 isMale()
方法并不是静态方法,所以实际去访问的时候还是需要一个 Companion
实例的。这里实例就是 User
类中定义的静态成员变量 Companion
:
public static final User.Companion Companion = new User.Companion((DefaultConstructorMarker)null);
看到静态字段,又该想到在类加载的时候初始化的了。那么,哪个操作触发了类加载呢?我们来反编译一下 User.MALE
的 Java 代码。
User.Companion.getMALE();
所以也是访问时时会初始化伴生对象。再回想一下前面说过的,
object
其实我们可以把它理解成 定义一个类并创建一个实例 。
伴生对象仍旧符合这一语义。
在 Java 中如何调用伴生对象呢?User.Companion.isMale(1)
即可。另外,我们可以给伴生对象命名,如下所示:
companion object X { ... }
那么,编译器生成的类就不是 Companion
了,而是 X
。在 Java 中就可以用 User.X.isMale(1)
了。
了解了伴生对象的本质之后,再来说两个它的其他用法。
创建静态工厂方法
interface Car {
val brand: String
companion object {
operator fun invoke(type: CarType): Car {
return when (type) {
CarType.AUDI -> Audi()
CarType.BMW -> BMW()
}
}
}
}
这里重载了 invoke()
方法,调用时直接 Car(CarType.BMW)
即可。你可以试着用 Java 代码实现上面的逻辑,对比一下。
伴生对象扩展方法
伴生对象也是支持扩展方法的。还是以上面的 Car
为例,定义一个根据汽车品牌获取汽车类型的扩展方法。
fun Car.Companion.getCarType(brand:String) :CarType { ...... }
虽然是在 Car.Companion
上定义的扩展函数,但实际上相当于给 Car
增加了一个静态方法,使用方式如下:
Car.getCarType("BMW")
对象表达式
对象表达式最经典的用法就是用来 代替 Java 的匿名内部类 。例如常见的点击事件:
xxx.setOnClickListener(object : View.OnClickListener{
override fun onClick(v: View) {
}
})
这和 Java 的匿名内部类是等价的。只不过像上面的单方法接口,我们很少用 object
写,而是用 lambda
代替,显得更加简洁。
xxx.setOnClickListener { view -> ...... }
当匿名对象需要重写多个方法时,就只能选择对象表达式了。
和 Java 的匿名内部类一样,对象声明中也可以访问外部变量。
对象表达式应该是 object
最朴实无华的使用方式了。
最后
看到这里,你应该已经完全掌握了 object
关键字的本质。那么,我也要来考考你,仔细看下面的代码:
class MainActivity : AppCompatActivity() {
val a = 1
object click : View.OnClickListener {
override fun onClick(v: View) {
val b = a + 1
}
}
}
上面的代码可以正确编译吗?为什么? 这里 object
的用法属于哪一种?史上快重学 Kotlin —— object,史上最 “快” 单例 ?