[Kotlin]详解Object关键字

5,144 阅读5分钟

Kotlin中object关键字在多种情况下出现,但是他们都遵循同样的核心理念:这个关键字定义一个类并同时创建一个实例对象。使用它的不同场景:

  1. 对象声明:定义单例对象
  2. 伴生对象:可以持有工厂方法和其他与这个类相关,但在调用时并不依赖类实例的方法。他们的成员可以通过类名来访问。
  3. 对象表达式:替代Java的匿名内部类。

有时候,我们需要创建一个对某个类做了轻微改动的类的对象,而不用为之显式声明新的子类。 Kotlin 用对象表达式和对象声明处理这种情况。

Demo传送门,求个Star

对象声明

对象声明可以用来轻易实现单例模式,它隐式创建一个类并返回类的对象,我们称之为Single Object(单例对象),object声明的对象可以拥有自己的方法和属性:

//onject声明的对象可以拥有方法和自己的属性,简称为对象声明
object DataClass{
    fun getData(){

    }

    val data = 1
}

同时可以实现接口或者继承其他类:

object DataClass2 : A {
    
}

具体使用:

import `interface`.People

class TestObject {

    /**
     * 单例对象
     */
    object SingleObject{
        //object声明的对象可以拥有方法和自己的属性
        var num = 0

        fun totalNum():Int{
            num += 1
            return num
        }
    }

    /**
     * 单例对象可以有超类(实现接口或者继承其他类)
     */
    object people:People{
        override fun setKind(): String {
            return "American"
        }

        override val name: String
            get() = "tony"
    }

}



fun main(){
    //调用单例对象
    System.out.println(TestObject.SingleObject.num)
    System.out.println(TestObject.SingleObject.totalNum() == 1)
    System.out.println(TestObject.SingleObject.totalNum() == 1)
    System.out.println(TestObject.SingleObject.totalNum())

    System.out.println(TestObject.people.setKind())
    System.out.println(TestObject.people.name)
}

输出:

0
true
false
3

American
tony

所以称对象声明为单例对象更加合适点,其就是生成单例模式的class对象,它拥有方法和属性。


伴生对象

类内部的对象声明可以用 ==companion== 关键字标记:

class CompainClass{
    companion object Factory{
        
    }
}

该伴生对象的成员可通过只使用类名作为限定符来调用:

val instance = CompainClass.creatFartory()

伴生对象也可以省略对象名称:

class CompainClass2{
    companion object
}

伴生对象可以实现其他超类型:

class CompainClass3{
    companion object : A{
        override val name: String
            get() = "伴生对象实现接口A"

    }
}

使用的时候就可以省略对象名称,直接用类名代替啦:


fun main(){

    val instance = CompainClass.creatFartory()
    val instance2 = CompainClass2.Companion
    val instance3 = CompainClass2
    println(CompainClass3.name)
    
}

具体使用实例:


import `interface`.People

class CompanionObject {
    //外部类私有属性
    private var name:String = "xlu"

    fun getMyName():String{
        //外部类可以访问伴生对象的私有属性
        return myName
    }

    companion object test{
        //伴生对象的私有属性
        private var myName:String = "companion xlu"
        //JvmStatic注解生成static成员
        @JvmStatic var test = "static"

        fun getCompaionName():String{
            val objects = create()
            //伴生对象可以访问外部类的私有属性
            return CompanionObject().name
        }

        fun create():CompanionObject{
            return CompanionObject()
        }
    }

    /**
     * 一个类最多一个伴生对象,以下会报错
     */
    companion object second:People{
        //伴生对象可以有超类型
        override fun setKind(): String {
            return super.setKind()
        }
    }
}

fun main(){
    System.out.println(CompanionObject.test)
    System.out.println(CompanionObject.getCompaionName())
}

伴生对象可以访问外部类的私有属性,外部类方法可以访问伴生对象私有属性。他们似乎没有什么约束,为什么这么设计呢?

我们在上面讲述过了对象声明就是一种单例模式的class对象,那么companion object可以理解是class为名称的单例对象,它任然是class的成员对象。

  1. 伴生对象很像java中的static静态成员,kotlin是没有static这个概念的
  2. kotlin中可以用顶层函数和伴生对象替代static
  3. 可以用@JvmStatic生成真正的静态成员
  4. 伴生对象和外部类的私有属性可以互相访问

对象表达式:

一个对象:继承父类、抽象类、接口的匿名类的对象

定义方式:

object:父类型{
    
}

如果我们只需要“一个对象”,并不需要特殊超类型,那么我们可以简单地写:

fun foo() {
    val adHoc = object {
        var x: Int = 0
        var y: Int = 0
        
        fun add(){
            x++
            y++
        }
    }
    print(adHoc.x + adHoc.y)
}

实际应用:

class ObjectExpression {
    /**
     * 匿名类的对象,简称“对象表达式”
     */
    var people = object :Action(){
        override fun eat(): String {
            return "油条"
        }

        override fun run() {
            TODO("Not yet implemented")
        }
    }

    fun main(){
        people.eat()
        people.run()
    }

}

abstract class Action{
    abstract fun eat():String

    abstract fun run()

    fun sleep(){
        //do something
    }

}

如果你不了解匿名类,建议先去了解下java中的匿名类。

如果将对象表达式作为函数的返回对象,其类型不一定是匿名类的真实类型

  1. 私有函数(private fun)修饰:返回的是匿名类真实类型
  2. 公有函数(public fun)修饰:返回的是父类型或者Any()

实例一:

class C {
    // 私有函数,所以其返回类型是匿名对象类型
    private fun foo() = object {
        val x: String = "x"
    }

    // 公有函数,所以其返回类型是 Any
    fun publicFoo() = object {
        val x: String = "x"
    }

    fun bar() {
        val x1 = foo().x        // 没问题
        val x2 = publicFoo().x  // 错误:未能解析的引用“x”
    }
}

实例二:

class ObjectExpression2 {

    //私有方法
    private fun A() = object : Parent(){
        val x:String = "x"
    }

    //公有方法
    public fun B() = object : Parent(){
        val x:String = "x"
    }

    fun test(){
        val objectPrivate = A()
        val objectPublic = B()

        //私有方法返回的是匿名类的真实类型,可以访问x和y对象
        System.out.println(objectPrivate.x + objectPrivate.y)

        //公有方法返回的是父类型对象,Parent()或者Any(),只能访问父类型的对象
        System.out.println(objectPublic.y)
        //访问x报错
        System.out.println(objectPublic.x)
    }
}

open class Parent{
    val y:String = "y"
}

定义在函数中的对象表达式可以访问函数内的属性

fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0

    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }

        override fun mouseEntered(e: MouseEvent) {
            enterCount++
        }
    })
    // ……
}

对象表达式和对象声明之间的语义差异

  • 对象表达式是在使用他们的地方立即执行(及初始化)的;
  • 对象声明是在第一次被访问到时延迟初始化的;
  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配。

如果有错误和不同的地方,请大佬们指出