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.

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