Kotlin学习总结:类、对象和接口(四)

Kotlin学习总结:类、对象和接口(四)

“object”关键字:将声明一个类与创建一个实例结合起来

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

  • 对象声明是定义单例的一种方式。
  • 伴生对象可以持有工厂方法和其他去这个类相关,但是调用时并不依赖类实例的方法。它们的成员可以通过类名来访问。
  • 对象表达式用来体检Java的匿名内部类。

对象声明:创建单例易如反掌

在面向对象系统设计中一个相当常见的情形就是只需要一个实例的类。在Java中,这通常通过单例模式来实现:定义一个使用private构造方法并且用静态字段来持有这个类仅有的实例。
Kotlin通过使用对象声明功能为这一切提供了最高级的语言支持。对象声明将类声明与改类的单一实例声明结合到了一起。
例如,可以使用一个对象声明来表示一个组织的工资单。也许不会有多个工资单,所以使用一个对象来表示看起来是明智的:

object Payroll {
    val allEmployees = arrayListOf<User>()

    fun calculateSalary() {
        for (user in allEmployees) {
            /* ... */
        }
    }
}

对象声明通过object关键字引入。一个对象声明可以非常高效地以一句话来定义一个类和一个该类的变量。
与类一样,一个对象声明也可以包含属性、方法、初始化语句块等的声明。唯一不允许的就是构造方法(包括主构造方法和从构造方法)。与普通类的实例不同,对象声明在定义的时候就立即创建了,不需要在代码的其他地方调用构造方法。因此,为对象声明一个构造方法是没有意义的。
与变量一样,对象声明允许参加对象名加.字符的方式来调用方法和属性:

Payroll.allEmployees.add(User(""))

Payroll.calculateSalary() 

对象声明同样可以继承自类和接口。这通常在使用的框架需要去实现一个接口,但是实现并不包含任何状态的时候很有用。例如,来看看java.util.Comparator接口。Comparator的实现接收两个对象并返回一个整数来表示哪个对象更大。比较器通常来说都不存储任何数据,所以通常只需要一个单独的Comparator实例来以特定的方式比较对象。这是一个非常完美的对象声明使用场景。
使用对象来实现Comparator

object CaseInsensitiveFileComparator: Comparator<File> {
    override fun compare(o1: File?, o2: File?): Int {
        if (o1 != null && o2 != null) {
            return o1.path.compareTo(o2.path,
                ignoreCase = true)
        }
        return -1
    }
}
>>> println(CaseInsensitiveFileComparator.compare(File("/User"), File("/user")))
0

可以在任何可以使用普通对象的地方使用单例对象。例如,可以将这个对象传入一个接收Comparator的函数:

>>> val files = listOf(File("/Z"), File("/a"))
>>> println(files.sortedWith(CaseInsensitiveFileComparator))
[\a, \Z]    

这里使用了sortedWith函数,它返回一个根据特定的比较器排序过的列表。


