kotlin 中 object 关键字的使用详解

1. 前言

object 关键字在 kotlin 中有两种使用场景:对象表达式 (object expressions)和对象声明(object declarations)。本文将对这两种使用场景分别说明。

2. 正文

2.1 对象表达式(object expressions)

创建继承某个(或某些)类型的匿名类的对象,这些类型可以是接口(以给 Button 设置点击事件为例):

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener(object: View.OnClickListener {
            override fun onClick(v: View?) {
                Log.d(MainActivity::class.java.simpleName, "click button")
            }
        })
    }
}

可以是抽象类(以属性动画中的 AnimatorListenerAdapter 为例):

val valueAnimator = ValueAnimator.ofInt(0, 5)
valueAnimator.addListener(object: AnimatorListenerAdapter() {
    override fun onAnimationStart(animation: Animator?) {
        super.onAnimationStart(animation)
        Log.d(MainActivity::class.java.simpleName, "onAnimationStart")
    }
    override fun onAnimationEnd(animation: Animator?) {
        super.onAnimationEnd(animation)
        Log.d(MainActivity::class.java.simpleName, "onAnimationEnd")
    }
})
valueAnimator.duration = 2000L
valueAnimator.start()

那么,超类型是接口和类的区别和联系是什么呢?

相同的地方是都是要把 object 关键字放在前面,后面加:,再加上接口或者类,在花括号里重写方法;
不同的地方是接口后面没有小括号,而类后面必须有小括号。

从上面的对象表达式的用法可以看到,它和 Java 中的匿名内部类的作用是可以对应得上的。

但是,如果完全把 object 对象表达式的用法和 Java 中的继承一个父类或者实现一个接口的匿名内部类对应,就太局限了。

实际上,对象表达式要更加强大

对象表达式可以同时继承一个类以及多个接口,或者同时继承多个接口,匿名内部类却不能这样

open class Man(_name: String) {
    val name = _name
}

interface Flyable {
    fun fly()
}

interface SuperHearing {
    fun hearSubtleNoises()
}

interface SuperSmell {
    fun trackBySmell()
}

interface SuperVision {
    fun seeThroughWalls()
}

fun main() {
    val superMan = object: Man("Clark"), Flyable, SuperHearing, SuperVision {
        override fun hearSubtleNoises() {
        }
        override fun seeThroughWalls() {
        }
        override fun fly() {
        }
    }
        val superPower = object: Flyable, SuperHearing,SuperVision {
        override fun fly() {
        }

        override fun hearSubtleNoises() {
        }

        override fun seeThroughWalls() {
        }

    }
}

对象表达式后面可以不跟任何类或者接口,这时它仅仅表示一个对象,封装一些数据和方法,而匿名内部类不能这样

class C {
    private fun foo() = object {
        val x: String = "x"
        fun getValue() = 5
    }

    private val goo = object {
        val x: String = "x"
        fun getValue() = 5
    }
    fun publicFoo() = object {
        val x: String = "y"
        fun getValue() = 6
    }

    val publicGoo = object  {
        val x: String = "y"
        fun getValue() = 6
    }


    fun bar() {
        val foo = foo()
        println(foo().x) // 打印:x
        println(foo.getValue()) // 打印:5
        println(goo.x) // 打印:x
        println(goo.getValue()) // 打印:5
        val publicFoo: Any = publicFoo() // 这里可以类型推导出是 Any 类型,我把它明显写出来,是为了说明后两行报错的原因
        // publicFoo.x // ERROR: Unresolved reference 'x'
        // publicFoo.getValue() // ERROR: Unresolved reference
        val goo: Any = publicGoo // 这里可以类型推导出是 Any 类型,我把它明显写出来,是为了说明后两行报错的原因
        // publicGoo.x // ERROR: Unresolved reference 'x'
        // publicGoo.getValue() // ERROR: Unresolved reference
    }
}

fun main() {
    val c = C()
    c.bar()
}

从上面的代码可以看出,只能把匿名对象用在局部或者私有的声明中。

对象表达式可以在封闭域中直接获取变量

val button = Button(this)
var clickCount = 0
button.setOnClickListener(object: View.OnClickListener {
    override fun onClick(v: View?) {
        clickCount++
    }
})

2.2 对象声明(object declarations)

2.2.1 对象声明

对象声明,就是在 kotlin 中声明单例的方式:

object DataRepository {
    var data = listOf(1,2,3)
}

对象声明的特点是 object 关键字后面跟着一个名称。就像声明一个变量那样,但是对象声明不是一个表达式,不能把对象声明放在赋值语句的右边。

利用 AndroidStudio 的 Show Kotlin Bytecode 功能,看一下对应的 .java 文件:

public final class DataRepository {
   @NotNull
   private static List data;
   public static final DataRepository INSTANCE;

