重学 Kotlin —— object,史上最 “快” 单例 ?

简介: 重学 Kotlin —— object,史上最 “快” 单例 ?

前言



这里是专栏 重学 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 代码。

image.png

这样,我们就可以轻松掌握 Kotlin 各种语法的本质。

本系列的每一篇文章都会选择一个关键字或者知识点,剖析本质,帮助大家快速深入理解 Kotlin 。

下面就进入今天的主角 object


目录


  1. object 有哪些用法?
  2. 对象声明 —— 一个关键字实现单例 ?
  3. 伴生对象 —— static 的代替者 ?
  4. 对象表达式 —— Kotlin 的匿名内部类 ?
  5. 这到底是哪种用法 ?


正文


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



相关文章
|
2月前
|
设计模式 安全 数据库连接
|
3月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
45 6
|
4月前
|
存储 Java Kotlin
Kotlin中的嵌套类、内部类、枚举类、密封类、数据类、单例类、伴生对象
Kotlin中的嵌套类、内部类、枚举类、密封类、数据类、单例类、伴生对象
|
Kotlin
Kotlin中继承、类型转换、Any超类、object关键字详解
Kotlin中继承、类型转换、Any超类、object关键字详解
131 0
|
Java Kotlin
【Kotlin 初学者】抽象类-嵌套类-单例类-接口
目录 一、抽象类 二、嵌套类(内部类) 三、单例类 3.1 object 关键字 3.2 对象声明 3.3 对象表达式 3.4 伴生对象 四、接口 4.1 接口定义 4.2 默认实现 4.3 小结
195 0
【Kotlin 初学者】抽象类-嵌套类-单例类-接口
|
设计模式 Java Kotlin
【Kotlin】Kotlin 单例 ( 懒汉式 与 恶汉式 | Java 单例 | Kotlin 单例 | 对象声明 | 伴生对象 | get 方法 | ? 与 !! 判空 )
【Kotlin】Kotlin 单例 ( 懒汉式 与 恶汉式 | Java 单例 | Kotlin 单例 | 对象声明 | 伴生对象 | get 方法 | ? 与 !! 判空 )
186 0
|
安全 Java Android开发
Kotlin 设计模式解析之单例
### 单例模式介绍 单例模式是一个比较简单的设计模式,同时也是挺有意思的一个模式,虽然看起来简单,但是可以玩出各种花样。比如 Java 当中的懒饿汉式单例等。 #### 什么是单例 单例模式的定义: > Ensure a class only has one instance, and provide a global point of access to it. 简单来说
1766 0
|
Java Kotlin
Kotlin中object的使用
一、object 只有一个实例的类 不能自定义构造方法 可以实现接口、继承父类 本质上就是单例模式最基本的实现 二、看一下例子 先看看kotlin中的单例 package net.
1523 0
|
27天前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
51 1
|
2月前
|
调度 Android开发 开发者
【颠覆传统!】Kotlin协程魔法:解锁Android应用极速体验,带你领略多线程优化的无限魅力!
【8月更文挑战第12天】多线程对现代Android应用至关重要,能显著提升性能与体验。本文探讨Kotlin中的高效多线程实践。首先,理解主线程(UI线程)的角色,避免阻塞它。Kotlin协程作为轻量级线程,简化异步编程。示例展示了如何使用`kotlinx.coroutines`库创建协程,执行后台任务而不影响UI。此外,通过协程与Retrofit结合,实现了网络数据的异步加载,并安全地更新UI。协程不仅提高代码可读性,还能确保程序高效运行,不阻塞主线程,是构建高性能Android应用的关键。
41 4