Kotlin的语法糖到底有多甜?
JYM大家好,好久没来写文了。
今天带给大家 Kotlin 的内容,可能一些常关注我的朋友也发现了,在我之前的文章中就开始用 Kotlin 代码做代码示例了,这是因为最近一年我都在高强度使用 Kotlin 进行后端开发。
相信很多安卓开发的朋友早就开始用上 Kotlin 了,但是许多后端对这门语言应该还是不太了解,反正在我的朋友圈里没有见到过用 Kotlin 写后端的程序员存在,在我亲身用了一年 Kotlin 之后已经不太想用 Java 进行代码开发了,起码在开发效率方面就已经是天差地别了,所以今天特地给大家分享一下 Kotlin 的好,希望能带领更多人入坑。
1. 第一段代码
很多人说 Kotlin 就是披了一层语法糖的 Java,因为它百分百兼容 Java,甚至可以做到 Kotlin 调用 Java 代码。
其实我对这个说法是赞同的,但是又不完全一样,因为 Kotlin 有自己的语法、更有自己的编译器、还有着多端支持,更有着自己的设计目标。
我更倾向于把 Kotlin 看成一个 JVM 系语言,就像 Scala 语言一样,只是恰好 Kotlin 有一个设计目标就是百分百兼容 Java。
在语言层面,Kotlin 几乎是借鉴了市面上所有的现代化语言的强大特性,协程、函数式、扩展函数、空安全,这些广受好评的特性全部都有。
而且从我个人感受来看,我用过 Java、Kotlin、JS、Go、Dart、TS、还有一点点 Python,我觉得 JS 和 Kotlin 的语法是比较方便易用的,Dart、Java 和 Go 的语法都不是太方便易用,语法的简单也在一定程度上减少了开发者的心智负担。
下面我将用一段代码,来简单说明一下 Kotlin 常见的语法特性:
fun main(args: Array<String>) {
val name = "Rookie"
// Hello World! Rookie
println("Hello World! $name")
// Hello World! 82,111,111,107,105,101
println("Hello World! ${name.chars().toList().joinToString(",")}")
test(createPerson = { Person("Rookie", 25) })
}
data class Person(
var name: String = "",
var age: Int = 0,
)
fun test(createPerson: () -> Person, test : String = "test"): Person {
val person = createPerson()
// Person(name=Rookie, age=25)
println(person)
return person
}
上面是一段简简单单的 Kotlin 代码,但是却可以看出 Kotlin 的很多特性,请听我娓娓道来~
- Kotlin 的启动方法也是 main 函数,但是 Kotlin 移除了所有基础类型,一切皆对象,比如 Java 中的数组对应的就是 Array 类,int 对应的是 Int 类。
- Kotlin 使用类型推断来声明类型,一共有两个关键字,val 代表这是一个不可变变量,var 代表这是一个可变的变量,这两个关键字选用我感觉比 JS 还要好。
- Kotlin 代码每一行不需要英文分号结尾。
- Kotlin 支持字符串模板,可以直接字符串中使用 '$' 符号放置变量,如果你想放置一个函数的计算结果,需要用 '${}' 来包裹。
- Kotlin 是一个函数式语言,支持高阶函数,闭包、尾递归优化等函数式特性。
- Kotlin 为了简化 Java Bean,支持了数据类 data class,它会自动生成无参构造、getter、setter、equals()、hashCode()、copy()、toJSON()、toString() 方法。
- Kotlin 的函数关键字是 fun,返回值在函数的最后面,变量名在类型的前面,几乎新兴语言都是这样设计的,可以明显感受到语言设计者想让我们更多关注业务含义而非数据类型。
- Kotlin 具有一些类似 go 和 Python 的内置函数,比如 println。
- Kotlin 的函数参数支持默认值。
- Kotlin 不支持多参数返回,但是为了解决这个问题它内置了两个类:Pair 和 Triple,分别可以包装两个返回值和三个返回值。
2. 基础常用特性
了解了一些 Kotlin 的基础语法之后,我再来介绍一些常用的基础特性。
第一个就是空安全和可空性。
Kotlin 中的变量可以声明为非空和可空,默认的声明都是非空,如果需要一个变量可空的,需要在类型后面加一个问号,就像这样:
fun start() {
val name1 : String = ""
val name : String? = null
}
函数的参数声明也一样,也会区分非空和可空,Kotlin 编译器会对代码上下文进行检查,在函数调用处也会对变量是否可空进行一致性检查,如果不通过则会有编译器提醒,我是强烈建议不用可空变量,一般都可以通过默认值来处理。
那么如果你接手的是前人代码,他声明变量为可空,但是希望为空的时候传递一个默认值,则可以使用这个语法进行处理:
fun start() {
val name : String? = null
println(name ?: "Rookie")
}
这是一个类似三元表达式的语法(Elvis 运算符),在 Kotlin 中极其常见,除此之外你还可以进行非空调用:
fun start() {
val name : String? = null
println(name?.chars() ?: "Rookie")
}
这段代码就表示:如果变量不为空就调用 chars 方法,如果为空则返回默认值 Rookie,在所有可空变量上都支持这种写法,并且支持链式调用。
第二个常用特性是异常处理, 写到这里突然想到了一个标题,Kotlin 的异常处理,那叫一个优雅!!!
fun start() {
val person = runCatching {
test1()
}.onFailure {
}.onSuccess {
}.getOrNull() ?: Person("Rookie", 25)
}
fun test1() : Person {
return Person()
}
这一段代码中的 test1 方法你可以当作一个远程调用方法或者逻辑方法,对了,这里隐含了一个语法,就是一个函数中的最后一行的计算结果是它的返回值,你不需要显示的去写 return。
我使用 runCatching 包裹我们的逻辑方法,然后有三个链式调用:
- onFailure:当逻辑方法报错时会进入这个方法。
- onSuccess:当逻辑方法执行成功时会进入这个方法。
- getOrNull:当逻辑方法执行成功时正常返回,执行失败时返回一个空变量,然后我们紧跟一个 ?: ,这代表当返回值为空时我们返回自定义的默认值。
如此一来,一个异常处理的闭环就完成了,每一个环节都会被考虑到,这些链式调用的方法都是可选的,如果你不手动调用处理会有默认的处理方式,大家伙觉得优雅吗?
第三个特性是改进后的流程控制。
fun start() {
val num = (1..100).random()
val name = if (num == 1) "1" else { "Rookie" }
val age = when (num) {
1 -> 10
2 -> 20
else -> { (21..30).random() }
}
}
我们先声明一个随机数,然后根据条件判断语句返回不同的值,其中 Java 中的 Switch 由 When 语法来替代。
而且这里每一段表达式都可以是一个函数,大家可以回忆一下,如果你使用 Java 来完成通过条件返回不同变量的逻辑会有多麻烦。
如果大家在不了解 Kotlin 的情况下尝试用更简单的方式来写逻辑,可以问问类似 ChatGPT 这种对话机器人来进行辅助你。
3. 常用内置函数
就像每个变量类型都有 toString 方法一样,Kotlin 中的每个变量都具有一些内置的扩展函数,这些函数可以极大的方便我们开发。
apply和also
fun start() {
val person = Person("Rookie", 25)
val person1 = person.apply {
println("name : $name, age : $age, This : $this")
}
val person2 = person.also {
println("name : ${it.name}, age : ${it.age}, This : $it")
}
}
这两个函数调用之后都是执行函数体后返回调用变量本身,不同的是 apply 的引用为 this,内部取 this 变量时不需要 this.name 可以直接拿 name 和 age 变量。
而 also 函数则默认有一个 it,it 就是这个变量本身的引用,我们可以通过这个 it 来获取相关的变量和方法。
run 和 let
fun start() {
val person = Person("Rookie", 25)
val person1 = person.run {
println("name : $name, age : $age, This : $this")
"person1"
}
val person2 = person.let {
println("name : ${it.name}, age : ${it.age}, This : $it")
"person2"
}
}
run 函数和 let 函数都支持返回与调用变量不同的返回值,只需要将返回值写到函数最后一行或者使用 return 语句进行返回即可,上例中 person 变量进行调用之后的返回结果就是一个 String 类型。
在使用上的具体差异也就是引用对象的指向不同,具体更多差异可以看一下网络上总结,我这里表明用法就可以了。
除了这四个函数之外,还有许多的类似函数帮我们来做一些很优雅的代码处理和链式调用,但是我根本没有用过其它的函数,这四个函数对我来说已经足够了,有兴趣的朋友可以慢慢发掘。
4. 扩展函数与扩展属性
上文了我们举了几个常见的内置函数,其实他们都是使用 Kotlin 的扩展函数特性实现的。
所谓扩展函数就是可以为某个类增加扩展方法,比如给 JDK 中的 String 类增加一个 isRookie 方法来判断某个字符串是否是 Rookie:
fun start() {
val name = "rookie"
println(name.isRookie())
}
fun String.isRookie(): Boolean {
return this == "Rookie"
}
this 代表了当前调用者的引用,利用扩展函数你可以很方便的封装一些常用方法,比如 Long 型转时间类型,时间类型转 Long 型,不比像以前一样再用工具类做调用了。
除了扩展函数,Kotlin 还支持扩展属性:
fun start() {
val list = listOf(1, 2, 3)
println(list.maxIndex)
}
val <T> List<T>.maxIndex: Int
get() = if (this.isEmpty()) -1 else this.size - 1
通过定义一个扩展属性和定义它的 get 逻辑,我们就可以为 List 带来一个全新属性——maxIndex,这个属性用来返回当前 List 的最大元素下标。
扩展函数和扩展属性多用于封闭类,比如 JDK、第三方 jar 包作为扩展使用,它的实际使用效果其实和工具类是一样的,只不过更加优雅。
不过借用这个能力,Kotlin 为所有的常用类都增加了一堆扩展,比如 String:
基本上你可以想到的大部分函数都已经被 Kotlin 内置了,这就是 Kotlin 的语法糖。
5. Kotlin的容器
终于来到我们这篇文章的大头了,Kotlin 中的容器基本上都是:List、Map 扩展而来,作为一个函数式语言,Kotlin 将容器分为了可变与不可变。
我们先来看一下普遍的用法:
fun start() {
val list = listOf(1, 2, 3)
val set = setOf(1, 2, 3)
val map = mapOf(1 to "one", 2 to "two", 3 to "three")
}
上面的例子中,我们使用三个内置函数来方便的创建对应的容器,但是此时创建的容器是不可变的,也就是说容器内的元素只能读取,不能添加、删除和修改。
当然,Kotlin 也为此类容器增加了一些方法,使其可以方便的增加元素,但实际行为并不是真的往容器内增加元素,而是创建一个新的容器将原来的数据复制过去:
fun start() {
val list = listOf(1, 2, 3).plus(4)
val set = setOf(1, 2, 3).plus(4)
val map = mapOf(1 to "one", 2 to "two", 3 to "three").plus(4 to "four")
}
如果我们想要创建一个可以增加、删除元素的容器,也就是可变容器,可以用以下函数:
fun start() {
val list = mutableListOf(1, 2, 3)
val set = mutableSetOf(1, 2, 3)
val map = mutableMapOf(1 to "one", 2 to "two", 3 to "three")
}
讲完了,容器的创建,可以来聊聊相关的一些操作了,在 Java 中有一个 Stream 流,在 Stream 中可以很方便的做一些常见的函数操作,Kotlin 不仅完全继承了过来,还加入了大量方法,大概可以包含以下几类:
- 排序:sort
- 乱序:shuffle
- 分组:group、associate、partition、chunked
- 查找:filter、find
- 映射:map、flatMap
- 规约:reduce、min、max
由于函数实在太多,我不能一一列举,只能给大家举一个小例子:filter:
一个 filter 有这么多种多样的函数,几乎可以容纳你所有的场景,这里说两个让我感觉到惊喜的函数:chunked 和 partition。
chunked 函数是一个分组函数,我常用的场景是避免请求量过大,比如在批量提交时,我可以将一个 list 中的元素进行 1000 个一组,每次提交一组:
fun start() {
val list = mutableListOf(1, 2, 3)
val chunk : List<List<Int>> = list.chunked(2)
}
示例代码中为了让大家看的清楚我故意声明了类型,实际开发中可以不声明,会进行自动推断。
在上面这个例子中,我将一个 list 进行每组两个进行分组,最终得到一个 List<List> 类型的变量,接下来我可以使用 forEach 进行批量提交,它底层通过 windowed 函数进行调用,这个函数也可以直接调用,有兴趣的朋友可以研究一下效果,通过名字大概可以知道是类似滑动窗口。
partition 你可以将其看作一个分组函数,它算是 filter 的补充:
fun start() {
val list = mutableListOf(1, 2, 3)
val partition = list.partition { it > 2 }
println(partition.first)
println(partition.second)
}
它通过传入一个布尔表达式,将一个 List 分为两组,返回值是上文提到过的 Pair 类型,Pair 有两个变量:first 和 second。
partition 函数会将符合条件的元素放到 first 中去,不符合条件的元素放到 second 中,我自己的使用的时候很多是为了日志记录,要把不处理的元素也记录下来。
容器与容器之间还可以直接通过类似:toList、toSet之类的方法进行转换,非常方便,转换 Map 我一般使用 associate 方法,它也有一系列方法,主要作用就是可以转换过程中自己指定 Map 中的 K 和 V。
6. 结束语
不知不觉都已经快四千字了,我已经要结束这篇文章了,但是仍然发现几乎什么都没写,也对,这只是一篇给大家普及 Kotlin 所带来效率的提升的文章,而不是专精的技术文章。
正如我在标题中写的那样:Kotlin 的语法糖到底有多甜?Kotlin 的这一切我都将其当作语法糖,它能极大提高我的开发效率,但是一些真正 Kotlin 可以做到而 Java 没有做到的功能我却没有使用,比如:协程。
由于我一直是使用 Kotlin 写后端,而协程的使用场景我从来没有遇到过,可能做安卓的朋友更容易遇到,所以我没有对它进行举例,对于我来说,Kotlin 能为我的开发大大提效就已经很不错了。
使用 Kotlin 有一种使用 JS 的感觉,有时候可以一个方法开头就写一个 return,然后链式调用一直到方法结束。
我还是蛮希望 Java 开发者们可以转到 Kotlin,感受一下 Kotlin 的魅力,毕竟是百分百兼容。
在这里要说一下我使用的版本,我使用的是 JDK17、Kotlin 1.8、Kotlin 编译版本为 1.8,也就是说 Kotlin 生成的代码可以跑在最低 JDK1.8 版本上面,这也是一个 Kotlin 的好处,你可以通过升级 Kotlin 的版本体验最新的 Kotlin 特性,但是呢,你的 JDK 平台不用变。
对了,Kotlin 将反射封装的极好,喜欢研究的朋友也可以研究一下。
好了,这篇文章就到这里,希望大家能帮我积极点赞,提高更新动力,人生苦短,我用KT。
链接:https://juejin.cn/post/7258970835044827192
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。