9.4 Kotlin的OOP功能
Kotlin首先是一种OOP语言,本节将全面介绍Kotlin的OOP基本知识:
- 定义包;
- 导入成员;
- 定义类和构造函数;
- 给类添加成员;
- 继承;
- 可见性限定符;
- 单例对象和伴生对象;
- 数据类;
- lambda和内联函数。
9.4.1 定义包
包是使用package语句定义的,这种语句的工作原理与Java中很像:
package com.example
不同于Java和Clojure,在Kotlin中,源代码的目录结构无需与包名匹配;换言之,你可以随心所欲地组织源代码。
请不要在Kotlin交互式REPL中使用
package语句,因为它不支持创建包。
9.4.2 导入成员
Kotlin import语句与Java import语句很像:
import java.util.ArrayListimport java.io.*
一个重要的不同是可指定别名,这在可能发生名称冲突的情况下提供了极大的便利:
import java.io.File as JavaFileval f = JavaFile("test.txt")
9.4.3 定义类和构造函数
类是使用关键字class定义的:
class ClassName {}
可在类头中指定主构造函数:
class Point constructor(x: Int, y: Int) {}
关键字constructor可以省略:
class Point (x: Int, y: Int) {}
如果要在实例化类时执行一些代码,可使用关键字init指定一个代码块:
class Point (x: Int, y: Int) {init {println("Executable code here...")}}
在init代码块中,可使用构造函数的参数以及当前类中定义的属性(至于如何添加属性,稍后将介绍)。对于要在方法中使用的构造函数参数,必须给它们加上前缀val(用于不可变属性)或var(用于可变属性):
class Point (val x: Int, val y: Int) {override fun toString(): String { return "${x}, ${y}" }}val p = Point(-30, 50)println(p)
这将打印-30, 50。通常,最好使用val将构造函数参数设置为不可变的,因此除非出于设计考虑,否则在使用var将构造函数设置为可变的之前一定要三思而后行。
指定了关键字constructor时,还可给主构造函数指定访问限定符,但在省略了关键字constructor的情况下,无法这样做(在这种情况下,构造函数默认为公有的):
class Customer private constructor(id: Int) { }
在没有显式地定义构造函数的情况下,将自动生成一个默认构造函数,这个构造函数是公有的且不接受任何参数。如果你不希望自动创建这样的构造函数,可显式地定义一个不接受任何参数的私有构造函数,如下所示:
class Customer private constructor()
可使用的访问限定符将在后面介绍。还可添加一个或多个辅助构造函数:
class Customer(val name: String, val country: String?) {constructor(name: String) : this(name, null) {println("Name: " + name)println("Country: " + country)}}var c = Customer("Your Name")
上述代码片段将打印Name: Your name和Country: null。辅助构造函数必须直接(如上例所示)或间接地调用主构造函数。所谓间接地调用主构造函数,指的是调用另一个辅助构造函数,而这个辅助构造函数直接或间接地调用了主构造函数。
9.4.4 给类添加成员
下面来介绍如何给类添加成员:
- 添加函数;
- 添加入口函数
main; 添加属性。
添加函数
在Java中,类中的函数被称为方法,但在Kotlin中,它们也被称为函数。由于函数在前面介绍过,因此添加函数的方式完全在意料之中:
class MethodDemo {fun instanceMethod(i: Int): Int {return i*i}var demo = MethodDemo()println(demo.instanceMethod(5))
有趣的是,Kotlin没有定义静态方法(类方法)的关键字。作为一种替代办法,你可将函数放在类外面(这个主题将在9.5节讨论)。你也可以使用伴生对象来生成静态函数,这也将在本章后面讨论。
- 入口函数
main
你知道,在Java中,可添加static main(String[] args),它将作为应用程序的入口。在Kotlin中,类中的方法自动为实例方法,因此在类中添加如下函数不管用:
// 下面的main()函数是普通的实例方法// 不能作为应用程序的入口class A {fun main(args : Array<String>) {println("Executable code here...")}}
在Kotlin中,定义入口main()的方法有两种。
- 将函数<code>main()</code>放在源代码的最顶层(不在任何类中),这将在9.5节讨论。- 将函数<code>main()</code>放在一个伴生对象中,并添加<code>@JvmStatic</code>注解,这将在9.4.8节演示。
- 添加属性
在Kotlin中,类不能包含独立的变量,但可包含属性。属性是有配套获取函数和/或设置函数的变量(具体情况取决于属性是否是可读和/或可写的)Kotlin可为属性生成默认的获取/设置函数,但你也可提供获取/设置函数的实现。
下面是一个可变(可读写)的属性;这个属性之所以是可变的,是因为声明它时使用了关键字var:
class PropertyDemo1 {var mutableProperty: Int = 0}val p1 = PropertyDemo1()println(p1.mutableProperty)p1.mutableProperty = 24println(p1.mutableProperty)
属性mutableProperty被初始化为0。由于没有显式地提供获取函数和设置函数的实现,Kotlin编译器将自动为这个属性生成获取函数和设置函数。要访问这个属性,只需使用属性名,而无需添加前缀get或set。Kotlin自动调用生成的获取函数和设置函数,这些函数在Kotlin中被称为存储函数(accessor)。
Kotlin编译器要求要么在声明属性的同时对其进行初始化(如前面的示例所示),要么在
init { }块中对其进行初始化。
下面是一个只读的属性;只读属性是使用关键字val声明的:
class PropertyDemo2 {val readOnlyProperty: Int = 1000}val p2 = PropertyDemo2()println(p2.readOnlyProperty)
如果你试图执行类似于前一个示例的代码p2.readOnlyProperty = 1234,将引发类似于下面的异常:java.lang.IllegalAccessError: tried to access field Line35 $PropertyDemo2.readOnlyProperty from class Line39。像可变属性一样,对于只读属性,也必须在声明时显式地初始化或在init { }块中初始化。
前面说过,可显式地给属性定义存取函数(获取函数和设置函数),下面就是一个这样的可变属性:
class PropertyDemo3 {var customProperty: Int = 1000get() { field + 1 }set(value) { field = value }p3.customProperty = 10println(p3.customProperty)
关键字field用于访问生成的字段,Kotlin文档称之为支持字段(backing field)。
可将设置函数设置为私有的以隐藏它:
class PropertyDemo4 {var anotherProperty: Int = 314private set}var p4 = PropertyDemo4()println(p4.anotherProperty)
属性anotherProperty 确实有设置函数(由Kotlin提供的默认实现),但由于它是私有的,因此被隐藏了。但与往常一样,获取函数被自动创建且是公有的。然而,你不能将获取函数设置为私有的;获取函数必须与属性的访问限定符匹配。在Kotlin中,访问限定符被称为可见性限定符,将在后面更详细地讨论。
9.4.5 继承
与大多数流行的JVM语言一样,Kotlin也只允许类最多扩展一个超类。对于没有显式地继承其他类的类,将继承Kotlin类kotlin.Any。kotlin.Any类似于Java类java.lang.Object,但需要注意的是,它们是两个不同的类。
创建时没有显式指定限定符的类都是final的,不能继承。要让类能够被继承,必须在定义它时在前面加上关键字open:
open class Person(name: String)class Customer(name: String, department: String) : Person(name) {}
如果子类没有主构造函数,要调用父类的构造函数,必须使用关键字super:
open class Person(name: String)class Customer : Person {constructor(name: String) : super(name)}
要继承方法,必须使用关键字override:
open class ParentClass {open fun greatMethod() {println("greatMethod in parent class")}}class ChildClass: ParentClass() {override fun greatMethod() {super.greatMethod()}}
请注意,除非显式地使用了访问限定符open,否则函数默认为final的。
9.4.6 接口
Kotlin的接口类似于Java 8中的接口,可包含抽象函数(没有实现的函数)和具体函数(带默认实现的函数):
interface NameOfInterface {fun functionWithoutImplementation()fun functionWithImplementation(i: Int) {// 下面是默认实现……}}
在Kotlin接口中,还可声明属性(这些属性可以有显式的获取函数,也可以没有):
interface InterfaceWithProperties {var propertyWithGetterAndSetter: Intval propertyWithGetterOnly: Stringval propertyWithDefaultImplementation: Doubleget() = 0.0}
接口不可能包含支持字段,因此在接口中不能使用关键字field。有鉴于此,对于接口中的属性,无法给它显式地提供设置函数实现。
Kotlin使用相同的语法来实现接口和继承类:
class DemoClass : NameOfInterface, InterfaceWithProperties {override fun functionWithoutImplementation() {println("but now it has a implementation")}override var propertyWithGetterAndSetter: Int = 0override val propertyWithGetterOnly: String = "test"}
类和接口的排列顺序无关紧要,但如果有超类,最好将它放在最前面,然后再指定类实现的接口。
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很像,也用于创建单例:
object ThisIsASingleton {fun coolMethod() = println("Not so cool, after all")}ThisIsASingleton.coolMethod()
将自动创建这个类的一个实例,并可通过对象名来访问其成员。
可在类中创建单例对象,为此需要添加前缀companion:
class NormalClass {companion object CompanionObject {var i = 100fun yetAnotherCoolMethod() {i = 50}}}NormalClass.CompanionObject.yetAnotherCoolMethod()println(NormalClass.i)println(NormalClass.CompanionObject.i)
正如这里演示的,要访问伴生对象的成员,可通过引用NormalClass.Companion Object,也可只使用类名NormalClass(因为它也指向伴生对象CompanionObject)。上述代码片段将打印50两次。
如果没有显式地指定伴生对象的名称,访问其成员时可指定伴生对象。在这种情况下,Kotlin将伴生对象称为Companion。
要访问伴生对象,只能通过其所属的类(这里为NormalClass),而不能通过引用变量来访问。因此,下面的代码无法通过编译:
var i = Normalclass()// 无法通过编译i.CompanionObject.yetAnotherCoolMethod()
这些代码将引发如下异常:error: nested companion object 'CompanionObject' accessed via instance reference。
之所以会出现这种错误,是因为伴生对象很像Java中的静态成员。由于伴生对象是单例对象,因此它只有一个实例,由其所属类的所有实例共享。为让你知道你使用的不是普通的实例变量字段和函数,Kotlin禁止通过引用变量访问伴生对象。
需要指出的是,严格地说,伴生对象及其成员并不是静态成员。在内部,它们依然是普通的实例成员,但由于在程序开始使用前就被自动实例化,因此它们很像静态成员。在伴生对象中,可生成真正的静态方法,为此可使用注解@JvmStatic:
class StaticDemo {companion object {@JvmStatic fun realStaticMethod() {println("Real static method...")}}}StaticDemo.realStaticMethod()
同样,要调用这个方法,只需通过伴生对象所属类的名称即可。当你这样做时,Kotlin将确保调用的是伴生对象的静态方法realStaticMethod()。
要将对象或伴生对象中的字段编译成真正的JVM静态字段,可在它前面添加关键字const:
class StaticFieldsDemo {companion object {const val CONSTANT_VALUE = 3}}
这与Java代码public static final int CONSTANT_VALUE = 3;等效。
你可能会问,考虑到伴生对象的行为很像静态成员,为何要创建真正的静态方法或静态字段呢?原因之一是对被其他JVM语言(如Java)调用的类来说,这可能很有用。
一个典型的真正的静态方法是应用程序的JVM入口方法main()。根据设计,这个方法必须是静态的。在Kotlin中,要在类中定义入口,唯一的办法是创建一个伴生对象并使用注解@JvmStatic:
class MainDemo {companion object {@JvmStatic fun main(args: Array<String>) {println("This is the main method")}}}
9.4.9 数据类
要创建只包含几个字段的类,数据类将很有用。第3章说过,在Java中创建POJO类时,必须编写大量的代码来定义字段并为每个字段定义两个方法(获取函数和设置函数)。Kotlin提供了数据类,它自动为每个字段生成属性。来看一个示例:
data class Computer(val brand: String, val cpu: String, var memoryGB:Int, var harddiskSizeGB: Int)
这行代码生成的类类似于下面这样:

在代码中,访问数据类的方式与访问Kotlin普通类相同;另外,其主构造函数将所有字段作为参数:
var pc = Computer("Dell", "Intel Core i5", 8, 1024)println(pc.brand)pc.memoryGB = 4
对于数据类,编译器自动为每个字段创建一个获取函数,对于使用关键字var而不是val定义的每个可变字段,还自动为其创建一个设置函数。编译器还自动给数据类添加常用JVM方法(如equals()、hashCode()和toString())的实现。
在Kotlin中,访问属性时,无需添加前缀get/set,前面的代码片段演示了这一点。
编译器还自动为数据类生成函数copy(),这提供了极大的便利,让你能够创建数据类实例的副本,并指定要在副本中修改哪些字段:
var pc2 = pc.copy(brand="HP", memoryGB=16)println(pc2)
这将创建变量pc指向的数据类Computer的实例的副本,但将字段brand(品牌)改成了HP,将字段memory(内存量)提高到了16GB。
数据类可实现接口,在Kotlin 1.1版中还可扩展类。
9.4.10 lambda和内联函数
与本书介绍的其他大多数语言一样,在Kotlin中,也可将lambda函数作为参数进行传递。假设有一个应用程序有足够的权限,能够重启服务器。那么调用重启服务器的函数时,你可能想将这种信息写入到某个地方。这个shutdown函数的函数头如下:
fun shutdown(logger: (m: String) -> Unit) {logger("The server is about to shutdown. There's no way back.")println("Code to shutdown the application here...")}
它接受一个参数——logger,这个参数是一个函数,将一个字符串(m)作为输入值(注意,并没有使用这个字符串参数)且什么都不返回(返回类型为Unit)。在你关闭服务器之前,函数shutdown()调用传入的函数logger,但对函数logger的实现细节一无所知,而只知道它将一个字符串(其中包含要写入到日志中的消息)作为参数,且不返回任何值。
调用函数shutdown()时,调用者可传入一个lambda——直接传入函数logger的函数体:
shutdown({ msg: String -> println("Logged message: '$msg'") })
这里的lambda只是将msg打印到控制台。
将一个函数传递给另一个函数的开销非常高。函数在内部被定义为对象;函数shutdown()调用函数logger()时,将在幕后做大量的工作,这将消耗一定的处理时间。由于lambda函数能够访问其所属类的成员,因此将把成员变量的副本传递给lambda函数。就当今而言,这通常不是大问题,但在速度至关重要时,Kotlin提供了一种巧妙的解决方案:内联函数。对于将lambda作为输入的函数(如前述示例中的shutdown函数),通过在它前面加上inline,可让Kotlin编译器重写代码,将lambda的实现复制到函数中,从而避免函数再调用lambda。
考虑到这可能听起来令人迷惑,我们来看一个示例——在函数shutdown()前面加上inline:
inline fun shutdown(logger: (m: String) -> Unit) {logger("The server is about to shutdown. There's no way back.")println("Code to shutdown the application here...")}
对于这个调用lambda函数的内联函数,编译器会重写调用它的代码。例如,请看下述调用函数shutdown的代码:
fun closeConnectionsAndShutdown() {println("Code that shutdowns active connections omitted...")shutdown({ msg: String -> println("Logged message: '$msg'") })}
编译器将把它重写为类似于下面这样:
fun closeConnectionsAndShutdown() {println("Code that shutdowns active connections omitted...")val msg = "The server is about to shutdown. There's no way back."println("Logged message: '$msg'")println("Code to shutdown the application here...")}
相比于原来的shutdown,这个版本的执行速度更快。
