上篇已提 (tu) 到(cao)Java 中的各种坑. 习惯了 C# 的各种特性和语法糖后, 再转到 Java 感觉比较别扭. 最后本着反正 Java 也不是很熟悉, 干脆再折腾折腾其他语言的破罐子破摔的心态, 逛了一圈 JVM 语言, 最终决定转 Kotlin.
为何选择 Kotlin
项目遭遇人员变动, 包括我在内就剩两个人开发, 转型成本低, 代码质量容易控制.
JVM 语言. 号称与 Java 100% 兼容. 实际使用的确能够与 Java 几乎无缝地相互调用, 基本上可以无缝迁移, 完美兼容 Java 生态.
OOP. 目前 OOP 仍是主流, 方便后续交接或者其它新加入的开发成员上手.
静态类型. 在选择语言的时候也考虑过像 Groovy,JRuby 等的动态类型语言. 然而俗话说得好, 动态一时爽, 重构火葬场. 当项目变大的时候, 静态类型支持的较为完善的语义分析能够帮助项目快速整理, 重构代码. 并且引入很多函数式特性后, 静态类型语言的开发效率与爽感, 不比动态类型语言低多少.
吸收了一些函数式特性. 除了常见的 lambda,map,filter,reduce 之外, 还吸收了 Ruby 的一些如对象上下文切换, 代码块语法糖等便捷的特性(但是也可能导致代码可读性下降).
对 JetBrain 的信任. JetBrain 在静态分析的成果上有目共睹. 相信 JetBrain 设计的语言应该会比较有品位(然而严格得不近人情的 null safety 是有点让人纠结).
最后, 就是刚好看到 Kotlin, 确认了眼神......
Kotlin 好用的特性
Lambda
牺牲了 CE 使得 Lambda 不像 Java 中那么多的约束. 引入类似 Ruby 代码块的写法(默认 it 参数), 让代码看起来比较好看, 虽然我个人不是很喜欢这种默认约定, 但是用起来真香.
面向表达式
不同于其他语言, Kotlin 里的 if else,try catch 等都是表达式, 我们可以直接这样子写代码:
- val y = if (x % 2 == 0) "even" else "odd"
- val z = try {
- readFromFile()
- } catch (ex: IOException) {
- ""
- }
- DSL
Lambda 是最后一个参数时, 可以写在括号外面(学自 Ruby). 主要是用来让回调比较好看, 和实现 DSL.
- val ls = listOf(1, 2, 3)
- ls.map {
- 2 * it
- } // returns [2, 4, 6]
Receiver.Kotlin 不仅有纯函数类型, 还可以通过 Receiver 声明类的方法类型. 这个特性可以用来实现类的方法扩展, this 切换的功能.
下面代码给 Int 扩展了个 double 方法:
- val double = fun Int.() = 2 * this
- val x = 3.double() // x = 6
下面例子通过切换 this 实现了一个类似 C# 初始化对象的方法:
- class Obj(init: Obj.() -> Unit) {
- var prop1: Int = 0
- var prop2: String = ""
- init {
- init(this)
- }
- }
- val obj = Obj {
- prop1 = 1
- prop2 = "abc"
- }
其他
很多好用的方法, 像 listOf,mapOf.to 操作符等
......
Kotlin 的坑
Kotlin 没有 final, 但是有 open.
Kotlin 中 Class 默认都是不能继承的. 需要继承的 Class 要在声明的地方加上 open 修饰. 另外提一下有个插件叫 all-open, 专门用来让所有 Kotlin 的类变为可继承的......
注解的继承
Kotlin 不支持可继承的注解.
纯的容器类型
List,Map 不能修改其内部存储的元素. 需要修改应该用 MutableList 和 MutableMap.
Lombok
号称和 Java 100% 兼容, 但是不能访问 Lombok 生成的方法!
因为 Lombok 的方法是编译期通过注解处理器 (annotation processing) 生成的, Kotlin 编译时只调用了 Javac, 所以无法处理 Lombok 定义的方法. 强制先编译 Java 代码, 后编译 Kotlin 代码, 可以解决这个问题, 但是又会有新的问题: 你不能在 Java 代码中调用 Kotlin 代码. 所以如果你要混合使用 Java 和 Kotlin 的话, 推荐所有数据类型都用 Kotlin 写.
val 和 var
var 就是普通变量. val 相当于 const. 平时尽量使用 val, 有益身心健康.
重头戏, null safety
Null safety 是 Kotlin 宣传得最多的特性, 但是我并没有放在 "好用的特性" 节中介绍, 因为它的坑非常多, 以至于我十分怀疑 null safety 的好处是否能抵消它带来的副作用.
所有类型默认都不包括 null 值, 除非加个问号定义为 Nullable 类型. Nullable 类型取值时, 强制 check null. 如果调用 Java 代码, 默认 Java 代码都是 Nullable. 不过从 Java 来的变量不做 check null 倒是不会报 error, 只报 warning. 如果运行时值为 null 的话, 仍然会抛
NullPointerException
.Kotlin 的 null safety 的特性其实只是一个编译器的特性, 通过将 null 与其他类型区分开来, 在类型检查的时候顺便检查了可能出现的
NullPointerException
, 但是在运行时非 Nullable 的变量实际上也是可以放进去 null 值的(比如通过反射).
由于非 Nullable 类型不被赋值为 null 值 (废话), 导致这些类型的变量可能会没有默认值! 这是个严重的问题. 如果是像 Int,String 这种比较像值的类型(其实也是引用类型) 还好, 可以有 0, 空字符串等默认值. 而像自定义的类, 这种类型的变量其实是个引用, 如果不能默认为 null 的话, 那么它的默认值的取值只能有这么几种方案:
类似 C 语言, 未初始化的随机值: 会产生更大更不确定硬隐蔽的问题.
定义一个 "未初始化" 的值: 那么这个值和 null 有什么区别? 又绕回来了.
类似 C++, 默认创建一个空对象: 但是并非所有类都有默认构造函数, 而且在拥有 GC 的语言中, 创建空对象需要分配内存, 还会调用构造函数中的逻辑. 声明变量时引入这么多过程是非常不合适的.
所以, Kotlin 最终选了一种简单粗暴的方案: 禁止变量未初始化.
禁止变量未初始化的问题在于, 当你需要定义大量的数据类的时候, 你就知道有多蛋疼了 -- 所有属性都必须有个初始值. 这不仅需要多敲不少键盘, 影响手指健康, 当碰到属性是非 Nullable 的聚合时, 也常常无法确定其初始值. 我已经隐隐看到某些开发人员将所有变量都标记为 Nullable 的画面了......Kotlin 自身也发现了这个问题, 因此引入了 lateinit 特性, 然而用起来仍然有点令人胆战心惊.
反序列化. 即使是业务逻辑上明确了不会为 null 值的属性, 你也无法保证网络上 / 数据库里传输过来的数据中, 对应的属性会不会是 null 值, 或者干脆漏了, 所以就算 model 设计正确的, 实际运行时可能还是会出现
NullPointerException
. 我又隐约看到某些开发人员将所有变量都标记为 Nullable 的画面了...... 另外反序列化时, 需要先生成一个空对象, 也就是属性都没初始化的对象. 当然 Kotlin 不会允许这么做的, 所以还需要引入 NoArg 插件来自动生成无参数的构造函数......
类型擦除式泛型
为了和 Java 100% 兼容, Kotlin 不得不跟着 Java 用类型擦除式泛型, 也拥有了前面说过的类型擦除式泛型的所有坑. 不过 Kotlin 可以使用内联函数来稍微缓解类型擦除的负面影响. 比如可以这样定义 JSON 反序列化的方法:
- inline fun <reified T> parse(JSON: String): T = objectMapper.readValue(JSON, T::class.java)
- return
Kotlin 有两种方法定义一个匿名函数: lambda 和 anonymous function. 当在这两种方法的函数体中使用 return 时, 执行的语义是不同的. 根据官方文档 return 会跳出最近的显示声明的函数或 anonymous function. 例如下面的 return 会直接跳出 foo 函数.
- fun foo() {
- listOf(1, 2, 3, 4, 5).forEach {
- if (it == 3) return // non-local return directly to the caller of foo()
- print(it)
- }
- println("this point is unreachable")
- }
- // outputs: 12
而下面这个只是当 value == 3 时跳过一次循环, 相当于其他语言的 continue
- fun foo() {
- listOf(1, 2, 3, 4, 5).forEach(fun(value: Int) {
- if (value == 3) return // local return to the caller of the anonymous fun, i.e. the forEach loop
- print(value)
- })
- print("done with anonymous function")
- }
- // outputs: 1245 done with implicit label
或者也可以使用 Label 来指定执行 return 后跳到的位置(感觉像 goto 似的).
- fun foo() {
- listOf(1, 2, 3, 4, 5).forEach lit@{
- if (it == 3) return@lit // local return to the caller of the lambda, i.e. the forEach loop
- print(it)
- }
- print("done with explicit label")
- }
另外, break 和 continue 也是有类似的问题.
写在最后
最近家庭工作都比较忙, 这短短的一篇转型踩坑记竟然写了个跨年. 有些踩坑的记忆随着时间流逝以及用习惯了给慢慢淡化掉了, 于是也没写进来. 目前 Java 系这边的开发我尽量使用 Kotlin, 并没有碰到什么根本上的大问题, 与 Java 的兼容性也挺好的, 有精力的同学可以放心品尝.
来源: https://www.cnblogs.com/skabyy/p/10202006.html