标签: Kotlin
本文包括:
1. 介绍用于集合、字符串和一般表达式的函数
2. 如何使用命名参数,默认参数值,中缀调用语法
3. 如何通过扩展函数和扩展属性将Java库适配到Kotlin中
4. 如何使用顶层和局部的函数和属性构造你的代码
本节介绍Kotlin是如何提升每个程序最重要的核心部分:声明和调用函数。也会介绍如何通过扩展函数将Java库适配成Kotlin风格的代码,以发挥Kotlin语言的强大特性。
本节会将Kotlin中的集合、字符串和一般表达式作为讲解知识点的基础。
先创建set、list或者map:
val set = setOf(1, 7, 53)
val list = listOf(1, 7, 53)
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
其中mapof的to
是一个一般函数(中缀调用),后面会介绍。
此外,set.javaClass
等价于Java中的set.getClass()
。注意Kotlin没有自己的集合,全部使用的Java库的集合。尽管如此,在Kotlin中却可以用这些Java集合拥有很多Java中原本没有的功能。
例如:
list.last()
set.max()
我们先打印出集合的内容:
val list = listOf(1, 7, 53)
println(list)
结果:
[1, 7, 53]
会调用Java集合默认的toString
方法,当然输出的格式并不一定是你所期望的。
我们实现一个函数joinToString
用于改变集合输出结果的前缀,后缀和分隔符:
fun joinToString(collection: Collection,
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()
}
结果:
println(joinToString(list, "/", "(", ")"))
(1/7/53)
Java中每次使用joinToString
函数都需要按顺序传入参数,但是Kotlin中可以通过参数名来制定参数(不需要按照参数顺序):
println(joinToString(list, prefix = "(", postfix = ")", separator = "/"))
一旦使用了命名参数,则需要写上所有参数的名称,不然编译器会混乱。
提示:
IntelliJ IDEA会在你更改参数名时自动更新调用函数时指定的函数名。建议你通过Rename操作来更改参数名,而不是仅仅手写。警告:
调用Java编写的方法时不能使用命名参数,包括JDK和Android框架中的所有方法。在Java 8中增加了在.class文件中保存参数名的可选特性,然而Kotlin是兼容Java 6的,因此不能在调用Java编写的方法时使用命名参数。
Kotlin可以通过默认参数避免创造重载的函数,减少过多的重复代码。
这里改造一下joinToString函数:
fun joinToString(collection: Collection,
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()
}
分隔符、前缀和后缀都是用了默认值。接下来调用该函数:
println(joinToString(list, ";")) //按顺序填写,并省略尾随的所有参数
println(joinToString(list, prefix = "(")) //命名参数:改变了前缀,其余采用默认值
结果:
1;7;53
(1,7,53
注意:
Java中没有默认参数的概念。因此在Java中调用Kotlin函数,需要显式制定所有参数的值。如果你想让Java调用者使用Kotlin函数更加方便,可以通过
@JvmOverloads
注解这些函数。内置的编译器会自动产生Java重载的所有构造器。
给joinToString加上注解@JvmOverloads:
@JvmOverloads fun <T> joinToString...
Kotlin中你不需要创建任何无意义的工具Util类,因为你可以直接将函数放置到源代码的顶层,而不需要在任何类中。这些函数仍然是声明在文件顶部的包的成员。当你需要调用这些函数时,仍然需要导入这些内容。
package strings
fun joinToString(...): String { ... }
必须在文件join.kt
中有包strings
,其中有函数joinToString
。当文件编译时,仍然会产生一些类,因为JVM仅仅可以运行在类中的代码。在Kotlin中使用这些函数是很方便的。然而需要在Java中使用这些函数时,该如何处理呢?
编译器会将join.kt
文件形成一个类JoinKt
,其中顶层的方法会被转换成该类的静态方法。这就是在Java中调用这些函数,编译器所做的事情:
/* Java */
package strings;
public class JoinKt {
public static String joinToString(...) { ... }
}
Java中调用:
JoinKt.joinToString(list, "/", "(", ")");
当我们需要改变Kotlin文件转为Java class时的类名:
//文件最开始通过注解指定生成的类名
@file:JvmName("StringFunctions")
package strings
fun joinToString(...): String { ... }
Java中调用:
/* Java */
import strings.StringFunctions;
StringFunctions.joinToString(list, ", ", "", "");
属性也能和函数一样放置在文件的顶层,虽然独立于类的单独数据不总是有需要的,但是仍然是有用的。
例如,你可以使用var
属性去计算一些操作执行的次数:
var opCount = 0
fun performOperation(){
opCount++
// ...
}
fun reportOperationCount() {
println("Operation performed $opCount times")
}
可以声明常量:
val UNIX_LINE_SEPARATOR = "\n"
默认的,顶层属性和其他属性一样,可以被Java代码通过访问器来访问get/set。如果想达到public static final
的效果,可以使用const
修饰符——用于原始类型的属性,例如String:
const val UNIX_LINE_SEPARATOR = "\n"
等效于Java代码:
public static final String UNIX_LINE_SEPARATOR = "\n"
Kotlin中用到了JDK等Java库,Android框架等第三方框架时,如何在不重写这些API的情况下,扩展这些API呢?扩展函数就满足了这些需求。
扩展函数
本质是一种能作为类的成员被调用,但却在该类外面定义的函数。
例如,我们给String扩展一个方法(lastChar)——能获取到String的最后一个字符:
fun String.lastChar(): Char = this.get(this.length - 1)
调用:
println("Hello".lastChar())
o
通过这种方法,就给String扩展了lastChar()函数,而不需要像Java一样继承String,并添加新方法。在该语句中String.lastChar()
的String
被称为“接收者类型”,后面的this.get以及this.length的this被称为“接收对象”(在扩展函数内部,也可以省略this)
优点:
这种方法,甚至不需要你有该类的源码。你也不需要关心该类是用Java编写的还是Kotlin编写的,或者是其他JVM语言如Groovy语言编写的。注意点:
扩展函数可以直接访问类的属性和方法,但是不能访问该类的private或protected修饰的成员。
在Kotlin中使用扩展函数,需要提前导入该函数,无论是import strings.lastChar
和import strings.*
都可以导入,这样是为了防止以外的命名冲突。
可以使用别名来解决冲突:
import strings.lastChar as last
val c = "Kotlin".last()
比如扩展函数lastChar写在文件StringUtil.kt
文件中,需要通过如下方法调用:
/* Java */
char c = StringUtilKt.lastChar("Java");
可以发现Java将StringUtil.kt转为了StringUtilKt类,而扩展方法lastChar成为该类的静态方法。
比如我们要给集合Collection添加扩展:
fun
joinToString作为工具扩展到了集合之上。
扩展函数的静态特性意味着扩展函数不能在子类中被覆盖。
Kotlin中的方法覆盖一般都是成员函数,但是你不能覆盖一个扩展函数。
例如,View有click方法,Button继承自View并覆盖了click方法,可以实现多态:
open class View{
open fun click() = println("View click")
}
fun View.showOff() = println("I'm a View")
class Button: View(){
override fun click() = println("Button click")
}
fun Button.showOff() = println("I'm a Button")
fun main(args: Array) {
//子类覆盖父类函数
var view: View = Button()
view.click()
//扩展函数
view.showOff()
}
结果:
Button click
I’m a View
由此可见,对于View变量,调用click方法,最终会去调用Button的方法。扩展函数即使名称,参数都完全一样(showOff())也不具备这种多态性,因为扩展函数具有静态特性。
注意:
如果有扩展函数和成员函数同名,会优先使用该扩展函数。这在扩展API时需要注意。
扩展属性提供一种扩展API类的方法,但是扩展属性和类的属性是不一样的,不能有任何状态和field域(因此一定要定义getter),之前扩展String的lastChar扩展函数,也可以用扩展属性来实现:
val String.lastChar: Char
get() = get(length - 1)
当你在Java中访问扩展属性时,需要通过get方法去获取StringUtilKt.getLastChar("Java")
.
本节讲解Kotlin标准库中用于集合的函数。通过这些内容,介绍语言相关的特性:
vararg
关键字允许你声明一个参数数量可变的函数destructuring declarations
,允许你拆解一个组合的值到多个变量中Kotlin中使用了Java库的集合类时,有很多Java中没有的函数,比如listOF,setOf
等等,这些都是作为扩展函数附加到Java库API上面的。
和Java中可变参数一样,Kotlin中也有可变参数:
val list = listOf(2, 3, 5, 9)
listOf函数原型如下:
fun listOf<T>(vararg values: T): List<T> { ... }
Kotlin与Java不同之处在于,Kotlin使用修饰符varags
修饰参数。
第二处不同在于,Java中你可以直接传入数组,Kotlin中需要显式解包数组,通过星号:
val array = arrayOf("Java", "C")
val list = listOf("Kotlin", *array)
list.forEach { println(it) }
}
如上,这样能组合一些值和数组(“Kotlin”和array数组中的内容组合起来),Java中是不允许的。
创建一个map:
val map = mapOf(10 to "ten", 3 to "three", 99 to "ninty-nine")
to
本质是中缀调用的方法,将函数名放置于目标对象和参数之间。10 to "ten"
类似于10.to("ten")
。
声明中缀函数,需要infix
修饰符:
infix fun Any.to(other: Any) = Pair(this, other)
Kotlin中可以将元素对分配到一组变量中:
val (number, name) = 1 to "one"
Kotlin中字符串和Java毫无区别,互相兼容,也不需要额外的包装。但是Kotlin提供了很多强大简洁的扩展函数。
字符串的split方法,是用于拆分字符串。但是Java中splite()
方法却无法生效在.
(点)上,比如"12.345-6.A".split(".")
,根据设想应该被拆分为12\365-6\A,但实际上却返回空数组,因为Java中.
是表示任何字符的正则表达式。
Kotlin通过扩展函数解决了这些复杂的问题,如果你想要拆分正则表达式,需要Regex
类型的参数,而不是String
类型。这样更容易区分正则表达式和一般文本的情况:
println("12.345-6.A".split("\\.|-".toRegex())) //正则表达式,用.和-拆分
[12, 345, 6, A]
非正则表达式:
println("12.345-6.A".split(".", "-"))
现在有个目标,是从文件路径path中解析出目录,文件名和文件后缀,可以如下实现:
fun parsePath(path: String) {
val directory = path.substringBeforeLast("/") //目录
val fullName = path.substringAfterLast("/") //获得完整文件名
val fileName = fullName.substringBeforeLast(".") //从完整文件名中解析
val extension = fullName.substringAfterLast(".")
println("Dir: $directory, name: $fileName, ext: $extension")
}
调用:
parsePath(“/Users/yole/kotlin-book/chapter.adoc”)
结果:Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc
该实例也可以通过正则表达式实现:
fun parsePathRegexp(path: String) {
val regex = """(.+)/(.+)\.(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if (matchResult != null) {
//析构声明
val (directory, filename, extension) = matchResult.destructured
println("Dir: $directory, name: $filename, ext: $extension")
}
}
三重引号字符串不仅可以避免转义字符,还可以包含任何字符,包括换行符。
val kotlinLogo = """| //
.|//
.|/ \"""
println(kotlinLogo.trimMargin(".")) //删除.点号以及前面的空白内容
三重引号中依旧可以使用字符串模板${...}
Java中总是会有一些重复代码,比如你想要验证用户信息合法,然后进行数据库操作,java思想代码如下:
fun saveUser(user: User) {
//第一次合法性判断
if (user.name.isEmpty()) {
throw IllegalArgumentException(
"Cannot save user ${user.id}: Name is empty")
}
//第二次合法性判断
if (user.address.isEmpty()){
throw IllegalArgumentException(
"Cannot save user ${user.id}: Address is empty")
}
//用户保存到数据库中等操作
}
可以通过局部函数优化:
class User(val id: Int, val name: String, val address: String)
fun saveUser(user: User) {
//使用局部函数
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException(
"Can't save user ${user.id}: " +
"$fieldName is empty")
}
}
validate(user.name, "Name")
validate(user.address, "Address")
//用户保存到数据库中等操作
}
通过扩展函数优化:
class User(val id: Int, val name: String, val address: String)
//User的扩展函数
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")
}
fun saveUser(user: User) {
user.validateBeforeSave() //对user合法性判断
//用户保存到数据库中等操作
}
通过扩展函数和局部函数,使得代码非常简洁,易读。比如上例中User扩展的方法,可能在用到User的其他地方根本不需要该方法,因此扩展函数的强大之处就显现出来。这里扩展函数还可以作为saveUser()
的局部函数,但是这样不易理解,我们也不建议使用超过一层的嵌套。