Kotlin优势浅析 我们为什么应该使用Kotlin开发新项目

Backgroud/Target

由Jetbrains在圣彼得堡的团队开发,得名于附近的一个Kotlin的小岛。

Jetbrains有多年的Java平台开发经验,他们认为Java编程语言有一定的局限性,而且由于需要向后兼容,它们很难得到解决。因此,他们创建了Kotlin项目,主要目标包括:

  • 兼容Java
  • 编译速度至少同Java一样快
  • 比Java更安全
  • 比Java更简洁
  • 比最成熟的竞争者Scala还简单

与其他JVM上的语言一样,编译成Java字节码

https://www.kotlincn.net/docs/tutorials/ 中文文档

https://kotlinlang.org/docs/reference/comparison-to-java.html 英文文档中对比Java的索引

为什么要使用

非语言层面

  • Jetbrains自己用于开发桌面IDE
  • 谷歌钦定,不用担心跑路没支持
  • 谷歌Demo和很多开源项目开始全面采用,不学看不懂了
  • Android Studio支持完善

语言层面

对下文中提到的Kotlin做出的语言上的改进做一个总结:

  • 通过语法层面的改进规范了一些行为
  • “消灭”了NPE
  • 语法更加灵活清晰/减少冗杂的代码
  • 变量声明更加符合直觉
  • 代码逻辑更加收敛/符合直觉
  • 减少样板代码的使用
  • 更舒服的lambda

最终归在一点:集中精力 提高效率 减少错误

Kotlin做出的改变

变量/类型

1
2
3
4
5
6
7
8
9
// 只读变量声明(更友好) 想想final
val a: Int = 1 // 后置类型声明
// 一般利用类型推断,思维更加顺畅,不用再关心参数是什么类型的问题
val a = 5
val s = String()
val clazz = s.getClass()
val method = clazz.getDeclaredMethod("name", null)
// 可变变量声明
var x = 5

不再有基本类型的概念,但运行时仍是基本类型表示(除了Int?…这类nullable变量,是包装类型)。

数组用Array<>类型表示,现在数组也是不可协变的了。

控制流

if else语句除了与java一致的用法外,还取代了条件运算符?:

1
val max = if (a > b) a else b // int max = (a > b) ? a : b;

但用法更加灵活:

1
2
3
4
5
6
7
return if (a > b) {
print("return a")
a
} else {
print("return b")
b
}

for的语法糖:

1
2
3
4
5
6
7
8
9
10
11
12
for (i in array.indices) {
println(array[i])
}
for ((index, value) in array.withIndex()) {
println("the element at $index is $value")
}
for (i in 1..3) {
println(i)
}
for (i in 6 downTo 0 step 2) {
println(i)
}

when取代switch,更加强大的分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
when (x) {
0, 1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}
when (x) {
in 1..10 -> print("x is in the range")
!in 10..20 -> print("x is outside the range")
else -> print("none of the above")
}
// 有返回值
val hasPrefix = when(x) {
is String -> x.startsWith("prefix")
else -> false
}
when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}

类和对象

引入属性的概念,隐式的gettersetter

1
2
3
4
5
6
7
8
class Test {
var a = 0 // has setter and getter
val b = 1 // just has getter
private c = 2 // no setter and getter
}

val test = Test()
test.a = test.b // test.setA(test.getB())

再也不用写setter/getter逻辑了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value)
}

// 相当于

public String getStringRepresentation() {
return this.toString();
}

public void setStringRepresentation(String value) {
setDataFromString(value)
}

java中已有的getter/setter会被“转换”成kotlin属性的形式

Null安全

图灵奖得主托尼·霍尔把Null这个设计称为十亿美元错误:“它导致了数不清的错误、漏洞和系统崩溃,可能在之后 40 年中造成了十亿美元的损失”

Java中,引入了@NonNull@Nullable注解辅助进行静态检查,有局限性。

Java8引入Optional<>来解决这个问题,写起来比较恶心,难以推广。

Kotlin希望在语言层面去解决这个问题->引入Nullable类型概念:

  • 声明为非空的变量永远都不会为空
  • 声明为可空的变量使用时必须判空
  • 利用推断来提高效率
    1
    2
    var nonnull: String = "must be inited with object"
    var nullable: String? = null

在语法层面做了诸多改进:

1
2
3
4
5
6
val 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不执行

推断的作用(智能转换):

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
}

如果被Java调用,由于Java无法保证非空(除非已经使用@NonNull注解注明),从Java接收的参数必须是可空的。

实际使用中,使得定义变量时必须要考虑是否可为空的问题,在一开始时如果不适应这种思维,可能会滥用可空类型,给调用带来麻烦。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
class MyFragment : Fragment() {
private var manager: MyAPIManager? = null

@Override
public void onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

manager = MyAPIManager(context)
manager.authorize()
}
}

这里第二行中,由于Kotlin要求我们必须为属性赋予一个初值,但这里的初始化需要用到后面传入的context,按照Java的思维习惯,这个地方很容易就直接把类型改成可空的,然后给了个null的初值,但是这其实违背了Kotlin提供的特性:

  • 我们知道其实这个manager对象一旦被初始化之后就不会再为空,所以这应当是个非空类型
  • 同时我们为了后面去初始化它把它设成了var,实际上它并不应当被重新赋值,所以这应当是个val对象

Kotlin为我们提供了解决问题的方法:

懒属性(Lazy Property)

