Kotlin优势浅析 我们为什么应该使用Kotlin开发新项目
Backgroud/Target
由Jetbrains在圣彼得堡的团队开发,得名于附近的一个Kotlin的小岛。
Jetbrains有多年的Java平台开发经验,他们认为Java编程语言有一定的局限性,而且由于需要向后兼容,它们很难得到解决。因此,他们创建了Kotlin项目,主要目标包括:
- 兼容Java
- 编译速度至少同Java一样快
- 比Java更安全
- 比Java更简洁
- 比最成熟的竞争者Scala还简单
与其他JVM上的语言一样,编译成Java字节码
https://kotlinlang.org/docs/reference/comparison-to-java.html 英文文档中对比Java的索引
为什么要使用
非语言层面
- Jetbrains自己用于开发桌面IDE
- 谷歌钦定,不用担心跑路没支持
- 谷歌Demo和很多开源项目开始全面采用,不学看不懂了
- Android Studio支持完善
语言层面
对下文中提到的Kotlin做出的语言上的改进做一个总结:
- 通过语法层面的改进规范了一些行为
- “消灭”了NPE
- 语法更加灵活清晰/减少冗杂的代码
- 变量声明更加符合直觉
- 代码逻辑更加收敛/符合直觉
- 减少样板代码的使用
- 更舒服的lambda
最终归在一点:集中精力 提高效率 减少错误
Kotlin做出的改变
变量/类型
1 | // 只读变量声明(更友好) 想想final |
不再有基本类型的概念,但运行时仍是基本类型表示(除了Int?
…这类nullable
变量,是包装类型)。
数组用Array<>
类型表示,现在数组也是不可协变的了。
控制流
if else
语句除了与java一致的用法外,还取代了条件运算符?:
1 | val max = if (a > b) a else b // int max = (a > b) ? a : b; |
但用法更加灵活:
1 | return if (a > b) { |
for
的语法糖:
1 | for (i in array.indices) { |
when
取代switch
,更加强大的分支:
1 | when (x) { |
类和对象
引入属性的概念,隐式的getter
与setter
:
1 | class Test { |
再也不用写setter/getter
逻辑了:
1 | var stringRepresentation: String |
java中已有的getter/setter
会被“转换”成kotlin属性的形式
Null安全
图灵奖得主托尼·霍尔把Null
这个设计称为十亿美元错误:“它导致了数不清的错误、漏洞和系统崩溃,可能在之后 40 年中造成了十亿美元的损失”
Java中,引入了@NonNull
和@Nullable
注解辅助进行静态检查,有局限性。
Java8引入Optional<>
来解决这个问题,写起来比较恶心,难以推广。
Kotlin希望在语言层面去解决这个问题->引入Nullable
类型概念:
- 声明为非空的变量永远都不会为空
- 声明为可空的变量使用时必须判空
- 利用推断来提高效率在语法层面做了诸多改进:
1
2var nonnull: String = "must be inited with object"
var nullable: String? = null推断的作用(智能转换):1
2
3
4
5
6val l = s?.length // s != null, l = s.length else l = null, l: Int?
val l = s!!.length // same as l = s.length in java, l: Int
val l = s?.length ?: 0 // s != null, l = s.length else l = 0, l: Int
return myValue ?: -1
// 链式使用:
bob?.department?.head?.name // 任一为null不执行如果被Java调用,由于Java无法保证非空(除非已经使用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// b: String?
if (b != null && b.length > 0) {
// b: String here
print("length ${b.length}")
} else {
print("Empty string")
}
fun getStringLength(obj: Any): Int? {
if (obj is String) {
// automatically cast to `String`
return obj.length
}
// `obj` is still of type `Any` outside of the type-checked branch
return null
}@NonNull
注解注明),从Java接收的参数必须是可空的。
实际使用中,使得定义变量时必须要考虑是否可为空的问题,在一开始时如果不适应这种思维,可能会滥用可空类型,给调用带来麻烦。
举个例子:
1 | class MyFragment : Fragment() { |
这里第二行中,由于Kotlin要求我们必须为属性赋予一个初值,但这里的初始化需要用到后面传入的context
,按照Java的思维习惯,这个地方很容易就直接把类型改成可空的,然后给了个null
的初值,但是这其实违背了Kotlin提供的特性:
- 我们知道其实这个
manager
对象一旦被初始化之后就不会再为空,所以这应当是个非空类型 - 同时我们为了后面去初始化它把它设成了
var
,实际上它并不应当被重新赋值,所以这应当是个val
对象
Kotlin为我们提供了解决问题的方法:
懒属性(Lazy Property)
当这个属性第一次被使用前再执地初始化代码,代码如下:
1 | class MyFragment : Fragment() { |
懒初始化属性(Lateinit Property)
在随后某个确定的时刻初始化,如果在使用时尚未被初始化,会抛出一个未初始化的运行时错误(与NPE略微不同),代码如下:
1 | class MyFragment : Fragment() { |
但这时manager
仍是一个var
,美中不足。
使用这样的机制可以确保这个对象的可空性满足我们的预期,也就是说经过这样的处理的对象,在Kotlin中永远不会报NPE
。而确实可为空的对象,我们利用?
表达式结合合适的默认值,是可以把NPE
消灭的。
但没有NPE
是一件好事吗?错误可能会因默认值变得隐含,虽然不会导致Crash,但给定位bug增加了一定难度。
Kotlin也提供了报NPE
的办法:使用!!
。
何时用,用哪个?
lateinit
只用于var
,而lazy
只用于val
。如果是值可修改的变量(即在之后的使用中可能被重新赋值),使用lateinit
模式- 如果变量的初始化取决于外部对象(例如需要一些外部变量参与初始化),使用
lateinit
模式。这种情况下,lazy
模式也可行但并不直接适用。 - 如果变量仅仅初始化一次并且全局共享,且更多的是内部使用(依赖于类内部的变量),请使用
lazy
模式。从实现的角度来看,lateinit
模式仍然可用,但lazy
模式更有利于封装初始化代码。 - 不考虑对变量值是否可变的控制,
lateinit
模式是lazy
模式的超集,你可以在任何使用lazy
模式的地方用lateinit
模式替代, 反之不然。lateinit
模式在函数中暴露了太多的逻辑代码,使得代码更加混乱,所以推荐使用lazy
,更好的封装了细节,更加安全。
https://dev.to/adammc331/understanding-nullability-in-kotlin
https://stackoverflow.com/questions/35723226/how-to-use-kotlins-with-expression-for-nullable-types
https://medium.com/@agrawalsuneet/safe-calls-vs-null-checks-in-kotlin-f7c56623ab30
函数、扩展方法、Lambda表达式
函数像其他函数式语言一样,成为了“一等公民”
- 函数可以在任意地方声明(类外部,甚至是在函数内部)
- 函数可以像对象一样通过参数传递:函数参数终于可以有缺省值了(不用
1
2
3
4fun dfs() {
}
val f = ::dfs
f(graph)Builder
了):1
2
3
4
5
6
7
8
9
10fun reformat(str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ') {
// do something
}
reformat(str)
reformat(str, wordSeparator = ' ') // 可以使用参数的名字给缺省参数赋值
// 可以通过@JvmOverloads注解生成Java的重载形式便于Java来调用扩展方法
比如说,给别人的View
加一个功能,给定一个资源Id
,去取得它对应的资源:
1 | // 写一个Util类,作为参数传进去 |
通过扩展方法来解决:
1 | fun View.findColor(id: Int) : Int { |
一系列这种类型的Java工具类在Kotlin中被“改造”成了扩展方法例如:
Collection.sort(list)
在Kotlin中直接list.sort()
就可以了。
可以完全取代以往的Util
类。
Kotlin提供的作用域扩展函数
语法简洁,逻辑连贯的最主要体现。
let/run
- 对象作为参数传入
lambda
(run
则作为this
) - 返回值为
lambda
表达式的返回值 - 常见场景:
- 转换类型
- 处理
nullable
类型
- 对象作为参数传入
1 | val length = s?.let { |
1 | // if...else...写法 |
apply
- 对象作为
this
传入lambda
- 返回值为对象本身
- 常见场景:
- 初始化对象
1
2
3
4
5
6
7
8
9
10
11// old way of building an object
val andre = Person()
andre.name = "andre"
andre.company = "Viacom"
andre.hobby = "losing in ping pong"
// after applying 'apply' (pun very much intended)
val andre = Person().apply {
name = "Andre"
company = "Viacom"
hobby = "losing in ping pong"
}1
2
3
4
5
6return itemView.animation_like.apply {
imageAssetsFolder = "images_feedcell/"
loop(false)
setAnimation("like_small.json")
setOnClickListener(onClickListener)
}
- 初始化对象
- 对象作为
also
- 对象作为参数传入
lambda
- 返回值为对象本身
- 常见场景:
- 链式调用中的副作用
1
2
3
4
5
6
7
8// transforming data from api with intermediary variable
val rawData = api.getData()
Log.debug(rawData)
rawData.map { /** other stuff */ }
// use 'also' to stay in the method chains
api.getData()
.also { Log.debug(it) }
.map { /** other stuff */ }
- 链式调用中的副作用
- 对象作为参数传入
takeIf/takeUnless
- 对象作为参数传入
lambda
- 返回值为对象本身或
null
(根据lambda
中语句的true or false
) - 常见场景:
- 链式调用形式的条件判断混合使用举例:
1
2val outFile
= File(outputDir.path).takeIf { it.exists() } ?: return false简洁,避免大量判空1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// if...else...写法
private fun testIfElse(): Object? {
return if (a !== null) {
val b = handleA(a)
if (b !== null) {
handleB(b)
} else {
null
}
} else {
null
}
}
// ?.let写法
private fun testLet(): Object? {
return a?.let { handleA(it) }?.let { handleB(it) }
}if
的使用可以将逻辑划分清楚,直观,避免判空打断思路。1
2
3
4
5
6
7File(url).takeIf { it.exists() }
?.let {
JSONObject(NetworkUtils.postFile(20 * 1024, "http://i.snssdk.com/2/data/upload_image/", "image", url))
}?.takeIf { it.optString("message") == "success" }
?.let {
post(content, contact, it.optJSONObject("data")?.optString("web_uri"))
} ?: mHandler.post { view?.onFail() }同样是划分逻辑,更加清晰?(需要适应)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21fun getMessages(context: Context, cursor: Int, direction: Int): ModelResult<MessageResponse> {
return UrlBuilder(LOAD_NOTIFY)
.apply {
addParam("cursor", cursor)
addParam("direction", direction)
}.let {
queryDataFromServer(it.build())
}?.let {
val statusCode = it.optInt("status_code", -1)
val statusMessage = it.optString("status_message")
if (statusCode == 0) {
MessageParser.parseMessageList(it.optString("data"))
?.let {
ModelResult(true, statusMessage, it)
}
?: ModelResult<MessageResponse>()
} else {
}
}
?: ModelResult<MessageResponse>())
}
- 链式调用形式的条件判断
- 对象作为参数传入
附图一张:
https://medium.com/@elye.project/using-kotlin-takeif-or-takeunless-c9eeb7099c22
https://proandroiddev.com/the-tldr-on-kotlins-let-apply-also-with-and-run-functions-6253f06d152b
Lambda表达式
本质上是一个匿名方法(单方法接口)
1 | fun isGreaterThanZero(n: Int): Boolean { |
单方法接口都可以传Lambda
:
1 | button.setOnClickListener(new View.OnClickListener() { |
内置常用的流处理Lambda
:
1 | names |
配合Rxjava
使用更佳(也有Rxkotlin
)。
在Android上
官方提供的扩展
View Binding
全自动,无需声明,无需findViewById
,直接使用layout id
就行
1 | text_view.text = "text" |
@Parcelable 注解
一个注解自动实现Parcelable
, 仍在实验阶段
异步
1 | // uiThread如果是Activity isFinish = true是不会调用的 |
Gradle DSL
其他黑科技
- 没有必检异常了
- 支持运算符重载(
String
的比较可以用==
了) - 接口可以有缺省方法
Object Class
单例Data Class
数据类型,自动实现equals/hashCode/toString
- 协程(没用过)
- 伴生对象
Anko
扩展
与Java协作
结论:100%协同,Kotlin调Java没有问题,Java调Kotlin会有些绕,但不会出问题。
性能对比
运行性能
来源:https://blog.dreamtobe.cn/kotlin-performance/
- 性能相比Java更差相关
- 对
varargs
参数展开,Kotlin比Java慢1倍,主要原因是在Kotlin在展开varargs
前需要全量拷贝整个数组,这个是非常高的性能开销。 - 对
Delegated Properties
的应用,Kotlin相比Java慢10%。
- 对
- 性能相比Java更优相关
- 对
Lambda
的使用,Kotlin相比Java快30%,而对用例中的transaction
添加inline
关键字配置内联后,发现其反而慢了一点点(约1.14%)。 - Kotlin对
companion object
的访问相比Java中的静态变量的访问,Kotlin与Java差不多快或更快一点。 - Kotlin对局部函数(
Local Functions
)的访问相比Java中的局部函数的访问,Kotlin与Java差不多快或更快一点。 - Kotlin的非空参数的使用相比没有使用空检查的Java,Kotlin与Java差不多快或更快一点。
- 对
- Kotlin自身比较
- 对于基本类型范围的使用,无论是否使用常量引用还是直接的范围速度都差不多。
- 对于非基本类型范围的使用,常量引用相比直接的范围会快3%左右。
- 对于范围遍历方式中,
for
循环方式无论有没有使用step
速度都差不多,但是如果对范围直接进行.foreach
速度会比它们慢3倍,因此避免对范围直接使用.foreach
。 - 在遍历中使用
lastIndex
会比使用indices
快2%左右。
包大小
标准库大小100k
左右。
新建标准工程(不带Kotlin支持),开启混淆,打release包。
将这个工程文件转为Kotlin实现,引入Kotlin支持,打release包,对比大小:
增加了约109k
将某应用一个module转换为Kotlin实现(直接使用AS的工具转换),对比编译生成的所有class
文件大小:
增加了约2%的体积。
编译速度
冷编译会慢一些,由于增量编译的存在,热编译速度比Java快。