本章内容包括
- 用于处理集合,字符串和正则表达式的函数
- 使用命名参数,默认参数,以及中缀调用语法
- 通过扩展函数和属性来适配Java库
- 使用顶层函数,布局函数和属性架构代码
在 Kotlin 中创建集合
Kotlin 没有自己的集合类库而是完全使用标准的 Java 集合类库。
val hashSet = hashSetOf(1, 2, 3, 4, 5)
println(hashSet.javaClass) // class java.util.HashSet
val linkedHashSet = linkedSetOf(1, 2, 3)
println(linkedHashSet.javaClass) // class java.util.LinkedHashSet
val arrayList = arrayListOf(1, 2, 3, 4, 5)
println(arrayList.javaClass) // class java.util.ArrayList
val list = listOf(1, 2, 3, 4, 5)
println(list.javaClass) // class java.util.Arrays$ArrayList
val hashMap = hashMapOf(1 to "a", 2 to "b", 3 to "c")
println(hashMap.javaClass) // class java.util.HashMap
通过上面这些函数就可以创建集合,通过自己 new 集合对象的方式也是可以的 , Kotlin 中在创建对象时省略了 new 关键字。虽然 Kotlin 采用的是 Java 集合类库,但是 Kotlin 提供了一些额外的扩展。
val list = listOf("小明", "丹尼", "李华")
println("获取第一个元素 : ${list.first()}") // 获取第一个元素 : 小明
println("获取最后一个元素: ${list.last()}") // 获取最后一个元素: 李华
println("获取指定下标的元素: ${list[1]}") // 获取指定下标的元素: 丹尼
println("获取当中最大的一个元素: ${list.max()}") // 获取当中最大的一个元素: 李华
println("翻转这个集合 :${list.asReversed()}") // 翻转这个集合 :[李华, 丹尼, 小明]
println("根据条件在集合中查找满足条件的元素 : ${list.find { it.startsWith("小") }}")
// 根据条件在集合中查找满足条件的元素 : 小明
在后面的部分会仔细探究他们的工作原来,以及这些在 Java 类中新增加的函数是从何而来。
让函数更好调用
这一节我们从一个例子开始,需求是得到一个集合的字符串展示形式,可以指定元素之间的分隔符号,前缀和后缀。先写一个最基本的函数。
fun <T> joinToString(collection: Collection<T> , separator: String ,
prefix: String , postfix: String): String {
val result = StringBuilder(prefix)
for ((index , element) in collection.withIndex()) {
if (index > 0) {
result.append(separator)
}
result.append(element)
}
result.append(postfix)
return result.toString()
}
val list = listOf("小明", "丹尼", "李华")
println(joinToString(list , "|" , "<" , ">"))
// <小明|丹尼|李华>
对 joinToString 函数的测试结果得到了我们的预期。接下来我们会用 Kotlin 支持的特性来改写这个函数,力求让它变得更简洁和实用。
命名参数
命名参数 是 Kotlin 的特性之一 ,可以解决可读性的问题 , 因为当你在调用这样一个 API : joinToString(Collection , "" , "" , "") 的时候。你很可能会搞不清楚每个位置的String类型的参数究竟意味着什么,只要参数的顺序传错了你就会得到一些奇怪的结果。为了避免这个问题你需要去看一下它的函数声明,来确定每个位置上的需要的是什么参数。
在 Kotlin 中可以通过命名参数来解决这个问题, 就是在调用一个函数传入参数的时候,可以显示的写上参数的名称,并且指定要传入的值赋值给那个参数。但是如果在调用一个函数时,指明了一个参数的名称时,为了避免混淆,这个参数之后的所有参数都需要标明名称了。 例如我对 prefix 参数标明了名称,那么必须在对之后的 postfix 和 separator 参数都标明名称。
这个特性是没法在调用 Java 函数时使用的。因为把参数名称保存到.class 文件中是 Java 8及其更高版本的一个可选功能,Kotlin 需要保持对 Java 6 的兼容性。所以编译器不能识别出调用函数的参数名称。
val list = listOf("小明", "丹尼", "李华")
println(joinToString(list , prefix = "<" , separator = "|" , postfix = ">"))
// <小明|丹尼|李华>
默认参数值
Java 的另一个普遍存在的问题是一些类的重载函数太多。这些重载,原本是为了向后兼容,方便这些API的使用者,又或者是出于别的原因,但导致的最终结果是一样的:重复。
在 Kotlin 中可以在声明函数的时候指定参数的默认值,这样可以避免创建重载函数。使用默认参数值对 joinToString 函数进行改写。
fun <T> joinToString(collection: Collection<T> , separator: String = ", " ,
prefix: String = "[" , postfix: String = "]"): String {
val result = StringBuilder(prefix)
for ((index , element) in collection.withIndex()) {
if (index > 0) {
result.append(separator)
}
result.append(element)
}
result.append(postfix)
return result.toString()
}
val list = listOf("小明", "丹尼", "李华")
println(joinToString(list)) // [小明, 丹尼, 李华]
在对 joinToString 函数进行调用的时候我们只传入了一个 list 参数值。其他参数都使用了我们在声明函数时所指定的默认值。 注意!参数的默认值是被编码到被调用的函数中,而不是调用的地方。如果你改变了参数的默认值并重新编译这个函数,没有给参数重新赋值的调用者,将会开始使用新的默认值
Java中是没有默认值概念的,所以当从 Java 代码中调用 Kotlin 函数的时候,调用者必须显示的指定所有参数的值。同时 Kotlin 也给出了符合 Java 习惯的解决方法 ,在函数上加上 @JvmOverloads 注解,编译器就会生成 Java 的重载函数,从最后一个开始省略每个参数,被省略的参数使用的是函数声明时指定的默认值。
@JvmOverloads
fun <T> joinToString(collection: Collection<T> , separator: String = ", " ,
prefix: String = "[" , postfix: String = "]"): String {
val result = StringBuilder(prefix)
for ((index , element) in collection.withIndex()) {
if (index > 0) {
result.append(separator)
}
result.append(element)
}
result.append(postfix)
return result.toString()
}
List<String> list = new ArrayList<>();
list.add("小明");
list.add("丹尼");
list.add("李华");
System.out.println(new KTDemo().joinToString(list)); // [小明, 丹尼, 李华]
消除静态工具类:顶层函数和属性
我相信绝大多数 Java 开发者都会在自己的,公司的,开源框架项目,或者是 JDK 中看到不少名称为 XXXUtils 或者 XXXs 的类。这些类存在的意义就是工作在一些不需要对象的地方。这样的类仅仅作为一堆静态函数的容器存在。看吧事实就是这样,并不是所有人都需要对象(object) (注意这里的对象指的是编程世界中的对象,而不是中文口语的那个对象,事实上在现实世界中人人都需要对象,不然人该有多孤单啊) 。
在 Kotlin 中根本酒不需要去创建这些无意义的类。相反,可以把这些函数直接放在代码文件的顶层 ,不用从属于任何类。这些放在文件顶层的函数任然是包内的成员,如果你需要从包外访问它,则需要 import 。
这里我们写了一个 join.kt 文件,直接将 joinToString 函数放在了文件内。在 Java 代码中调用这个函数 。
仔细观察可以发现 import static kt.demo.JoinKt.joinToString 这行代码,这说明了 join.kt 文件被编译成了一个类名为 JoinKt , joinToString 是其中的一个静态函数。当然这里你也可以这样写。
import kt.demo.JoinKt
public class JavaClassDemo {
@Test
public void test1() {
List<String> list = new ArrayList<>();
list.add("小明");
list.add("丹尼");
list.add("李华");
System.out.println(JoinKt.joinToString(list));
}
}
修改文件类名
想要改变包含 Kotlin 顶层函数的编译生成的类名称,需要给这个 Kotlin 文件添加 @JvmName 的注解,将其放到这个文件的开头,为于包名的前面:
使用时就可以使用 JoinFunctions 这个名称。
顶层属性
和函数一样属性也可以被放到文件顶层。放在顶层的属性会被编译成一个静态字段。默认情况下顶层属性和其他任意属性是一样的,是通过访问器暴漏给使用者。为了方便使用,如果你想要把一个常量以 public static final 的属性暴漏给 Java 可以使用 const 来修饰它。
const val UNIX_LINE_SEPARATOR = "\n"
public static final String UNIX_LINE_SEPARATOR = "\n";
// 这两行代码等同
给别人添加方法:扩展函数和属性
理论上来说扩展函数非常简单,就是一个类的成员函数,不过这个成员函数定义在了类的外面。如下图我们就为 String 定义了一个扩展函数用来获取字符串的最后一个字符。
fun String.lastChar(): Char = this.last()
- 扩展函数中接收者类型是由扩展函数定义的,所谓的接收者就是要被扩展的那个类,在这个例子中是 String
- 接收者对象是该类型的一个实例,在这个例子中接收者对象是一个 String 类型的实例,也就是这个例子中的 this
可以像调用类的普通成员去调用这个函数:
println("Kotlin".lastChar()) // n
在上面这个例子中 ,String 就是接收者类型 。 "Kotlin" 字符串就是接收者对象。现在我们不需要修改 String 类的源码就为它增加了新的行为。不管 String 类是用 Java 、Kotlin,或者像 Groovy 的其他 JVM 语言编写的,只要他会编译为 Java 类,就可以为这个类添加自己的扩展。
- 在扩展函数中可以直接访问接收者类的其他方法和属性
- 扩展函数不允许打破接收者的封装性,在扩展类中不能访问接收者的私有或者受保护的成员
导入和扩展函数
一个扩展函数不会自动在整个项目范围内生效。如果你需要使用它需要进行导入。如果导入后发现了命名冲突可以使用 as 关键字来另外定义一个名称,这样对导入的类或者函数都是有效的。
import javax.persistence.Entity
import org.hepeng.cornerstone.entity.Entity as E
从 Java 中调用扩展函数
- 实际上扩展函数是一个静态函数,它把接收者对象做为第一个参数传递给函数。扩展函数本质上是静态函数的一个高效语法糖。
- 因为扩展函数的本质是静态函数所以也不存在重写的问题
- 如果一个类的成员函数和扩展函数有相同的签名,成员函数会被优先使用
因为是静态函数,这样调用扩展函数就不会创建适配的对象或者任何运行时的额外开销。知道了这一点如何从 Java 中调用扩展对象就很简单了,无非就是调用这个静态函数罢了。
import kt.demo.StringsKt;
public class JavaClassDemo {
@Test
public void test2() {
String s = "kotlin";
System.out.println(StringsKt.lastChar(s)); // n
}
}
作为扩展函数的工具函数
在学习了以上这些知识后我们可以进一步改写 joinToString 函数了 :
@JvmOverloads
fun <T> Collection<T>.joinToString(collection: Collection<T> , separator: String = ", " ,
prefix: String = "[" , postfix: String = "]"): String {
val result = StringBuilder(prefix)
for ((index , element) in collection.withIndex()) {
if (index > 0) {
result.append(separator)
}
result.append(element)
}
result.append(postfix)
return result.toString()
}
在 Kotlin 中调用扩展函数 :
val list = listOf("小明", "丹尼", "李华")
println(list.joinToString(separator = " @ ")) // [小明 @ 丹尼 @ 李华]
扩展属性
扩展属性提供了一种方法,用来扩展类的 API ,可以用来访问属性,用的是属性语法而不是函数语法。尽管他们被称为属性,但是他们可以没有任何状态,因为没有合适的地方来存储它,不可能给现有的 Java 对象实例添加额外的字段。但有时短语法仍然是便于使用的。
- 声明一个扩展属性,这里必须显示的定义 getter 函数,因为没有对应的字段所以也不会存在默认的 getter 实现。同理初始化也是不可以的,因为没有地方存储值。
- 在 Java 中调用扩展属性的时候,是显示的调用它的 getter 函数。
val String.lastChar: Char
get() = this.last()
处理集合:可变参数,中缀调用和库支持
这节内容会涉及到的语言特性:
- 可变参数的关键字 vararg ,可以用来声明一个函数将可能有任意数量的参数
- 一个中缀表示法,当你在调用一些只有一个参数的函数时,使用它会让代码更简练
- 解构声明,用来把一个单独的组合值展开到多个变量中
Kotlin 扩展 Java 集合的 API
- Kotlin 对 Java 集合类库的扩展是通过扩展函数来实现的。
可变参数:让函数支持任意数量的参数
使用函数来创建集合的时候可以传入任意个数的参数。
val list = listOf(1 , 2 , 3 , 4 , 5)
在 Java 中的可变参数是通过 ... 声明的, 可以把任意个数的参数值打包到数组中传给函数。 Kotlin 的可变参数使用 vararg 声明。Kotlin 和 Java 之间另一给区别是,当需要传递的参数已经包装在数组中时,调用该函数的语法。在 Java 中可以按原样传递数组 ,而 Kotlin 则要求你显示的解包数组,以便每个数组元素在函数中能作为单独的参数来调用。从技术角度来讲这个功能被称为展开运算符,而使用的时候,不过是在参数前面放一个 * 。
fun main(args: Array<String>) {
val list = listOf("args: " , *args)
println(list)
}
键值对的处理:中缀调用和解构声明
在之前的内容中我写过一些这样的代码来创建一个 map 集合。在这行代码中 to 不是内置的结构,而是一种特殊的函数调用,被称为中缀调用。
在中缀调用中没有添加额外的分隔符,函数名称是直接放在目标对象名称和参数之间的。 第二行代码和第一行代码调用方式是等价的。
val hashMap = hashMapOf(1 to "a", 2 to "b", 3 to "c")
val hashMap = hashMapOf(1.to("a"), 2.to("b"), 3.to("c"))
- 中缀调用可以与只有一个参数的函数一起使用,无论是普通的函数还是扩展函数。
- 要允许使用中缀符号调用函数 ,需要使用 infix 修饰符来标记它
infix fun String.join(s: String) = this.plus(" $s")
println("hello" join "world") // hello world
解构声明
解构声明可以把一个对象解构成很多变量,这样会带来一些便利性。
val map = mapOf(1 to "One", 2 to "Two", 3 to "three")
for ((key , value) in map) {
println("key = $key , value = $value")
}
例如这里 (key , value) in map 就是一个解构声明
data class Cat(var name: String? , var color: String?)
val cat = Cat(name = "小将" , color = "白色")
val (name , color) = cat
这里对 cat 也是一个解构声明
字符串和正则表达式的处理
Kotlin 字符串和 Java 字符串完全相同。Kotlin 提供了一些有用的扩展函数,使得字符串使用起来更加方便。
Kotlin 中使用与 Java 完全相同的正则表达式语法。
三重引号字符串
val text = """
>Tell me and I forget.
>Teach me and I remember.
>Involve me and I learn.
>(Benjamin Franklin)
"""
三重引号字符串中的内容不会被转义,它可以包含任何字符,将会保持原样。上面的字符串打印后会按照原样输出。
如果为了更好的表示这样的字符串,可以去掉缩进(左边距)。为此可以向字符串内容添加前缀,标记边距的结尾,然后调用 trimMargin 来删除每行中的前缀和前面的空格。
val text = """
>Tell me and I forget.
>Teach me and I remember.
>Involve me and I learn.
>(Benjamin Franklin)
""".trimMargin(">")
三重引号字符串中也是可以使用字符串模板的
让你的代码更整洁:局部函数和扩展
许多开发人员认为,好代码的重要标准之一是减少重复代码,甚至还给这个原则起了个名字:不要重复你自己(DRY)。但是当你写 Java 代码的时候,有时候做到这点就不那么容易了。许多情况下可以抽取出多个方法,把长的函数分解成许多小的函数然后重用他们。但是这样可能会让代码更费解,因为你以一个包含许多小方法的类告终,而且他们之间没有明确的关系。可以更进一步将提取的函数组合成一个内部类,这样就可以保持结构,但是这种函数需要用到大量的样板代码。
Kotlin 提供了一个更整洁的方案: 可以在函数中嵌套这些提取的函数。这样既可以获得所需要得结构,也无需额外得语法开销。
data class User(var id:Int , var name: String , var address: String)
fun saveUser(user: User) {
if (user.name.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty Name")
}
if (user.address.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty Address")
}
// 保存到数据库
}
提取局部函数来避免重复
data class User(var id:Int , var name: String , var address: String)
fun saveUser(user: User) {
fun validate(value: String , fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty $fieldName")
}
}
validate(user.name , "Name")
validate(user.address , "Address")
// 保存到数据库
}
- 局部函数可以访问所在函数中的所有参数和变量
提取逻辑到扩展函数中
data class User(var id:Int , var name: String , var address: String)
fun saveUser(user: User) {
user.validateBeforeSave()
// 保存到数据库
}
fun User.validateBeforeSave() {
fun validate(value: String , fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user $id: empty $fieldName")
}
}
validate(name , "Name")
validate(address , "Address")
}
小结
- Kotlin 没有自己的集合类,而是在 Java集合类的基础上提供了更丰富的 API
- Kotlin 可以给函数参数定义默认值,这样大大降低了重载函数的必要性,而且命名参数让多参数函数的调用更加易读
- Kotlin 允许更灵活的代码结构:函数和属性都可以直接在文件中声明,而不仅仅是在类中作为成员
- Kotlin 可以调用扩展函数和属性来扩展任何类的 API,包括在外部库中定义的类,而不修改其源代码,也没有运行时开销
- 中缀调用提供了处理单个参数的,类似调用运算符方法的简明语法
- Kotlin 为普通字符串和正则表达式都提供了大量的方便字符串处理的函数
- 三重引号的字符串提供了一种简洁的方式,解决了原本在 Java 中需要进行大量啰嗦的转义和字符串连接的问题
- 局部函数帮助你保持代码的整洁同时,避免重复