9.4 Kotlin的OOP功能

Kotlin首先是一种OOP语言,本节将全面介绍Kotlin的OOP基本知识:

  • 定义包;
  • 导入成员;
  • 定义类和构造函数;
  • 给类添加成员;
  • 继承;
  • 可见性限定符;
  • 单例对象和伴生对象;
  • 数据类;
  • lambda和内联函数。

9.4.1 定义包

包是使用package语句定义的,这种语句的工作原理与Java中很像:

  1. package com.example

不同于Java和Clojure,在Kotlin中,源代码的目录结构无需与包名匹配;换言之,你可以随心所欲地组织源代码。

9.4 Kotlin的OOP功能 - 图1 请不要在Kotlin交互式REPL中使用package语句,因为它不支持创建包。

9.4.2 导入成员

Kotlin import语句与Java import语句很像:

  1. import java.util.ArrayList
  2. import java.io.*

一个重要的不同是可指定别名,这在可能发生名称冲突的情况下提供了极大的便利:

  1. import java.io.File as JavaFile
  2. val f = JavaFile("test.txt")

9.4.3 定义类和构造函数

类是使用关键字class定义的:

  1. class ClassName {
  2. }

可在类头中指定主构造函数:

  1. class Point constructor(x: Int, y: Int) {
  2. }

关键字constructor可以省略:

  1. class Point (x: Int, y: Int) {
  2. }

如果要在实例化类时执行一些代码,可使用关键字init指定一个代码块:

  1. class Point (x: Int, y: Int) {
  2. init {
  3. println("Executable code here...")
  4. }
  5. }

init代码块中,可使用构造函数的参数以及当前类中定义的属性(至于如何添加属性,稍后将介绍)。对于要在方法中使用的构造函数参数,必须给它们加上前缀val(用于不可变属性)或var(用于可变属性):

  1. class Point (val x: Int, val y: Int) {
  2. override fun toString(): String { return "${x}, ${y}" }
  3. }
  4. val p = Point(-30, 50)
  5. println(p)

这将打印-30, 50。通常,最好使用val将构造函数参数设置为不可变的,因此除非出于设计考虑,否则在使用var将构造函数设置为可变的之前一定要三思而后行。

指定了关键字constructor时,还可给主构造函数指定访问限定符,但在省略了关键字constructor的情况下,无法这样做(在这种情况下,构造函数默认为公有的):

  1. class Customer private constructor(id: Int) { }

在没有显式地定义构造函数的情况下,将自动生成一个默认构造函数,这个构造函数是公有的且不接受任何参数。如果你不希望自动创建这样的构造函数,可显式地定义一个不接受任何参数的私有构造函数,如下所示:

  1. class Customer private constructor()

可使用的访问限定符将在后面介绍。还可添加一个或多个辅助构造函数:

  1. class Customer(val name: String, val country: String?) {
  2. constructor(name: String) : this(name, null) {
  3. println("Name: " + name)
  4. println("Country: " + country)
  5. }
  6. }
  7. var c = Customer("Your Name")

上述代码片段将打印Name: Your nameCountry: null。辅助构造函数必须直接(如上例所示)或间接地调用主构造函数。所谓间接地调用主构造函数,指的是调用另一个辅助构造函数,而这个辅助构造函数直接或间接地调用了主构造函数。

9.4.4 给类添加成员

下面来介绍如何给类添加成员:

  • 添加函数;
  • 添加入口函数main
  • 添加属性。

  • 添加函数

