Kotlin - 单例、伴生对象、对象表达式 object

本文详细介绍了Kotlin中object关键字的三种用法:对象声明、伴生对象和对象表达式。对象声明用于实现单例,伴生对象则类似于Java的静态成员,而对象表达式则常用于匿名内部类。文章探讨了它们的实现原理、用法差异以及与Java的交互方式,并提供了示例代码进行说明。

一、概念

object 更适合用于无状态的工具类、常量集合,或者作为依赖注入框架内部的模块定义。

1.1 object 关键字的几种用法

修饰类(对象声明)

是一个饿汉单例,定义一个类并创建它的实例。

修饰伴生对象(静态调用)

伴随着类而存在的对象,在类加载的时候被实例化,多个实例会共享。

对象表达式(匿名内部类)

可以多实现,访问外部函数中的局部变量不用加 final。

1.2 几种类似“Java静态方法”的方式 

Kotlin 极度弱化了静态方法这个概念,提供了比静态方法更好用的语法特性,同样是通过【类名.函数名】来调用。

object

适合写“全是静态方法的工具类”。(Kotlin v1.9 版本新增的 data object 是针对密封类/密封接口使用场景的优化,toString() 不会打印 HashCode 等无用信息,让输出更有意义)

companion object适合给类定义“专门的静态方法”。
顶层函数

假设定义顶层函数 method() 的文件名为 Helper.kt,编译器会创建一个 HelperKt.java,在Java中就可以 HelperKt.method() 调用了。

@JvmStatic 注解给上面的单例类或伴生对象添加注解后,在Java中就可以当做静态方法调用了。

1.3 乱用的隐患

单体提供了全局访问入口,无需注入哪里都能调用,非常方便但也带来了一些风险影响。更适合用于无状态的工具类、常量集合,或者作为依赖注入框架内部的模块定义。替代方案是使用接口和依赖注入。

  • 隐藏的依赖:任何一个类都可能在内部悄悄依赖 XXXManager,你无法从它的构造函数或公开 API 中看出这一点。
  • 全局状态:一个全局可变的状态,任何地方的任何代码都可能修改它,导致行为不可预测,尤其是在多线程环境中。
  • 测试噩梦:你无法在单元测试中轻易地替换或 mock UserManager 的行为。例如,你如何测试“token 失效”的场景?
// 通过接口和依赖注入
interface SessionProvider {
    fun getToken(): String?
    fun saveToken(token: String)
}

class InMemorySessionProvider : SessionProvider {
    private var userToken: String? = null
    // ... 实现方法
}

@Singleton // 在依赖注入框架中声明其生命周期
class InMemorySessionProvider @Inject constructor() : SessionProvider { ... }

// 在 ViewModel 或其他类中注入
class MyViewModel @Inject constructor(
    private val sessionProvider: SessionProvider
) : ViewModel() {
    fun doSomething() {
        val token = sessionProvider.getToken()
        // ...
    }
}

二、单例(对象声明)

定义一个类并创建它的实例。可以继承类实现接口、包含属性和方法,但是不能手动声明构造。由于不能使用构造函数,可以使用 init()。需要构造传参见下方伴生对象。

 可以声明在顶层、也可以声明在类中(见下方和伴生对象对比)

object Demo{
    val name = "单例"
    fun show() = println(name)
}

Demo.show()
println(Demo.name)

反编译成Java代码:(饿汉)

public final class Demo {
    //成员变量
    private static String name;
    //通过静态字段提供实例
    public static final Demo INSTANCE;
    //私有化构造
    private Demo() { }
    //静态代码块中初始化
    static {
        Demo var0 = new Demo();
        INSTANCE = var0;
        name = "单例";
    }
    //成员方法
    public final void show() {
        String var1 = name;
        System.out.println(var1);
    }
    //生成的getter、setter
    public final String getName() { return name; }
    public final void setName(@NotNull String var1) {
        Intrinsics.checkNotNullParameter(var1, "<set-?>");
        name = var1;
    }
}

 模仿Java方式写Kotlin带参数单例:

class Demo private constructor(
    private var id: Long,
    private var name: String
) {
    companion object {
        private var instance: Demo? = null
        fun getInstance(id: Long, name: String) {
            instance ?: synchronized(this) {
                instance ?: Demo(id, name).also { instance = it }
            }
        }
    }
}

