¶主要内容
- 函数处理集合、字符串、正则表达式
- 使用命名参数,默认参数值,中缀调用
- 通过扩展函数和属性适配Java库
- 顶层代码结构,本地函数和属性
¶Creating collections in Kotlin
创建一个新的Set,使用setOf
函数。
1 | val set = hashSetOf(1, 7, 53) |
对应地,创建数组list,
1 | val list = arrayListOf(1, 7, 53) |
注意to
不是一个具体的结构体,而是一个函数。
Kotlin的集合和Java对应,
1 | >>> println(set.javaClass) // javaClass 是Kotlin版的`getClass()` |
为什么Kotlin不提供自己的集合库实现?还是那句话,方便和Java做集成。
Kotlin 的集合虽然和Java是一回事,但可以做更多事情。
1 | >>> val strings = listOf("first", "second", "fourtheenth") |
后面的内容会介绍lambda表达式,会更多用到集合的使用。
¶Making functions easier to call
Java集合有默认的toString()
实现,但输出格式不尽人意:
1 | >>> val list = listOf(1, 2, 3) |
假设你想要覆盖它的方法实现,希望输出 (1; 2; 3)
。Java的解决办法是使用第三发库Guava或Apache Commons或自己实现。在Kotlin中提供了这方面的函数处理。
这里我们希望自己实现这个函数。我们实现joinToString
函数,
1 | fun <T> joinToString( |
该函数是通用的:对任何元素类型的集合都生效。泛型的语法和Java类似。
下面verify一下函数是否如是:
1 | >>> val list = listOf(1, 2, 3) |
函数实现友好。下面关注点是,怎么声明函数函数已使得调用的时候减少冗余?每次调用函数的时候减少非必要参数的传递。
¶Named arguments
第一个问题我们将会定位关注的是函数的可读性。例如下面对joinToString
的调用:
1 | joinToString(collection, " ", " ", ".") |
如果不看方法签名的话,可能不知道这些点号逗号都是啥。虽然IDE工具可以帮到你,但调用的时候并不明显。
这里问题对于参数类型为Boolean尤其明显。Java应对的做法是推荐创建枚举带代替Boolean。甚至有些会让你加注释。
1 | joinToString(collection, /* separator */ " ", /* preifx */ " ", /* postfix */ "."); |
使用Kotlin,可以做得更好:
1 | joinToString(collection, separator = " ", prefix = " ", postfix = ".") |
Kotlin函数调用时,可以指定参数名来避免误解。
¶Default parameter values
另外一个Java常见的问题是过于丰富的重载方法。看看java.lang.Thread
类以及它的构造函数。基于兼容性和用户友好的API考量提供重构,但最终的结果是一致的:重复。参数名和返回类型被重复了多次。另外,如果调用一个重载方法省略某些参数,又会显得不明显,不知调用的是哪些参数。
在Kotlin,你可以避免重复创建重载方法。因为你可以在函数声明时指定默认值。下面我们重构joinToString
函数。默认值参数通常被用作大部分case适用性。
1 | fun <T> joinToString( |
现在你既可以调用函数带所有参数,亦或者省略默认值部分:
1 | >>> joinToString(list, ", ", "", "") |
如果使用常规调用语法,你必须按照函数声明顺序指定参数,仅可以省略尾部参数(trailing arguments)。如果你使用命名参数,你可以省略参数列表的中间部分参数,并指定一个你需要用到的值,并且顺序不作要求:
1 | >>> joinToString(list, suffix = ";", prefix = "# ") |
注意,参数的默认值在函数调用时被编码了,不是在调用方编码。如果重新编译class类时包含该函数,并改变参数的默认值,调用者再次调用该函数不指定默认值时会使用新的值。
Default values and Java
Java 没有默认参数值的概念,通过Java调用Kotlin的带默认参数时,需要显式指定所有参数值。如果你需要频繁地从Java调用Kotlin的函数,你可以使用@JvmOverloads
注解。它会在编译期生成对应的Java的overloaded方法,从最后一个参数开始逐个省略参数并赋予默认值。例如,如果你使用
@JvmOverloads
注解joinToString
,会生成下面的重载方法:
1
2
3
4
5 /* Java */
String joinToString(Collection<T> collection, String separator, String prefix, String postfix);
String joinToString(Collection<T> collection, String separator, String prefix);
String joinToString(Collection<T> collection, String separator);
String joinToString(Collection<T> collection);每重载一个方法就从方法签名省略最后一个默认值。
目前为止,你看到的这类工具类(utility)函数并没有包含在某些类内,这样正确吗?实际上,Kotlin认为工具类是非必要的。
¶Getting rid of static utility classes: top-level functions and properties
我们都知道Java是一门面向对象语言,要求所有方法代码写在类内。通常,这中实现是好的。但现实是,几乎每个大型项目带有大量的代码,这些代码实际不属于任何某个类。有时某个对象对协作的两个或多个类起到等价的重要性;有时会有某个主要的对象,并不希望实例化时传递它的API,但不开发它的API又无法在子类继承。
最终的结果可能是,你会将那些不包含状态或实例的方法包装成静态方法。最明显的例子就是JDK中的Collections
。另外一个明显的例子就是,你的项目代码中会有大量的*Util
命名的代码。
在Kotlin,你不需要创建所有这些无意义的类。相反,你可以直接将函数放置在源文件的最顶层,其它类的任何地方。这些函数仍然是package的成员属性,所以你可以通过import方式访问。
譬如我们将joinToString
函数直接放置在strings
包层级。创建一个文件join.kt内容如下:
1 | package strings |
怎么跑起来?你知道,当编译文件时,某些类会被产生,因为JVM仅在类内执行代码。当使用Kotlin工作时,需要知道。通过Java调用kotlin函数时,会编译成对应如下的Java类:
1 | /* Java */ |
Kotlin 编译生成的类名对应于文件名。文件内容的顶层函数则编译到了该类里面的静态方法。因此,如果通过Java来调用Kotlin会非常简单:
1 | /* Java */ |
Changing the file class name
默认地,Kotlin的文件顶层函数包装的类名对应于文件名;如果想要修改类型。可以添加@JvmName
注解到文件最开始位置。要在包定义前:
1
2
3 "StringFunctions") // 指定类名 (
package strings // 包语句要跟在文件注解之后
fun joinToString(...): String { ... }现在函数的Java调用变为了:
1
2
3 /* Java */
import strings.StringFunctions;
StringFunctions.joinToString(list, ", ", "", "");注解部分内容会在后面章节介绍。
TOP-LEVEL PROPERTIES
和函数一样,属性也可以放置在文件的顶层。将独立的数据存储在类外部并不常见但很有效。
例如,使用var
属性统计操作执行的次数。
1 | var opCount = 0 // 声明一个顶层属性 |
这类属性值会被存储在一个静态字段。
Top-Level的properties允许你定义常量:
1 | val UNIX_LINE_SEPARATOR = "\n" |
默认地,Top-Level的properties会相应保留其accessor方法给Java方法调用。但如果你想作为Java的常量public static final
字段,可以使用const
修饰符:
1 | const val UNIX_LINE_SEPARATOR = "\n" |
它实际上等价于Java代码:
1 | /* Java */ |
¶Adding methods to other people’s classes: extension functions and properties
Kotlin的一个主题曲是平滑地集成已有的代码。甚至纯粹的Kotlin项目构建于JDK库、Android框架、第三方框架。当你要将Kotlin集成到一个Java项目,你通常需要处理存在的代码,这部分代码不会被或不可能被转换为Kotlin。使用原有的优良的API而不必要转换它这不是好事吗?这就是扩展函数允许你做的事情。
概念上,extension function
是个简单的东西:它是一个可以作为类成员调用的函数进行调用,但却定义在该类的外部。为了证实它,下面添加了一个方法用于计算最后一个字符串的字节数:
1 | package strings |
你需要做的是,将需要进行扩展的类名或接口名放在定义函数名前。类名称为 receiver type ;扩展函数的值部分称为 receiver object. 如下图:
你可以以普通类成员的语法形式使用该函数:
1 | >>> println("Kotlin".lastChar()) |
在该例子中,String
是接收者类型(receiver type),"Kotlin"
是接收者对象(receiver object).
在某种意义上,你往String
类添加了自定义方法。尽管String
不是你代码的一部分,甚至你可能没有该类的源码,你仍然可以对其进行方法扩展。并不需要关系String
到底是Java写的、Kotlin写的还是其他JVM语言写的,譬如Groovy。只要它能被编译成Java类,你就可以对其进行方法扩展。
在扩展函数的body部分,你可以使用this
来表示对应的类。实际上你可以省略:
1 | package strings |
在extension function,你可以直接访问其自身内部的方法和属性,因为该方法定义的就是它自身。但值得注意的是,extension函数不允许你打破其封装。不像在类内定义方法,extension函数不允许访问private或protected的成员。
后面我们将使用到method
术语来描述extension函数。例如,我们可以说extension函数的body部分调用了receiver的任意method,意味着你可以调用成员方法,也可以调用extension函数本身。而对于调用方,extension 函数区分于成员属性,因为它是被定义在类外部的,也就是说不关心这个特殊的方法是作为一个member还是作为一个extension。
¶Imports and extension functions
当你定义一个扩展函数,并不会自动地在你整个项目中可用。相反,它需要被导入,就和其它类或函数一样。这样有助于避免名字冲突。Kotlin允许你导入独自的函数:
1 | import strings.lastChar |
当然,*
号的导入也是OK的:
1 | import strings.* |
你可以使用as
关键字来更改导入的类或函数名
1 | import strings.lastChar as last |
import是更改原有的名字是个常态化行为。因为你可能会用到不同的包名和类名或方法名相同的情况。
¶Calling extension functions from Java
在底层,extension函数实际上是一个静态方法,第一个参数为receiver object实例传入。调用该函数实际上并没有涉及到适配对象的创建或任何其他运行时的额外开销。
这使得从Java调用扩展函数变得相当容易:直接调用该静态函数,然后传递receiver对象实例即可。就像其他top-level函数一样,包含该静态方法的Java类名,取决于该函数声明的文件名的声明。假设改方法声明在一个StringUtil.kt的文件:
1 | char c = StringUtilKt.lastChar("Java"); |
扩展函数被声明为一个top-level函数,因此它被编译为一个静态方法。你可以在Java中静态地引入lastChar
方法,最简单的写法是lastChar("Java")
。这种代码写法在Kotlin版本易读性差一点,在Java语言层面反而很顺畅。
¶Utility functions as extensions
现在我们来编写最终版的joinToString
函数。这其实会在Kotlin标准库中有很多类似的地方:
1 | fun <T> Collection<T>.joinToString( |
这里为所有参数都提供了默认值。所以现在你可以像调用member成员一样调用该方法:
1 | >>> val list = arrayListOf(1, 2, 3) |
因为扩展函数实际上是静态方法的高效语法糖,你可以使用更多的指定类型的receiver type,不仅仅是一个类。譬如:
1 | fun Collection<String>.join( |
这类函数如果使用其它类型不会生效:
1 | >>> listOf(1, 2, 8).join() |
这也意味着扩展函数不能在子类中进行重载(override)。
¶No overriding for extension functions
Kotlin中成员函数(member function)的重载是可行的,但对于extension function的重载是禁止的。假设我们有两个类,View
和它的子类Button
,Button
类可以重载父类的click
函数。
1 | open class View { |
如果你声明了一个类型为View
的变量,你可以使用该变量存储类型为Button
的值,因为Button
是View
的子类。如果你调用一个常规的方法,例如这里的click
,由于Button
类进行了重载,所以实际上它是调用了Button
的click
的实现部分代码:
1 | >>> val view: View -= Button() |
但是这种方式对于extension function不起作用,如图figure 3.2 。
extension function并不是类的一部分;他们在类外部被声明。尽管你可以定义跟基础类内方法名同名的扩展函数,但函数的调用取决于变量的静态声明类型,而不是运行时存储的变量类型。
下面例子展示了同名的showOff
扩展函数的声明。
1 | fun View.showOff() = println("I'm a view!") |
扩展函数是被静态处理的。如果你在Java中调用这函数时就一点不会感到惊讶:
1 | /* Java */ |
Kotlin中对extension function不支持重载,它被静态处理。
NOTE
如果一个类内部包含同名的成员函数和扩展函数。成员函数(member function)总是优先处理。
¶Extension properties
扩展属性和扩展函数具有相似的语法和行为。
1 | val String.lastChar: Char |
扩展属性带有receiver type,其它地方和常规的properties差不多。并且getter必须总是被定义,因为它没有被定义在类,没有后背的字段值也没有默认的getter实现。初始化器(initializers)不允许这种情况发生,因此getter必须定义。
如果你要在StringBuilder
上定义相同的属性,你可以使用var
,因为StringBuilder
的内容是可以被修改的:
1 | var StringBuilder.lastChar: Char |
以及,你可以像成员属性一样方法扩展属性:
1 | >>> println("Kotlin".lastChar) |
需要在Java中访问扩展属性是,你应该显式地调用它的getter:StringUtilKt.getLastChar("Java")
¶Working with collections: varargs, infix calls, and library support
该小节会展示一些Kotlin方式的集合库的调用,包括有:
vararg
关键字,允许你声明一个字面量类型函数infix
符号调用某些一个参数的函数Destructuring declarations
结构一个组合值到多个变量
¶Extending the Java Collections API
从本章开始会留意到Kotlin的集合类和Java的集合类是相同的,但有extend API。譬如之前的例子:
1 | >>> val strings: List<String> = listOf("first", "second", "fourteenth") |
其中last
和max
函数为extension function。
last
和max
扩展函数的声明如下:
1 | fun <T> List<T>.last(): T { /* return sthe last element */ } |
大多数的扩展函数会被声明在Kotlin的标准库中,这里不一一列表。你可能想方设法去学习Kotlin标准库。实际上没必要——任何时候在任何对象需要用到集合相关的内容时,IDE编辑器会提示相应对象的可能选项。IDE的代码提示会包含常规方法(regular method)和扩展函数(extension function);你可以选择你需要的函数。另外,标准库也引用列表所有出该类内所有可用的方法列表和扩展列表(包括properties、extensions).
在章节开始的部分看到如何创建集合。这些集合的调用参数为任意数字。
¶Varargs: functions that accept an arbitrary number of arguments
当你调用函数来创建一个list时,你可以传递任何数字型的参数:
1 | val list = listOf(2, 3, 5, 7, 11) |
该函数在标准库的定义如下:
1 | fun listOf<T>(vararg values: T): List<T> { ... } |
你可能会熟悉Java的vararg:允许传递数字型字面值到一个方法并包装到一个数组中。Kotlin的vararg和Java如出一辙,但语法上有明显的区别:Java中采取形如main(String args...)
的形式,Kotlin中使用vararg
修改器标记。
另一个不同的地方是,Kotlin的语法表述上要求传入的是一个包装好的数组。Java的三点形式的参数不要求传递单个或多个。技术上,这种特性被称为 扩展符(spread operator),对于要传入的数组使用*
符号进行扩展:
1 | fun main(args: Array<String>) { |
¶Working with pairs: infix calls and destructuring declarations
要创建map,使用mapOf
函数:
1 | val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three") |
单词to
在代码行中并不是内置结构体,而是一个特殊的方法调用,称为 中缀调用(infix call)
。
中缀调用就是方法名处于两个参数对象之间的意思,并且不带任何分隔符。实际上下面这两种调用是等价的:
1 | 1.to("one") // 常规模式 |
两种调用方式的参数相同,要允许一个函数使用中缀记法,需要在函数签名前面使用infix
修饰符。下面是to
函数的简单写法:
1 | infix fun Any.to(other: Any) = Pair(this, other) |
to
函数返回一个实例Pair
,它是Kotlin标准库类,表示一对元素。实际上Pair
的to
声明使用了泛型,不过这里省略掉以方便描述。
注意Pair
可以直接用于初始化变量:
1 | val (number, name) = 1 to "one" |
该特性称为 声明解构(destructuring declaration)
。
NOTE 声明解构不是模式解构,仅限于变量声明;做不到类似rust的match匹配。when
更不是模式匹配,而是Java的一种instanceOf
做得不伦不类。
声明解构不局限于pair。也可作用于loop循环,
1 | for ((index, element) in collection.withIndex()) { |
这里的to
函数是一个扩展函数。你可以创建任意类型的pair:1 to "one"
、"one" to 1
、list to list.size()
等等。让我们看看mapOf
函数的声明:
1 | fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V> |
¶Working with strings and regular expressions
Kotlin的字符串和Java 的字符串是一回事。因此,Kotlin标准库中定义的有关字符串的函数或方法可以直接在Java代码中使用。Kotlin标准库中对于字符串没有作任何额外的封装,其中包含了大量的扩展函数(extension functions)。
¶Splitting strings
split
字符串在Java中也有。Kotlin中对split
作了增强。譬如支持正则表达式:
1 | >>> println("12.345-6.A".split("\\.|-".toRegex())) |
又或者写成任意个数分隔字符的形式,对应于前面说到的vararg
。
1 | >>> println(12.345-6.A".split(".", "-")) |
¶Regular expressions and triple-quoted strings
(略,没有亮点)
¶Multiline triple-quoted strings
多行三引号字符串。该特性允许将超大字符串以更美观的方式进行多行表述。实际上还是一个字符串。类似scala语言的trim。
1 | val kotlinLogo = """| // |
其中,上面的点号可以不写,换成trimMargin("")
。三引号字符串也可以压缩成一行的写法"""C:\Users\<yourname>\kotlin-book"""
。
¶Making your code tidy: local functions and extensions
遵循于DRY原则(Don’t Repeat Yourself)。Kotlin允许使用本地函数的方式来简化重复代码或者样板代码。譬如下面例子:
1 | class User(val id: Int, val name: String, val address: String) |
上面的两处if
语句包含的重复的部分。如果将校验部分写成本地函数,会让代码简洁很多。
1 | class User(val id: Int, val name: String, val address: String) |
另外本地函数由于在语句块内,可以直接访问生成的参数,user可以省略。
1 | class User(val id: Int, val name: String, val address: String) |
为了让代码逻辑更清晰,你可以将validation 逻辑更改为User
类的扩展函数,因为它仅属于该类的部分。
1 | class User(val id: Int, val name: String, val address: String) |
其中,扩展函数也可以本地函数,即本地函数里面再嵌套本地函数,不过这种写法可读性并不好。
¶Summary
- Kotlin并没有定义自己的集合类,而是在Java集合的基础上作扩展。
- 定义函数参数的默认值可以减少函数的重载,函数命名参数(named-argument)的调用方法可读性更加良好。
- 函数(functions)和属性(properties)可以直接作为kt文件的top-level。不仅可以作为一个类的成员属性,允许更灵活的代码结构。
- 扩展函数(extension functions)和扩展属性(extension properties)相当于对已有类的扩展实现,包括对已有标准库类的函数和属性的扩展,它的原理是编程成对应的static方法,所以它不是基于运行时的,不会有额外的运行时开销。
- 中缀调用(infix call)提供了一种清晰的语法体验。
- Kotlin提供了大量的有关字符串的处理方法和语法。
- 三重引号(triple-quoted)字符串提供了一种清晰的字符串语法表述。
- 本地函数帮助你优化代码减少重复代码和样板代码的实现。