在Java中,类中的函数被称为方法,但在Kotlin中,它们也被称为函数。由于函数在前面介绍过,因此添加函数的方式完全在意料之中:

  1. class MethodDemo {
  2. fun instanceMethod(i: Int): Int {
  3. return i*i
  4. }
  5. var demo = MethodDemo()
  6. println(demo.instanceMethod(5))

有趣的是,Kotlin没有定义静态方法(类方法)的关键字。作为一种替代办法,你可将函数放在类外面(这个主题将在9.5节讨论)。你也可以使用伴生对象来生成静态函数,这也将在本章后面讨论。

  • 入口函数main

你知道,在Java中,可添加static main(String[] args),它将作为应用程序的入口。在Kotlin中,类中的方法自动为实例方法,因此在类中添加如下函数不管用:

  1. // 下面的main()函数是普通的实例方法
  2. // 不能作为应用程序的入口
  3. class A {
  4. fun main(args : Array<String>) {
  5. println("Executable code here...")
  6. }
  7. }

在Kotlin中,定义入口main()的方法有两种。

  1. - 将函数<code>main()</code>放在源代码的最顶层(不在任何类中),这将在9.5节讨论。
  2. - 将函数<code>main()</code>放在一个伴生对象中,并添加<code>@JvmStatic</code>注解,这将在9.4.8节演示。
  • 添加属性

在Kotlin中,类不能包含独立的变量,但可包含属性。属性是有配套获取函数和/或设置函数的变量(具体情况取决于属性是否是可读和/或可写的)Kotlin可为属性生成默认的获取/设置函数,但你也可提供获取/设置函数的实现。

下面是一个可变(可读写)的属性;这个属性之所以是可变的,是因为声明它时使用了关键字var

  1. class PropertyDemo1 {
  2. var mutableProperty: Int = 0
  3. }
  4. val p1 = PropertyDemo1()
  5. println(p1.mutableProperty)
  6. p1.mutableProperty = 24
  7. println(p1.mutableProperty)

属性mutableProperty被初始化为0。由于没有显式地提供获取函数和设置函数的实现,Kotlin编译器将自动为这个属性生成获取函数和设置函数。要访问这个属性,只需使用属性名,而无需添加前缀get或set。Kotlin自动调用生成的获取函数和设置函数,这些函数在Kotlin中被称为存储函数(accessor)。

9.4 Kotlin的OOP功能 - 图2 Kotlin编译器要求要么在声明属性的同时对其进行初始化(如前面的示例所示),要么在init { }块中对其进行初始化。

下面是一个只读的属性;只读属性是使用关键字val声明的:

  1. class PropertyDemo2 {
  2. val readOnlyProperty: Int = 1000
  3. }
  4. val p2 = PropertyDemo2()
  5. println(p2.readOnlyProperty)

如果你试图执行类似于前一个示例的代码p2.readOnlyProperty = 1234,将引发类似于下面的异常:java.lang.IllegalAccessError: tried to access field Line35 $PropertyDemo2.readOnlyProperty from class Line39。像可变属性一样,对于只读属性,也必须在声明时显式地初始化或在init { }块中初始化。

前面说过,可显式地给属性定义存取函数(获取函数和设置函数),下面就是一个这样的可变属性:

  1. class PropertyDemo3 {
  2. var customProperty: Int = 1000
  3. get() { field + 1 }
  4. set(value) { field = value }
  5. p3.customProperty = 10
  6. println(p3.customProperty)

关键字field用于访问生成的字段,Kotlin文档称之为支持字段(backing field)。

可将设置函数设置为私有的以隐藏它:

  1. class PropertyDemo4 {
  2. var anotherProperty: Int = 314
  3. private set
  4. }
  5. var p4 = PropertyDemo4()
  6. println(p4.anotherProperty)

属性anotherProperty 确实有设置函数(由Kotlin提供的默认实现),但由于它是私有的,因此被隐藏了。但与往常一样,获取函数被自动创建且是公有的。然而,你不能将获取函数设置为私有的;获取函数必须与属性的访问限定符匹配。在Kotlin中,访问限定符被称为可见性限定符,将在后面更详细地讨论。

9.4.5 继承

与大多数流行的JVM语言一样,Kotlin也只允许类最多扩展一个超类。对于没有显式地继承其他类的类,将继承Kotlin类kotlin.Anykotlin.Any类似于Java类java.lang.Object,但需要注意的是,它们是两个不同的类。

创建时没有显式指定限定符的类都是final的,不能继承。要让类能够被继承,必须在定义它时在前面加上关键字open

  1. open class Person(name: String)
  2. class Customer(name: String, department: String) : Person(name) {
  3. }

如果子类没有主构造函数,要调用父类的构造函数,必须使用关键字super

  1. open class Person(name: String)
  2. class Customer : Person {
  3. constructor(name: String) : super(name)
  4. }

要继承方法,必须使用关键字override

  1. open class ParentClass {
  2. open fun greatMethod() {
  3. println("greatMethod in parent class")
  4. }
  5. }
  6. class ChildClass: ParentClass() {
  7. override fun greatMethod() {
  8. super.greatMethod()
  9. }
  10. }

请注意,除非显式地使用了访问限定符open,否则函数默认为final的。

9.4.6 接口

Kotlin的接口类似于Java 8中的接口,可包含抽象函数(没有实现的函数)和具体函数(带默认实现的函数):

  1. interface NameOfInterface {
  2. fun functionWithoutImplementation()
  3. fun functionWithImplementation(i: Int) {
  4. // 下面是默认实现……
  5. }
  6. }

在Kotlin接口中,还可声明属性(这些属性可以有显式的获取函数,也可以没有):

  1. interface InterfaceWithProperties {
  2. var propertyWithGetterAndSetter: Int
  3. val propertyWithGetterOnly: String
  4. val propertyWithDefaultImplementation: Double
  5. get() = 0.0
  6. }

接口不可能包含支持字段,因此在接口中不能使用关键字field。有鉴于此,对于接口中的属性,无法给它显式地提供设置函数实现。

Kotlin使用相同的语法来实现接口和继承类:

  1. class DemoClass : NameOfInterface, InterfaceWithProperties {
  2. override fun functionWithoutImplementation() {
  3. println("but now it has a implementation")
  4. }
  5. override var propertyWithGetterAndSetter: Int = 0
  6. override val propertyWithGetterOnly: String = "test"
  7. }

类和接口的排列顺序无关紧要,但如果有超类,最好将它放在最前面,然后再指定类实现的接口。

9.4.7 可见性限定符

在Kotlin中,可在类外定义函数和属性;类外被称为包的顶层(top level),将在9.5节更详细地介绍。对于顶层声明和类成员,可使用的可见性限定符(在Java中被称为访问限定符)不同。下表列出了Kotlin支持的可见性限定符。

可见性限定符 可用于 描述
public 顶层声明和类成员 这是在Kotlin中没有显式指定可见性限定符时默认使用的可见性限定符,其函数与Java访问限定符public相同:声明可在任何地方使用
private 顶层声明和类成员 相应的定义仅对当前文件中的代码可见
internal 顶层声明和类成员 相应的定义仅对当前模块中的代码可见。模块可以是一组一起编译的Kotlin文件,如特定项目的所有Kotlin源代码文件
protected 仅限于类成员 与Java中相同:成员仅对当前类及其子类可见,对其他类都不可见

在Java中,没有指定访问限定符时,成员默认是包私有的,但Kotlin没有与之对应的可见性限定符。

9.4.8 单例对象和伴生对象

Kotlin关键字object与Scala关键字object很像,也用于创建单例:

  1. object ThisIsASingleton {
  2. fun coolMethod() = println("Not so cool, after all")
  3. }
  4. ThisIsASingleton.coolMethod()

将自动创建这个类的一个实例,并可通过对象名来访问其成员。

可在类中创建单例对象,为此需要添加前缀companion

  1. class NormalClass {
  2. companion object CompanionObject {
  3. var i = 100
  4. fun yetAnotherCoolMethod() {
  5. i = 50
  6. }
  7. }
  8. }
  9. NormalClass.CompanionObject.yetAnotherCoolMethod()
  10. println(NormalClass.i)
  11. println(NormalClass.CompanionObject.i)

正如这里演示的,要访问伴生对象的成员,可通过引用NormalClass.Companion Object,也可只使用类名NormalClass(因为它也指向伴生对象CompanionObject)。上述代码片段将打印50两次。

9.4 Kotlin的OOP功能 - 图3 如果没有显式地指定伴生对象的名称,访问其成员时可指定伴生对象。在这种情况下,Kotlin将伴生对象称为Companion。

要访问伴生对象,只能通过其所属的类(这里为NormalClass),而不能通过引用变量来访问。因此,下面的代码无法通过编译:

  1. var i = Normalclass()
  2. // 无法通过编译
  3. i.CompanionObject.yetAnotherCoolMethod()

这些代码将引发如下异常:error: nested companion object 'CompanionObject' accessed via instance reference

之所以会出现这种错误,是因为伴生对象很像Java中的静态成员。由于伴生对象是单例对象,因此它只有一个实例,由其所属类的所有实例共享。为让你知道你使用的不是普通的实例变量字段和函数,Kotlin禁止通过引用变量访问伴生对象。

需要指出的是,严格地说,伴生对象及其成员并不是静态成员。在内部,它们依然是普通的实例成员,但由于在程序开始使用前就被自动实例化,因此它们很像静态成员。在伴生对象中,可生成真正的静态方法,为此可使用注解@JvmStatic

  1. class StaticDemo {
  2. companion object {
  3. @JvmStatic fun realStaticMethod() {
  4. println("Real static method...")
  5. }
  6. }
  7. }
  8. StaticDemo.realStaticMethod()

同样,要调用这个方法,只需通过伴生对象所属类的名称即可。当你这样做时,Kotlin将确保调用的是伴生对象的静态方法realStaticMethod()

要将对象或伴生对象中的字段编译成真正的JVM静态字段,可在它前面添加关键字const

  1. class StaticFieldsDemo {
  2. companion object {
  3. const val CONSTANT_VALUE = 3
  4. }
  5. }

这与Java代码public static final int CONSTANT_VALUE = 3;等效。

9.4 Kotlin的OOP功能 - 图4 你可能会问,考虑到伴生对象的行为很像静态成员,为何要创建真正的静态方法或静态字段呢?原因之一是对被其他JVM语言(如Java)调用的类来说,这可能很有用。

一个典型的真正的静态方法是应用程序的JVM入口方法main()。根据设计,这个方法必须是静态的。在Kotlin中,要在类中定义入口,唯一的办法是创建一个伴生对象并使用注解@JvmStatic

  1. class MainDemo {
  2. companion object {
  3. @JvmStatic fun main(args: Array<String>) {
  4. println("This is the main method")
  5. }
  6. }
  7. }

9.4.9 数据类

要创建只包含几个字段的类,数据类将很有用。第3章说过,在Java中创建POJO类时,必须编写大量的代码来定义字段并为每个字段定义两个方法(获取函数和设置函数)。Kotlin提供了数据类,它自动为每个字段生成属性。来看一个示例:

  1. data class Computer(val brand: String, val cpu: String, var memoryGB:
  2. Int, var harddiskSizeGB: Int)

这行代码生成的类类似于下面这样:

9.4 Kotlin的OOP功能 - 图5

在代码中,访问数据类的方式与访问Kotlin普通类相同;另外,其主构造函数将所有字段作为参数:

  1. var pc = Computer("Dell", "Intel Core i5", 8, 1024)
  2. println(pc.brand)
  3. pc.memoryGB = 4

对于数据类,编译器自动为每个字段创建一个获取函数,对于使用关键字var而不是val定义的每个可变字段,还自动为其创建一个设置函数。编译器还自动给数据类添加常用JVM方法(如equals()hashCode()toString())的实现。

在Kotlin中,访问属性时,无需添加前缀get/set,前面的代码片段演示了这一点。

编译器还自动为数据类生成函数copy(),这提供了极大的便利,让你能够创建数据类实例的副本,并指定要在副本中修改哪些字段:

  1. var pc2 = pc.copy(brand="HP", memoryGB=16)
  2. println(pc2)

这将创建变量pc指向的数据类Computer的实例的副本,但将字段brand(品牌)改成了HP,将字段memory(内存量)提高到了16GB。

9.4 Kotlin的OOP功能 - 图6 数据类可实现接口,在Kotlin 1.1版中还可扩展类。

9.4.10 lambda和内联函数

与本书介绍的其他大多数语言一样,在Kotlin中,也可将lambda函数作为参数进行传递。假设有一个应用程序有足够的权限,能够重启服务器。那么调用重启服务器的函数时,你可能想将这种信息写入到某个地方。这个shutdown函数的函数头如下:

  1. fun shutdown(logger: (m: String) -> Unit) {
  2. logger("The server is about to shutdown. There's no way back.")
  3. println("Code to shutdown the application here...")
  4. }

它接受一个参数——logger,这个参数是一个函数,将一个字符串(m)作为输入值(注意,并没有使用这个字符串参数)且什么都不返回(返回类型为Unit)。在你关闭服务器之前,函数shutdown()调用传入的函数logger,但对函数logger的实现细节一无所知,而只知道它将一个字符串(其中包含要写入到日志中的消息)作为参数,且不返回任何值。

调用函数shutdown()时,调用者可传入一个lambda——直接传入函数logger的函数体:

  1. shutdown({ msg: String -> println("Logged message: '$msg'") })

这里的lambda只是将msg打印到控制台。

将一个函数传递给另一个函数的开销非常高。函数在内部被定义为对象;函数shutdown()调用函数logger()时,将在幕后做大量的工作,这将消耗一定的处理时间。由于lambda函数能够访问其所属类的成员,因此将把成员变量的副本传递给lambda函数。就当今而言,这通常不是大问题,但在速度至关重要时,Kotlin提供了一种巧妙的解决方案:内联函数。对于将lambda作为输入的函数(如前述示例中的shutdown函数),通过在它前面加上inline,可让Kotlin编译器重写代码,将lambda的实现复制到函数中,从而避免函数再调用lambda。

考虑到这可能听起来令人迷惑,我们来看一个示例——在函数shutdown()前面加上inline

  1. inline fun shutdown(logger: (m: String) -> Unit) {
  2. logger("The server is about to shutdown. There's no way back.")
  3. println("Code to shutdown the application here...")
  4. }

对于这个调用lambda函数的内联函数,编译器会重写调用它的代码。例如,请看下述调用函数shutdown的代码:

  1. fun closeConnectionsAndShutdown() {
  2. println("Code that shutdowns active connections omitted...")
  3. shutdown({ msg: String -> println("Logged message: '$msg'") })
  4. }

编译器将把它重写为类似于下面这样:

  1. fun closeConnectionsAndShutdown() {
  2. println("Code that shutdowns active connections omitted...")
  3. val msg = "The server is about to shutdown. There's no way back."
  4. println("Logged message: '$msg'")
  5. println("Code to shutdown the application here...")
  6. }

相比于原来的shutdown,这个版本的执行速度更快。