三、伴生对象(静态调用)

  • 伴随着类而存在的对象,在类加载的时候被实例化,多个实例会共享。Kotlin 中没有 static 静态的概念,可以使用顶层函数和常量。顶层函数不能访问类中私有的成员,在伴生对象中需要用外部实例来访问(静态内部类无法直接访问外部类中的非静态成员),例如Outter().num。
  • 伴生对象的名称可以省略(默认为Companion),不管伴生对象的名称是否手动声明,都可以直接用外部类名调用伴生对象中的属性和方法。
  • 当类中成员和伴生对象中成员重名的时候,类名调用的是伴生对象中的,实例调用的是类中的。
class Outer {
    companion object Inner {    //Inner名称可以省略
        var str: String = ""
        fun show(){}
    }
}

fun main() {
    Outer.str = ""
    Outer.show()
    Outer.Inner.str = "" //编译器会提示 Inner 是多余的
    Outer.Inner.show() //编译器会提示 Inner 是多余的
    val obj: Outer.Inner = Outer.Inner
    obj.str = ""
    obj.show()
}

3.1 伴生对象 & 类中单例

伴生对象

类中单例对象

相同

都是一个静态内部类,通过(外部类名.内部类名.成员名)的方式调用。

构造都是 private

都可以继承类、实现接口、拥有属性和函数

不同

①外部类中创建并持有伴生对象的实例。

②定义的属性会成为外部类的私有静态字段,声明private便不会。函数还留在内部。

③一个类中只能拥有一个伴生对象。

①自己就是实例。

②持有自己的属性和函数。

③一个类/接口中可以拥有多个单例对象(可用来作为常量中的分组)。

④属于嵌套类(叫嵌套对象),就是静态内部类。

Kotlin代码:

class Demo {
    //伴生对象
    companion object One {    //不取名的话,默认名称是 Companion
        var a = 1    //可以自定义getter、setter
        val b = 2
        fun one() = println("伴生对象")
    }
    //单例
    object Two {
        var c = 3    //可以自定义getter、setter
        val d = 4
        fun two() = println("单例")
    }
}

 反编译成Java代码:

public final class Demo {
    //伴生对象中的成员变量(外部类持有)
    private static int a = 1;
    private static final int b = 2;
    //持有伴生对象实例(外部类持有)
    public static final Demo.One One = new Demo.One((DefaultConstructorMarker)null);
    
    //伴生对象
    public static final class One {
        //私有化构造
        private One() { }
        // $FF: synthetic method    合成的方法
        public AA(DefaultConstructorMarker $constructor_marker) {
            this();
        }
        //成员方法
        public final void one() {
            String var1 = "伴生对象";
            System.out.println(var1);
        }
        //生成的getter、setter
        public final int getA() { return Demo.a; }
        public final void setA(int var1) { Demo.a = var1; }
        public final int getB() { return Demo.b; }
    }
    
    //单例
    public static final class Two {
        //通过静态字段提供实例
        public static final Demo.Two INSTANCE;
        //私有化构造
        private Two() { }
        //静态代码块中初始化
        static {
            Demo.Two var0 = new Demo.Two();
            INSTANCE = var0;
            c = 3;
            d = 4;
        }
        //成员变量
        private static int c;
        private static final int d;
        //成员方法
        public final void two() {
            String var1 = "单例";
            System.out.println(var1);
        }
        //生成的getter、setter
        public final int getC() { return c; }
        public final void setC(int var1) { c = var1; }
        public final int getD() { return d; }
    }

}

在伴生对象中的定义的 val/var 反编译后的权限修饰符是private,因此会生成一个静态对象和 getter/setter,成本远超一个静态参数的价值,可以做如下优化:

  • 静态常量:由于 val 反编译是 private static final,因此会生成 getter。使用 consta val 反编译是 public static final,就不会生成 getter 了。
  • 静态变量:由于 var 反编译是 private static,因此会生成 getter&setter。使用 @JvmFeild 反编译是 public static,就不会生成 getter&setter。

3.2 静态工厂

interface Car {
    val brand: String
    companion object {
        operator fun invoke(type: CarType): Car {
            return when (type) {
                CarType.AUDI -> Audi()    
                CarType.BMW -> BMW()
            }
        }
    }
}

