**object
关键字在Kotlin中大量出现。但是它们共用相同的核心理念:这个关键词定义一个类并且同时创建了这个类的一个实例。让我们来看看它使用的不同情况:
- 对象声明是定义一个单例的一种方法
- 伴生对象可以包含工厂方法和其他跟这个类相关但不需要类实例调用的方法。它们的成员可以通过类名访问。
- 使用对象表达式而不是Java的匿名内部类
现在我们将详细的讨论这些Kotlin特性。
4.4.1 对象声明:单例变得更容易
在面向对象系统的设计中相当常见的是你需要的一个类只有一个实例。在Java中,这个类通常使用单例模式实现:你定义了一个拥有私有构造函数和一个只保存唯一一个类实例的静态字段。
Kotlin为使用对象声明特性提供了一流的语言支持。对象声明组合了类声明以及这个类的一个单例声明。
举个例子,你可以使用一个对象声明来表示一个组织的工资单。你可能不会有多个工资单,所以为此使用一个对象听起来更加合理:
object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary() {
for (person in allEmployees) {
...
}
}
}
如你所见,对象声明使用object
关键字引入。一个对象声明有效的用一个单一的声明定义了一个类和这个类的一个变量。
就像一个类一样,一个对象声明可以包含属性、方法、初始化块等声明。唯一不允许的是构造函数(主要的或者次要的都不允许)。不同于正常的类实例,对象声明在定义的时刻立即被创建,不是通过代码中某处的构造函数调用。因此,为一个对象声明定义一个构造函数并没有意义。
跟一个类实例一样,一个对象声明允许你通过在.
符号的左边使用对象名来调用方法和访问属性。
Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()
对象声明也能继承自雷和接口。当你使用的框架要求你实现一个接口,但你的实现没有包含任何声明时,这是非常有用的。让我们拿java.util.Comparator
接口来举个例子。一个Comparator
实现接收了两个对象并返回了表示那个对象更大的一个整数。比较器几乎不保存任何数据,所以你通常只需要为比较对象的一种特定的方式准备一个单独的Comparator
实例。这是对象声明的一个完美用例。
作为一个具体的示例,让我们实现以大小写敏感的方式比较文件路径的比较器:
object CaseInsensitiveFileComparator : Comparator<File> {
override fun compare(file1: File, file2: File): Int {
return file1.getPath().compareTo(file2.getPath(),
ignoreCase = true)
}
}
>>> println(CaseInsensitiveFileComparator.compare(
... File("/User"), File("/user")))
0
你可以在普通对象可以使用的任何环境使用单例对象。举个例子,你可以把这个对象作为参数传递给接收一个Comparator
的函数:
>>> val files = listOf(File("/Z"), File("/a"))
>>> println(files.sortedWith(CaseInsensitiveFileComparator))
[/a, /Z]
这里,你使用sortedWith
函数。它根据一个具体的比较器返回一个列表。
SIDEBAR 单例和依赖注入
就像单例模式那样,对象声明使用在大型软件系统中并非总是理想的。对于依赖较少或者没有依赖的小块代码,它们是很不错的。但不适合跟系统的许多其他部分交互的大型组件。主要的原因是你没有任何对象实例化的控制,而且你不能为构造函数指定参数。
这意味着你不能取代对象自身的实现,或者在单元测试及软件系统的不同配置中,对象所依赖的其他类。如果你需要那样的能力,你应该结合一个诸如Guice的依赖注入框架来使用常规的Kotlin类,就像在Java中那样。
你也可以在类中什么一个对象。这样的对象也有只有一个单例。它们在每个容器类实例中没有单独的实例。举个例子,放置一个比较那个类内部的一个类的对象的比较器是合理的:
data class Person(val name: String) {
object NameComparator : Comparator<Person> {
override fun compare(p1: Person, p2: Person): Int =
p1.name.compareTo(p2.name)
}
}
>>> val persons = listOf(Person("Bob"), Person("Alice"))
>>> println(persons.sortedWith(Person.NameComparator))
[Person(name="Alice"), Person(name="Bob")]
SIDEBAR 通过Java使用Kotlin
Kotlin中的一个对象被编译为一个带有一个保存它的单例的静态字段INSTANCE
的类。如果你在Java中实现过单例模式,你可能已经手动做过同样的事情了。因此,为了从Java代码使用Kotlin对象,你访问这个静态的INSTANCE
字段:/* Java */
CaseInsensitiveFileComparator.INSTANCE.compare(file1, file2);
在这个例子中,INSTANCE
字段为CaseInsensitiveFileComparator
类型。
现在,让我们来看一个特殊的案例。对象嵌套在一个类的内部:伴生对象。
4.4.2 伴生对象:一个放置工厂函数和静态成员的地方
在Kotlin中,类不能有静态成员。Java的static
关键字不是Kotlin语言的一部分。作为一个替代,Kotlin依赖于包级别的函数(在许多情况下可以替代Java的静态函数)和对象声明(替代Java的静态方法,在某些情况下也可以作为静态字段)。在大部分情况,推荐你使用包级别函数。但是包级别函数无法访问类的私有成员。因此,如果你需要写一个可以无需拥有一个类实例时被调用,但需要访问类的内部的函数,你可以在类的内部把它写成一个对象的成员。所有这样的函数的例子都可以是一个工厂方法。
图4.5 私有成员无法使用在顶层,除非这个函数在类的外部
定义在类中的一个对象可以用一个特殊的关键字来标记:companion
。如果你这样做的话,你就获得了通过所在的类的名字直接访问这个对象的方法和属性的能力。而你无需显式的指定这个对象的名字。最终的语法看上去像极了Java的静态方法调用。这是一个演示语法的基本案例:
class A {
companion object {
fun bar() {
println("Companion object called")
}
}
}
>>> A.bar()
Companion object called
还记得我们承诺过你一个放置调用私有构造函数的地方吗?那就是伴生对象了。伴生对象拥有访问类的所有私有成员的权限。它是工厂模式的一个理想候选方案。
来看一个声明两个构造函数并将其改为在伴生对象使用工厂方法的例子。我们将会使用前面的FacebookUser
和SubscribingUser
来构建一个案例。之前,这些实体是实现了公共的User
接口的不同的类实例。现在你决定只使用一个类来管理。但是你将提供不同的方法来创建它:
class User {
val nickname: String
constructor(email: String) { // 次构造函数
nickname = email.substringBefore('@')
}
constructor(facebookAccountId: Int) { // 次构造函数
nickname = getFacebookName(facebookAccountId)
}
}
表达同样逻辑的一个可选方法,是使用工厂方法来创建这个类的实例。这也许是出于多个有利因素的考虑。User
实例是通过工厂方法创建的,而不是通过多构造函数:
class User(val nickname: String) {
companion object { // 声明伴生对象
fun newSubscribingUser(email: String) = // 工厂方法通过email创建一个新的用户
User(email.substringBefore('@'))
fun newFacebookUser(accountId: Int) = // 工厂方法通过Facebook账号ID创建一个新的用户
User(getFacebookName(accountId))
}
}
你可以通过类名来调用伴生对象的方法:
>>> val subscribingUser = User.newSubscribingUser("[email protected]")
>>> val facebookUser = User.newFacebookUser(4)
>>> println(subscribingUser.nickname)
bob
工厂方法是非常有用的。它们可以根据它们的用途来命名,就像例子中展示的那样。另外,当SubscribingUser
和FacebookUser
是类时,正如例子中那样,一个工厂方法可以返回声明工厂方法的类的子类。当这不是必要的时候,你也可以避免创建新的对象。例如,你可以确保每个邮箱地址对应一个不同的User
实例,并且,当使用一个邮箱地址调用一个存在于缓存的工厂方法时,返回一个存在的实例而不是一个新的实例。但是如果你需要扩展这样的类,使用多个构造函数或许是一个更好的方案。因为伴生对象并不能在子类中被覆盖。
4.4.3 伴生对象是常规的对象
伴生对象是声明在一个类中的常规对象。它可以被命名、可以实现一个接口,或者拥有扩展函数或属性。在这一节,我们来看一个例子。假定你在为一个公司的工资单web服务而工作。你需要把对象序列化和反序列化成JSON。你可以在一个伴生对象中添加序列化逻辑。
class Person(val name: String) {
companion object Loader {
fun fromJSON(jsonText: String): Person = ...
}
}
>>> person = Person.Loader.fromJSON("{name: 'Dmitry'}") // 你可以使用两种方式来调用fromJSON方法
>>> person.name
Dmitry
>>> person2 = Person.fromJSON("{name: 'Brent'}") // 你可以使用两种方式来调用fromJSON方法
>>> person2.name
Brent
在大多数情况下,你通过包含它的类的名字来引用伴生对象。所以你不需要担心它的名字。但是你可以指定它的名字,如果有需要的话,就像在例子中那样:companion object Loader
。如果你忽略了伴生对象的名字,分配给它的默认名称是Companion
。当我们讨论伴生对象扩展时,你将会后面看到一些使用这个名字的例子。
在伴生对象中实现接口
就像其他对象声明那样,一个伴生对象那个可以实现接口。正如你将看到的那样,你可以直接使用容器类的名字作为一个实现接口的对象实例。
假定在你的系统中你有多种对象,包括Person
。你想提供一个普通的方法来创建所有类型的对象。你有一个用于能够从JSON反序列化的对象的JSONFactory
接口。你的系统中所有的对象都能够通过这个工厂来创建。你可以为你的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 loadFromJSON<T>(factory: JSONFactory<T>): T {
...
}
loadFromJSON(Person) // 传递伴生对象给这个函数
注意,Person
类的名字被用作一个JSONFactory
实例,
SIDEBAR Kotlin伴生对象和静态成员
类的伴生对象被编译成一个常规对象:类里面的一个指向自己的实例的静态字段。如果伴生对象没有被命名,它可以从Java代码通过Companion
引用来访问:/* Java */
Person.Companion.fromJSON("...");
如果一个伴生对象有一个名字,你应该使用这个名字而不是Companion
。
但是你可能需要使用要求类的某个成员是静态的Java代码。你可以对相应的成员使用@JvmField
标注来实现这个要求。如果你想声明一个static
字段,对一个顶层属性或者声明在对象中的属性使用@JvmField
。这些特性的存在,只是为了达到互操作性的目的。严格来讲,不是语言内核的一部分。我们将在第10章详细覆盖标注的内容。
注意,Kotlin可以使用跟Java中一样的语法来访问声明在Java类中静态方法和属性。
伴生对象扩展
正如你在3.3一节看到的那样,扩展函数允许你定义能够被定义在代码的任意地方的类实例调用的方法。但是,如果你需要定义可以在类的内部被调用的函数,像伴生对象方法或者Java静态方法,该怎么办呢?如果这个类有一个伴生对象,你可以通过为它定义扩展函数来达到这个目的。更具体些是,如果类C
有一个伴生对象,你在类C
中定义了一个扩展函数fun
,那么你可以写成C.func()
来调用它 。
举个例子,想象一下,你想要为你的Person
类有一个更清晰的关注分离。这个类本身将会成为核心业务逻辑模块的一部分。但是你不想把这个模块跟任何其他具体的数据格式耦合在一起。因此,需要在这个模块定义反序列化函数来负责客户端/服务端通信。你可以使用这个扩展函数来完成这个任务。注意,你如何使用默认名称(Companion
)来引用伴生对象是不需要显式名称来声明的:
// business logic module
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
函数,是因为它被定义在伴生对象内部。但它是伴生对象的一个扩展。由于它经常带有扩展函数,伴生对象看上去像一个成员,但它并不是。为了能够给它定义一个扩展,注意,你必须在类的内部声明一个伴生对象,即使是空的。
你已经看到了伴生对象可以是多么有用。现在让我们继续下一个特性,在Kotlin中同样用object
关键字来表达:对象表达式。
4.4.4 对象表达式:匿名内部类的另一种表达方式
object
关键字不仅可以被用在声明像单例那样的命名对象,也能用来声明匿名对象(anonymous objects)。匿名对象取代了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匿名对象可以实现多个接口或不实现(尽管后者不是非常有用)。
NOTE Note
不像对象声明,匿名对象不是单例。对象表达式每次执行,都会创建这个对象的一个新的实例。
跟Java的匿名类一样,对象表达式中的代码可以访问创建它的函数中的变量。但跟Java不同的是,这并不局限于final
变量。你也可以从一个对象表达式的内部修改变量的值。
举个例子,让我们来看一看你可以如何使用侦听器来统计窗口点击的次数:
fun countClicks(window: Window) {
var clickCount = 0 // 声明一个本地变量
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++ // 更新变量的值
}
})
// ...
}
NOTE 注意
当你需要在你的匿名对象中覆盖多个方法时,对象表达式是最有用的。如果你只需要实现一个单一方法的接口(比如Runnable
),你可以依靠Kotlin对SAM转换(将一个函数字面量转换为带有一个单一抽象方法的接口实现)的支持和编写你的实现作为一个函数字面。我们将会在第5章更加详细的讨论lambda和SAM转换。
我们已经完成了有关类、接口和对象的讨论。在下一章,我们将会继续Kotlin最有趣的一个领域:lambda和函数式编程。