   @NotNull
   public final List getData() {
      return data;
   }

   public final void setData(@NotNull List var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      data = var1;
   }

   static {
      DataRepository var0 = new DataRepository();
      INSTANCE = var0;
      data = CollectionsKt.listOf(new Integer[]{1, 2, 3});
   }
}

可以看到确实是一个单例模式的形式,是饿汉式的单例。这种形式的单例是线程安全的。
使用对象声明的方式:直接使用名称调用即可。代码如下:

Log.d(MainActivity::class.java.simpleName, DataRepository.data.toString())

需要注意的是,对象声明不能在局部作用域(即直接嵌套在函数内部),但是可以嵌套在其他对象声明或非内部类中。

对象声明不能在局部作用域

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        object Singleton { }// 这里编译报错:Named object 'Singleton' is a singleton and cannot be local. Try to use anonymous object instead

    }
}

对象声明嵌套在其他对象声明中

这里是把 Singleton 对象声明嵌套在 DataRepository 中:

object DataRepository {
    var data = listOf(1,2,3)
    object Singleton {
        
    }
}

对象声明可以在非内部类

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    object Singleton {}
    
}

对象声明不可以在内部类中

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    inner class InnerClass { // 在 kotlin 中声明内部类,必须显式地加上 inner 关键字
        object Singleton {} // 编译报错:Object is not allowed here
    }
}

这一点需要解释一下,因为报错信息只是说 Object 在这里不允许,没有说明为什么不允许。
我们知道 Singleton是一个饿汉式的单例,它编译成的 java 代码会有一个静态的属性。
静态类型的属性随类的加载而加载;而 InnerClass 是一个内部类,或者说是一个成员内部类,成员内部类必须先实例化外部类对象然后再实例化成员内部类。这里就产生了矛盾,根据静态类型来看,是可以访问的,根据成员内部类来看,还不能访问。

为了进一步理解,我们写出对应的 java 代码如下:

public class ClassA {
    class InnerClass {
        class Singleton {
        	 // 编译报错:static 下方会出现红色波浪线,信息为  Inner classes cannot have static declaration
        	 // new Singleton() 下方会出现红色波浪线,信息为 ClassA.InnerClass.this cannot be referenced from a static context
            private static final Singleton INSTANCE = new Singleton();
            private Singleton() {
            }
            public static Singleton getInstance()  {
                return INSTANCE;
            }
        }
    }
}

因此,不能在内部类中写对象声明。

对象声明可以在静态内部类中

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    class InnerClass { 
        object Singleton {}
    }
}

所以,可以看到:使用对象声明,可以非常容易地声明一个单例。
思考一下:对象声明和普通类的实例有什么区别?
对象声明不允许有构造方法,而普通类的实例获取需要构造方法。

对象声明可以有超类型

data class Person(val name: String, val age: Int)
object PersonComparator : Comparator<Person> {
    override fun compare(o1: Person, o2: Person): Int {
        return o1.age.compareTo(o2.age)
    }
}

2.2.2 伴生对象(companion object)

类内部的对象声明可以用 companion 关键字标记,这就是伴生对象。伴生对象在一个类中,只能声明一个。
这里使用自定义的 Application 作为例子:

class App : Application() {
    companion object {
        private var instance: Application? = null
        fun instance() = instance!!
    }

    override fun onCreate() {
        super.onCreate()
        instance = this
    }
}

这个伴生对象的成员,可以通过类名. 的方式调用:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val instance = App.instance()
    }
}

在类中定义的对象声明和伴生对象的区别是什么呢?
对于对象声明,object关键字后面必须有一个名字;而伴生对象的名称可以省略,这种情况下它的名称就是 Companion,也可以显式地有一个名字。
对于对象声明,在类内部可以声明多个;而伴生对象只能声明一个。
对于对象声明,获取它的成员必须是所在类的名字.对象声明的名字.成员名;而伴生对象则是所在类的名字.成员名即可。

伴生对象在开发中常常用来替代 Java 中的 static 方法或字段

我们以启动一个 Activity的静态方法为例来说明

Java 代码是这样的:

public class MyActivity extends AppCompatActivity {

    public static final String TAG = "MyActivity";
    public static void start(Context context) {
        Intent starter = new Intent(context, MyActivity.class);
        context.startActivity(starter);
    }
}

Kotlin 代码是这样的:

class MyActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MyActivity"
        @JvmStatic // 如果在 Java 代码中调用此方法,必须加上这个注解,才能编译出静态方法
        fun start(context: Context) {
            val starter = Intent(context, MyActivity::class.java)
            context.startActivity(starter)
        }
    }
}

3. 最后

这篇文字简单介绍了 object 关键字的使用,希望对大家有帮助。

参考

1.Object Expressions and Declarations.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章