前言

这里是专栏重学 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.MALEUser.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:

publicstaticfinal 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的用法属于哪一种?

在评论区留下你的答案吧!


更多相关文章

  1. Python 加速运行技巧
  2. 强!8 个 Python 优化提速的小技巧!
  3. 阅读代码:Spark 与 Flink 中的 RPC 实现
  4. 面向对象 oop 与类的进阶
  5. 使用面向对象方法实现用户信息增删改查
  6. 版本管理·玩转git(快速入门git)
  7. 回答两个被频繁问到的代码写法问题
  8. 软件测试人员需不需懂代码
  9. 为什么编程都带点强迫症?

随机推荐

  1. android EditText 默认情况下不获取焦点(
  2. Android中的Context
  3. Android 弹出对话框Dialog
  4. appt命令检测Apk信息的方法
  5. 安卓中的布局属性详解
  6. Android应用程序的安装位置
  7. Android Studio 创建虚拟机失败 Failed t
  8. react-native-vector-icons(android)的安装
  9. Android(安卓)Transition(Android过渡动画
  10. Android中为窗口定义主题