单例和依赖注入
就像单例模式一样,在大型软件系统中使用对象声明也并不总是理想的。它们在少部分只有少量依赖或没有依赖的代码中非常好用,但在与系统中其他部分有着非常多的交互的大型组件中却不然。主要的原因就是对对象实例化没有任何控制,并且不能通过构造方法指定特定的参数。
这意味着不能在单元测试或软件系统的不同配置中替换掉对象自身的实现,或对象依赖的其他类。如果需要那样的能力,需要将依赖注入框架(譬如Guice,https://github.com/google/guice )与普通的Kotlin类一起使用,就像在Java中那样。


同样可以在类中声明对象。这样的对象同样只有一个单一实例:它们在每个容器类的实例中并不具有不同的实例。例如,在类中放置一个用来比较特定对象的比较器是合乎逻辑的。

data class Person(val name: String) {
    object NameComparator: Comparator<Person4> {
        override fun compare(o1: Person4?, o2: Person4?): Int {
            if (o1 != null && o2 != null)
                return o1.name.compareTo(o2.name)
            return -1
        }
    }
}
>>> val persons = listOf(Person("Bob"), Person("Alice"))
>>> println(persons.sortedWith(Person.NameComparator))
[Person(name=Alice), Person(name=Bob)]

在Java中使用Kotlin对象
Kotlin中的对象声明被编译成了通过静态字段来持有它的单一实例的类,这个字段名字始终都是INSTANCE。如果在Java中实现单例模式,也许也会顺手做同样的事。因此,要从Java代码使用Kotlin对象,可以通过访问静态的INSATNCE字段:

CaseInsensitiveFileComparator.INSTANCE.compare(file1, file2);

在这个例子中,INSTANCE字段的类型是CaseInsensitiveFileComparator。


伴生对象:工厂方法和静态成员的地盘

Kotlin中的类不能拥有静态成员:Java中的static关键字并不是Kotlin语言的一部分。作为替代,Kotlin依赖包级别函数(在大多数情形下能够替代Java的静态方法)和对象声明(在其他情况下替代Java的静态方法,同时还包括静态字段)。在大多数情况下,还是推荐使用顶层函数。但是顶层函数不能访问类的private成员,就像图4.5画出的那样。因此如果需要写一个可以在没有类实例的情况下调用但是需要访问类的内部的函数,可以将其写成那个类中的对象声明的成员。这种函数的一个例子就是工厂方法。
在这里插入图片描述
图4.5 私有成员不能在类外部的顶层函数中使用
在类中定义的对象之一可以使用一个特殊的关键字来标记:companion。如果这样做,就获得了直接通过容器类名称来访问这个对象的方法和属性的能力,不再需要显示地指明对象地名称。最终地语法看起来非常像Java中的静态方法调用。下面就是展示这种语法的基础示例:

class A {
    companion object {
        fun bar() {
            println("Companion object called")
        }
    }
}

>>> A.bar()
Companion object called

伴生对象可以访问类中的所有private成员,包括private构造方法,它是实现工厂模式的理想选择。
定义一个拥有多个从构造方法的类:

class User {
    val nickname: String

    constructor(email: String) {    // 从构造方法
        nickname = email.substringBefore('@')
    }

    constructor(facebookAccountId: Int) {   // 从构造方法
        nickname = getFacebookName(facebookAccountId)
    }
}

表示相同逻辑的另一种方法,就是使用工厂方法来创建类实例,这有多方面的好处。User实例就是通过工厂方法来创建的,而不是通过多个构造方法。
使用工厂方法来替代从构造方法:

class User private constructor(val nickname: String) {    // 将主构造方法标记为私有
    companion object {  // 声明伴生对象
        fun newSubscribingUser(email: String) =
            User(email.substringBefore('@'))
        fun newFacebookUser(accountId: Int) =   // 用工厂方法通过Facebook账号来创建一个新用户
            User(getFacebookName(accountId))
    }
}

可以通过类名调用companion object的方法:

>>> val subscribingUser = User.newSubscribingUser("bob@gamil.com")
>>> val facebookUser = User.newFacebookUser(4)

println(subscribingUser.nickname)
bob 

工厂方法是非常有用的。它们可以根据它们的用途来命名,就像示例中展示的那样。此外,工厂方法能够返回声明这个方法的类的子类,就像例子中的SubscribingUser和FacebookUser类。还可以在不需要的时候避免创建对象。例如,可以确保每一个email都与一个唯一的User示例对应,并且如果email在缓存中已经存在,那么在调用工厂方法时就会返回这个存在的实例而不是创建一个新的。但是如果需要扩展这样的类,使用多个构造方法也许是一个更好的方案,因为伴生对象成员在子类中不能被重写。

作为普通对象使用的伴生对象

伴生对象是一个声明在类中的普通对象。它可以有名字,实现一个接口或者有扩展函数或属性。
假设正工作在一个公司工资单的网站服务上,并且需要在对象和JSON之间序列化和反序列化,可以将序列化的逻辑放在伴生对象中。
声明一个命名伴生对象:

class Person(val name: String) {
    companion object Loader {
        fun fromJSON(jsonText: String): Person = ...
    }
}

>>> val person = Person.fromJSON("{name: 'Dmitry'}")
>>> println(person.name)
Dmitry 
>>> val person2 = Person.fromJSON("{name: 'Brent'}")
>>> println(person2.name)
Brent

在大多数情况下,通过包含伴生对象的类的名字来引用伴生对象,所有不必关心它的名字。但是如果需要也可以指明,就像上述例子中那样:companion object Loader。如果省略了伴生对象的名字,默认的名字将会分配为Companion。

在伴生对象中实现接口

就像其他对象声明一样,伴生对象也可以实现接口。可以直接将包含它的类的名字当作实现了该接口的对象实例来使用。
假设系统中有许多种对象,包括Person,想要提供一个通用的方式来创建所有类型的对象。假设有一个JSONFactory接口可以从JSON反序列化对象,并且系统中的所有对象都通过这个工厂来创建。可以为Person类提供一个这种接口的实现。
在伴生对象中实现接口:

interface JSONFactory<T> {
    fun fromJSON(jsonText: String): T
}

class Person(val name: String) {
    companion object : JSONFactory<Person> {
        override fun fromJSON(jsonText: String): Person = ... // 实现接口的伴生对象
    }
}

这时,如果有一个函数使用抽象方法来加载实体,可以传给它的Person对象。

fun <T> loadFromJSON(factory: JSONFactory<T>): T? {
    ...
}

loadFromJSON(Person)  // 将伴生对象实例传入函数中

注意,Person类的名字被当作JSONFactory的实例。


Kotlin的伴生对象和静态成员
类的伴生对象会同样被编译成常规对象:类中的一个引用了它的实例的静态字段。如果伴生对象没有命名,在Java代码中它可以通过Companion引用来访问:

/* Java */
Person.Companion.fromJSON("...");

如果伴生对象有名字,那么就有这个名字替代Companion。
注意,Kotlin可以访问在Java类中声明的静态方法和字段,使用与Java相同的语法。


伴生对象扩展

扩展函数允许定义可以通过代码库中其他地方定义的类实例调用的方法。但是如果需要定义可以通过类自身调用的方法,就像伴生对象方法或者是Java静态方法,可以通过在其上定义扩展函数来做到这一点。具体来说,如果类C有一个伴生对象,并且在C.Companion上定义了一个扩展函数func,可以通过C.func()来调用它。
假设希望Person类有一个清晰的关注点分离,这个类本身回是核心业务逻辑模块的一部分,但是不想将这个模块与任何特定的数据格式耦合起来。正因为如此,反序列化函数需要定义在模块中用来负责客户端/服务器通信。可以使用扩展函数来做到这一点。著恶意,该怎样使用默认名字(Companion)来引用没有显示地定义名字地伴生对象。
为伴生对象定义一个扩展函数:

class Person(val firstName: String, val lastName: String) {
    companion object {  // 声明一个空的伴生对象
    }
}

// client/server communication module
fun Person.Companion.fromJSON(json: String): Person { // 声明一个扩展函数
    ...
}

val p = Person.fromJSON(json)

调用fromJSON就好像它是一个伴生对象定义地方法一样,但是实际上它是作为扩展函数在外部定义的。正如之前的扩展函数一样,看起来像是一个成员,但实际并不是。但是请注意,为了能够为自己的类定义扩展,必须在其中声明一个伴生对象,即使是一个空的。

对象表达式:改变写法的匿名内部类

object关键字不仅仅能用来声明单例式的对象,还能用来声明匿名对象。匿名对象替代了Java中匿名内部类的用法。例如,将一个典型的Java匿名内部类用法——事件监听器——转换成Kotlin:
使用匿名对象来实现事件监听器:

window.addMouseListener(
    object : MouseAdapter() {   // 声明一个继承MouseAdapter的匿名对象
        override fun mouseClicked(e: MouseEvent) {  // 重写MouseAdapter方法
            // ...
        }
        
        override fun mouseEntered(e: MouseEvent) {  // 重写MouseAdapter方法
            // ...
        }
    }
)

除了去掉了对象的名字外,语法是与对象声明相同的。对象表达式声明了一个类并创建了这类的一个实例,但是并没有给这个类或是实例分配一个名字。通常来说,它们都是不需要名字的,因为会将这个对象用作一个函数调用的参数。如果需要给对象分配一个名字,可以将其存储到一个变量中:

val listener = object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { ... }
    override fun mouseEntered(e: MouseEvent) { ... }
}