当这个属性第一次被使用前再执地初始化代码,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
class MyFragment : Fragment() {
private val manager: MyAPIManager by lazy {
MyAPIManager(context)
}

@Override
public void onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

manager.authorize()
}
}

懒初始化属性(Lateinit Property)

在随后某个确定的时刻初始化,如果在使用时尚未被初始化,会抛出一个未初始化的运行时错误(与NPE略微不同),代码如下:

1
2
3
4
5
6
7
8
9
10
11
class MyFragment : Fragment() {
lateinit var manager: MyAPIManager

@Override
public void onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

manager = MyAPIManager(context)
manager.authorize()
}
}

但这时manager仍是一个var,美中不足。

使用这样的机制可以确保这个对象的可空性满足我们的预期,也就是说经过这样的处理的对象,在Kotlin中永远不会报NPE。而确实可为空的对象,我们利用?表达式结合合适的默认值,是可以把NPE消灭的。

但没有NPE是一件好事吗?错误可能会因默认值变得隐含,虽然不会导致Crash,但给定位bug增加了一定难度。

Kotlin也提供了报NPE的办法:使用!!

何时用,用哪个?

  1. lateinit只用于var,而lazy只用于val。如果是值可修改的变量(即在之后的使用中可能被重新赋值),使用lateinit模式
  2. 如果变量的初始化取决于外部对象(例如需要一些外部变量参与初始化),使用lateinit模式。这种情况下,lazy模式也可行但并不直接适用。
  3. 如果变量仅仅初始化一次并且全局共享,且更多的是内部使用(依赖于类内部的变量),请使用lazy模式。从实现的角度来看,lateinit模式仍然可用,但lazy模式更有利于封装初始化代码。
  4. 不考虑对变量值是否可变的控制,lateinit模式是lazy模式的超集,你可以在任何使用lazy模式的地方用lateinit模式替代, 反之不然。lateinit模式在函数中暴露了太多的逻辑代码,使得代码更加混乱,所以推荐使用lazy,更好的封装了细节,更加安全。

https://kotlinlang.org/docs/reference/null-safety.html

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
    4
    fun dfs() {
    }
    val f = ::dfs
    f(graph)

函数参数终于可以有缺省值了(不用Builder了):

1
2
3
4
5
6
7
8
9
10
fun 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
2
3
4
5
6
7
// 写一个Util类,作为参数传进去
public class ViewUtils {
public static int findColor(View view, int resId) {
return view.getResources().getColor(resId);
}
}
ViewUtils.findColor(view, resId);

通过扩展方法来解决:

1
2
3
4
5
fun View.findColor(id: Int) : Int {
return this.resources.getColor(id)
}

view.findColor(resId)

一系列这种类型的Java工具类在Kotlin中被“改造”成了扩展方法例如:

Collection.sort(list)在Kotlin中直接list.sort()就可以了。

可以完全取代以往的Util类。

Kotlin提供的作用域扩展函数

语法简洁,逻辑连贯的最主要体现。

  • let/run
    • 对象作为参数传入lambdarun则作为this
    • 返回值为lambda表达式的返回值
    • 常见场景:
      • 转换类型
      • 处理nullable类型
1
2
3
4
val length = s?.let {
doSomething(it)
it.length
} ?: 0
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) }
}
  • 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
6
return 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
        2
        val 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
7
File(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
21
fun 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

https://proandroiddev.com/the-difference-between-kotlins-functions-let-apply-with-run-and-else-ca51a4c696b8

Lambda表达式

本质上是一个匿名方法(单方法接口)

1
2
3
4
5
6
7
8
9
fun isGreaterThanZero(n: Int): Boolean {
return n > 0
}

collection.filter(::isGreaterThanZero)
// 使用Lambda
collection.filter{ i: Int -> i > 0 }
collection.filter{ i -> i > 0 }
collection.filter{ it > 0 }

单方法接口都可以传Lambda

1
2
3
4
5
6
7
8
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showToast();
}
});

button.setOnClickListener{ showToast() }

内置常用的流处理Lambda

1
2
3
4
5
names
.filter{ it.startsWith("A") }
.sortedBy{ it }
.map{ it.toUpperCase() }
.forEach{ print(it) }

配合Rxjava使用更佳(也有Rxkotlin)。

在Android上

官方提供的扩展

View Binding

全自动,无需声明,无需findViewById,直接使用layout id就行

1
text_view.text = "text"

@Parcelable 注解

一个注解自动实现Parcelable, 仍在实验阶段

异步

1
2
3
4
5
6
7
// uiThread如果是Activity isFinish = true是不会调用的
doAsync {
print("其他线程")
uiThread {
print("UI线程")
}
}

Gradle DSL

https://docs.gradle.org/current/dsl/index.html

其他黑科技

  • 没有必检异常了
  • 支持运算符重载(String的比较可以用==了)
  • 接口可以有缺省方法
  • Object Class单例
  • Data Class数据类型,自动实现equals/hashCode/toString
  • 协程(没用过)
  • 伴生对象
  • Anko扩展

https://www.jianshu.com/p/9f720b9ccdea

https://www.tuicool.com/articles/aEbeayN

https://github.com/android/android-ktx

https://github.com/adisonhuang/awesome-kotlin-android 其他开源库

与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%的体积。

https://discuss.kotlinlang.org/t/kotlin-generated-class-file-out-of-kt-is-bigger-than-java-file/1520/4

https://blog.dreamtobe.cn/2016/11/30/kotlin/

编译速度

冷编译会慢一些,由于增量编译的存在,热编译速度比Java快。