Car(CarType.BMW)

3.3 扩展方法

虽然是在伴生对象上扩展,实际相当于给外部类增加了静态方法。

fun Outter.Companion.show(){}
Outter.show()

3.4 Java互调

反编译成 Java 代码发现伴生对象是外部类持有的一个静态实例,属性和方法全都需要通过Outter().Inner.XXX来调用,想要通过 类名.XXX 调用,可以使用注解 @JvmStatic 修饰函数、@JvmField 修饰属性,就在外部类中增加了对应的 public static 字段和方法。

//Kotlin
class Demo {
    companion object Three {
        @JvmField
        var a = 1
        @JvmField
        val b = 2
        @JvmStatic
        fun three() = println("单例")
    }
//Java
class Demo {
    public static int a = 1;
    public static final int b = 2;
    public static final void three() { Three.three(); }
}

3.5 需要传参的单例

object 修饰类不能有构造,因此用 private 修饰普通类的构造,在伴生对象中创建实例并提供获取方法。以下是双重校验锁,比懒汉效率高。

class Demo private constructor(
    val name: String
) {
    companion object {
        // volatile确保多线程环境下的可见性
        @Volatile
        private var instance: Demo? = null
        fun getInstance(name: String): Demo {
            //第一次检查,避免不必要的同步
            if (instance == null) {
                synchronized(this) {
                    // 第二次检查,确保只创建一个实例
                    if (instance == null) instance == Demo(name)
                }
            }
            return instance!!
        }
    }
}
//上面 getInstance() 可简写
fun getInstance(name: String) = instance ?: synchronized(this) {
    instance ?: Demo(name).also { instance = it }
}

四、对象表达式(匿名内部类)

解决一Java 在运行时将匿名内部类当作是它所继承/实现的父类/接口来使用,因此在匿名内部类中增加了父类/父接口之外的额外方法是无法正常使用的。Kotlin 用对象表达式取代了 Java 的匿名内部类解决了这一点。
解决二

访问外部函数中的局部变量也不用加 final(详见:Lambda闭包)。对SAM(单抽接口)一般使用 Lambda 更便捷,当需要重写多个抽象方法时,只能选择匿名对象了。

4.1 赋值给变量

4.1.1 无继承无实现

val obj = object {
    init { println() }
    val name = ""
    fun show() { println() }
}

4.1.2 单个父类型

abstract class A{}
interface B{}
val obj1: A = object : A() {}
val obj2: B = object : B {}

4.1.3 多个父类型

顶层变量

成员变量

声明为 private 的变量其类型才能被正常识别(IDE显示类型为 <anonymous object : AA, BB>)。
声明为 public 的变量或当作函数返回值时,会被 Kotlin 识别为父类/父接口类型,由于有多个父类型需要显示声明为其中的一个,如果声明没有就会被识别为 Any 类型从而不能调用成员变量/方法。
局部变量不存在可见性修饰符,能被正常识别(IDE显示类型为 <anonymous object : AA, BB>)。
abstract class AA{ abstract fun aa() }
interface BB{ fun bb() }
//用作顶层变量
val obj1: AA = object: AA(), BB {
    override fun aa() {}
    override fun bb() {}
}
private val obj2 = object: AA(), BB {
    override fun aa() {}
    override fun bb() {}
}
//用作成员变量
class Demo {
    val obj3: AA = object: AA(), BB {
        override fun aa() {}
        override fun bb() {}
    }
    private val obj4 = object: AA(), BB {
        override fun aa() {}
        override fun bb() {}
    }
}
//用作局部变量
fun show(){
    val obj = object : AA(), BB {
        override fun aa() {}
        override fun bb() {}
    }
}

4.2 匿名对象传参、闭包

fun outer() {
    var num = 0    //被访问的局部可以声明为 var,而不是有 final 的 val
    val obj = object {
        fun inner() { num += 1 }
    }
}

​//Android中设置点击监听时,访问外部局部变量
fun show() {
    var num = 0
    //用对象表达式写
    xxx.setOnClickListener(object : View.OnClickListener{
        override fun onClick(v: View){ num += 1 }
    })
    //用 Lambda 简写(仅针对只有一个抽象方法的接口,多方法重写只能用对象表达式)
    xxx.setOnClickListener{ view -> num += 1 }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值