与Java匿名内部类只能扩展一个类或实现一个接口不同,Kotlin的匿名对象可以实现多个接口或者不实现接口。


注意,与对象声明不同,匿名对象不是单例的。每次对象表达式被执行都会创建一个新的对象实例。


与Java的匿名类一样,在对象表达式中的代码可以访问创建它的函数中的变量。但是与Java不同,访问并没有被限制在final变量,还可以在对象表达式中修改变量的值。
从匿名对象访问局部变量:

fun countClicks(window: Window) {
    var clickCount = 0

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

注意,对象表达式在需要在匿名对象中重写多个方法时是最有用的。

小结

  • Kotlin的接口与Java的相似,但是可以包含默认实现(Java从第8版才开始支持)和属性。
  • 所有的声明默认都是final和public的。
  • 要想使声明不是final的,将其标记为open。
  • internal声明在同一个模块中可见。
  • 嵌套类默认不是内部类。使用inner关键字来存储外部类的引用。
  • sealed类的子类只能嵌套在自身的声明中(Kotlin1.1允许将子类防止在同一文件的任意地方)。
  • 初始化语句块和从构造方法为初始化类实例提供了灵活性。
  • 使用field标识符在访问器方法体中引用属性的支持字段。
  • 数据类提供了编译器生成的equals、hashCode、toString、copy和其他方法。
  • 类委托帮助避免在代码中出现许多相似的委托方法。
  • 对象声明式Kotlin中定义单例类的方法。
  • 伴生对象(与包级别函数和属性一起)替代了Java静态方法和字段定义。
  • 伴生对象与其他对象一样,可以实现接口,也可以拥有扩展函数和属性。
  • 对象表达式式Kotlin中针对Java匿名内部类的替代品,并增加了诸如实现多个接口的能力和修改在创建对象的作用域中定义的变量的能力等功能。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值