Kotlin 是一门新兴的编程语言,它具有空安全,函数式编程等许多 Java 所不具有的新特色,许多同事对它的一些细节尚未那么熟悉,本手册旨在帮助你们了解一些在学习中不易被记忆的 Kotlin 的使用细节;其中包括一些类库中定义的许多方便的扩展函数,或者一些更为优雅的语法细节;使用本手册中介绍的 Kotlin 技巧,能够帮助您的代码更优雅,更具备复用性,甚至能够作到零警告,咱们都知道,编译器给出的警告虽然不会影响代码的运行,可是也表明了一种潜在的危险,所以,消除警告就很是有意义;咱们将经过如下几点,来详细展开本手册的内容:算法
在 Kotlin 中,咱们应尽可能将变量声明成非空的,这样作有利于最大程度的杜绝空指针异常,但有时,咱们会遇到必须将变量声明称可空类型的状况,面对这种状况,Kotlin也提供了多种语法来帮助咱们进行可空调用,这里相信你们都理解,因此只作一个简短回顾。编程
class Person(val name: String? = "", val age: Int = 0) {
fun doSomething() {
println("do something")
}
}
fun function(person: Person?) {
if (person != null) {
println(person.age)
}
}
复制代码
fun function(person: Person?) {
person?.doSomething()
}
复制代码
fun function(person: Person?) {
println(person.age ?: 23)
}
复制代码
fun function(person: Person?) {
person?.let {
println(it.age)
println(it.name ?: "Mark")
}
}
复制代码
fun function(person: Person?) {
person!!.domeSomething()
}
复制代码
这几种可空调用语法分别对应不一样的状况,这里直接说使用它们的原则:数组
以上这些 tips 的结论来自于两点,首先是判空次数,例如 3 和 4 的状况,若是要连续进行屡次非空调用,实际上只须要进行一次判空就能够了,这样能够节省判空带来的开销,若是在 3 和 4 的状况拼房使用非空调用,将是一种性能糟糕的写法。其次,代码的优雅程度,咱们应该尽量的减小代码的复杂度和潜逃层级(即大括号嵌套的状况),使代码更易阅读,Elvis 运算符和非空调用都是这样的的语法糖,所以,在非空调用和Elvis运算符可以胜任的简单状况下,咱们无需使用非空判断和let函数。最后一点,非空断言是很是危险的一种写法,在个人编程经验中,一般有两种状况才会用到,其一是本身手动实现某种数据结构时,例如用 kotlin 作算法题的时候常常会用到,其二是当一个可空对象被赋值后当即使用它时,咱们明明已经给它赋值过,可是编译器显然没法理解这一点,在编译器看来,可空类型就是另一种类型,若是不进行手动强制转型或者通过非空判断的智能转换,它都不能够直接调用成员函数,但这时在咱们极度确信的状况下,咱们才可以使用非空断言。安全
咱们有时为了加快启动速度,只须要让对象在它第一次被调用的时候进行初始化,或者咱们在声明一个对象时,因为缺乏某些必要的参数,没法当即进行初始化,这时咱们须要使用延时初始化功能。bash
若是咱们须要将一个对象延时初始化,咱们首先应该使用的是 by lazy 写法,以下所示:数据结构
private val mRed by lazy { ContextCompat.getColor(mContext, R.color.red) }
复制代码
有时候咱们须要根据不一样的状况给一个 TextView 设置为不一样的颜色,但咱们无需在这个界面(多是 Activity 也多是 Fragment 或一个 RecyclerView的Adapter)初始化的时候就把全部须要的颜色都初始化给一个成员引用,而是在第一次用到这个颜色的时候再初始化它,这样能够节省界面打开的耗时,也能在必定程度上节约内存(例如在初始化一个更大的对象的时候更为明显)。多线程
在另一些状况下,咱们的某个对象的初始化须要一些在声明时没法获取到的参数,这时候 by lazy 没法胜任这样的工做,而 lateinit 在这时就是最优的写法,其中一个很重要的缘由就是 lateinit 属性在调用本身的方法或使用本身的成员变量的时候无需进行可空调用。并发
class MyPresenter() : Presenter {
private lateinit var mView: View
override fun init(baseView: View) {
mView = baseView
}
}
复制代码
以上这种情形,在当前 OKEx 的 Android 客户端中常常会见到,是咱们在使用MVP模式时最多见的 Presenter 的例子。我曾见过一些早期代码,采用以下方式进行延时初始化:app
class MyPresenter() : Presenter {
private var mView: View? = null
override fun init(baseView: View) {
mView = baseView
}
}
复制代码
看上去变化不大,就是将 lateinit 换成了可空类型;这样写的弊端在于,以后在每次 mView 调用方法的时候都要进行可空调用,上一节中已经说了,可空调用的本质就是在调用前进行判空操做,在 MVP 这样的样例中,这样的操做显然是多余的,由于咱们能够确保 mView 只要在 inti() 函数中赋值就必定是非空的。dom
换一种思路想一下,若是咱们的lateinit属性没有赋值,咱们就使用它调用它本身的成员函数,会发生什么状况?会报错,而程序会 crash。可是在某些特殊状况下,咱们确实不能确保 lateinit 属性已经被初始化了,这时咱们可使用以下的语法对 lateinit 属性进行检查:
if (thia::mView.isInitialized) {
// 还未初始化
} else {
// 已经初始化
}
复制代码
经过成员引用的方式能够获取到对象的 isInitialized 属性,若是它为 true,则表示还未初始化,不然就是已经初始化;若是你的 lateinit 对象经常使你不能肯定你在使用它时是否它已经被初始化,即若是每次使用它都要进行判断的话,建议仍是使用可空类型,这样会更优。
Kotlin 的字符串和 Java 的字符串大致上没有区别,可是 Kotlin 增长了许多扩展函数,字符串操做进行了大量的语法优化,本节主要介绍一些经常使用且典型的扩展库函数。
咱们经常须要对字符串进行空判断,咱们在使用 Java 时常使用 TextUtils 类的 isEmpty() 方法,可是在 Kotlin 中咱们有更优秀的写法:
val str1 = "123"
str1.isEmpty() // false
str1.isBlank() // false
str1.isNotEmpty() // true
str1.isNotBlank() // true
val str2 = ""
str2.isEmpty() // true
str2.isBlank() // true
str2.isNotEmpty() // false
str2.isNotBlank() // false
val str3 = " "
str3.isEmpty() // false
str3.isBlank() // true
str3.isNotEmpty() // true
str3.isNotBlank() // false
复制代码
如上面的例子所示,一共有四个函数:isEmpty(),isBlank(),isNotEmpty(),isNotBlank()。其中 isEmpty() 与 isNotEmpty() 是互逆的,而 isBlank() 与 isNotBlank() 也是互逆的。之因此要有这样互逆的两个函数是由于它们能够在咱们写程序时语意表达更清晰,而没必要使用“!”这样的取反操做符来下降代码的可读性。 isEmpty()和isBlank()的区别在于,若是一串字符串只含有空格,isEmpty()会返回false,而isBlank()会返回true。
在 Java 中,咱们可使用 charAt(int index) 方法来获取字符串指定位置的 char 字符,而在 Kotlin 中,这个方法被隐藏了,取而代之的是 get(index: Int);这是一个小细节,没什么值得多说的,具体状况看下面的例子:
//Java
String str = "Android";
char c = str.charAt(2); //c = ‘d’
//Kotlin
val str = "Android"
char c = str.get(2) //c = 'd'
复制代码
若是咱们要判断一个字符串中是否包含某个子字符串或某个子字符,咱们可使用 in 操做符,具体如如下示例:
val str = "Kotlin"
val b1 = 't' in str //true
val b2 = "lin" in str //true
复制代码
字符串模版是一种可读性更高的语法,它优于传统的"+"操做符。以下所示:
val str1 = 'I'
val str2 = "love"
// 推荐写法,输出:I love Kotlin
println("$str1 $str2 Kotlin")
// 不推荐的“+”写法,输出:I love Kotlin
println(str1 + " " + str2 + " " + "Kotlin")
复制代码
val list = ArrayList<String>()
// Java 时代的旧写法
if (list.size == 0) // 集合为空
if (list.size != 0) // 集合不为空
// Kotlin 提供的扩展函数
if (list.isEmpty()) // 集合为空
if (list.isNotEmpty()) // 集合不为空
复制代码
咱们可使用 isEmpty() 和 isNotEmpty() 来判断这个集合容器中是否已经装入元素。
val list = ArrayList<String>()
list.forEach {
//最多见的遍历集合的元素
}
list.forEachIndexed { index, str ->
// 带标号的遍历,index是指当前元素在集合中的位置,而str则是元素自己的引用
}
复制代码
讲解请看样例代码的注释。 在少数状况下,咱们仍是须要使用 for 循环来遍历集合,咱们一般会这样写:
for (i in 0 until list.size) {
//do something......
}
复制代码
可是 until 和".."操做符咱们有时常常会用错,所幸 Kotlin 的集合有更优的写法:
for (i in list.indices) {
//do something......
}
复制代码
成员indices直接表示集合的区间。
在《Kotlin 实战》的第五章的 5.2 小节中介绍了大量的集合的流操做函数,它们支持链式调用,其中包括:filter(过滤),map(转换),all(是否都知足某条件),find(查找)等等,使用它们能够对集合进行一些强大且复杂的操做,且这些函数的算法都是优化过的,这一章的后续内容还介绍了一些更为复杂的函数,以及将集合转化为序列,使得内存空间利用率更高等,具体内容请参照相关章节。 在这里我介绍一些书中没有提到的,可是很是强大的函数。
val list = ArrayList<Person>()
// 将集合中包含的元素的某一属性,所有相加求和
val age = list.sumBy { it.age } // 获得全部人的总年龄
// 将集合中以某一条件判断的,最大/最小的元素,装入数组并返回
val maxAge = list.maxBy { it.age }
val maxAge = list.minBy { it.age }
复制代码
这里再对 maxBy 和 minBy 两个函数多说两句,例如集合中有三我的,a,b,c;且a.age = 22,b.age = 23,c.age = 23。调用 maxBy 后会返回一个 Array 数组,数组中包含 b 和 c,调用 minBy 后也会返回一个数组,数组中只包含 a。即这两个函数会找到最大值/最小值,而后把全部拥有最大值/最小值的元素都装入数组并返回。
在编写多线程程序时,咱们在使用 Java 的时候经常会使用一些关键字,例如 synchronized 用来声明方法或代码块是同步的,volatile 关键字用来表示变量是可见性的;可是 Kotlin 中没有了这两个关键词,取而代之,咱们可使用注解 @Synchronized 与 @Volatile 来表示同步方法和可见性属性。 若是咱们要对一个代码块添加同步锁,咱们可使用以下库函数:
// 对应Java中synchronized关键字修饰的同步代码块
synchronized(Any) {
// 同步代码块
}
复制代码
除此以外的一些使用类库完成的同步操做,例如可重入锁 ReentrantLock 等不受影响。 在 Java 中咱们经常使用 try-finally 语法来添加使用 ReentrantLock,而在 Kotlin 中咱们使用 tryLock 扩展函数:
// Java 中使用 RenntrantLock
Lock lock = new ReentrantLock();
try {
lock.lock(); // 上锁
/**
* 同步代码
*/
} finally {
lock.unLock(); // 解锁
}
// Kotlin 中使用 RenntrantLock
val lock = ReentrantLock()
lock.tryLock {
/**
* 同步代码
*/
}
复制代码
try-finally 语法不优雅不说,还须要咱们手动的上锁和解锁,而 tryLock 扩展函数在内部封装了上锁与解锁的动做,更优雅方便以及准确,咱们在编码时,若是设计使用可重入锁,咱们应该使用 tryLock 扩展函数。
注意:本节内容只是针对兼容老代码的状况,在协程引入项目后,咱们应该使用协程来进行异步和并发,尽可能避免直接使用线程;使用协程的目的是避免进程内存在大量阻塞状态的线程消耗系统资源以及协程能够完全消灭在线程层面上的死锁状况出现。 更多细节,请参阅协程相关的资料。
解构在简化语法上很是有用:
// 遍历 Map
for ((key, value) in mHashMap) {
// do something
}
复制代码
当一个表达式或者函数要返回两个或两个以上的值的时候,解构也很是优雅:
val (color, backgrond) = if (isRed) {
mRed to mRedBackground
} else {
mGreen to mGreenBackground
}
复制代码
在 Java 7 以前(虽然在 Java 7 以后也有不少人这么写)咱们常使用 try-finally 语法来使用 IO 流,这和上面可重入锁的问题同样——须要咱们显式关闭流,在 Java 7 以后,Java 提供了 try-with-resource 语法来优化这个问题。 但在 Kotlin 中没有 try-with-resource 语法,取而代之的是 use 扩展函数:
// Java 7 以前
BufferReader bufferReader = new BufferedReader(new InputStreamReader(context.assets.open(fileName), "UTF-8"));
try {
// IO 流操做
bufferReader.readLine();
} finally {
bufferReader.close();
}
// Java 7 以及以后的版本
try (BufferReader bufferReader = new BufferedReader(new InputStreamReader(context.assets.open(fileName), "UTF-8"))) {
// IO 流操做
bufferReader.readLine();
}
// Kotlin
BufferedReader(InputStreamReader(context.assets.open(fileName), "UTF-8")).use {
// IO 流操做
it.readLine()
}
复制代码
当咱们频繁调用某一个对象的属性或者方法的时候,最大的不优雅之处就在于咱们要将这个对象写无数遍(这在编写 RecyclerView 的 Adapter 的 onBindViewHolder 方法的代码时尤其常见),咱们能够经过 apply 或者 with 函数来消除这种样板代码,其中的原理是——带接收者的 lambda:
// 不使用任何优化
holder.mTitle.text = "123456"
holder.mContent.text = "123456"
holder.mTime.text = "123456"
holder.mImageView.setBitmap(data.bitmap)
// 使用 with 函数
with(holder) {
mTitle.text = "123456"
mContent.text = "123456"
mTime.text = "123456"
mImageView.setBitmap(data.bitmap)
}
// 使用 apply 函数
holder.apply {
mTitle.text = "123456"
mContent.text = "123456"
mTime.text = "123456"
mImageView.setBitmap(data.bitmap)
}
复制代码
咱们能够看到,使用这两个函数能够将每一行代码本来要写的 holder 所有省略掉;with 函数与 apply 的区别除了例子中能看出来的——with 是顶层函数,而 apply 是扩展函数外,另外一个区别就在于 apply 函数返回调用它的对象,而 with 函数不返回任何东西。 除此以外,apply 函数的用途有时候能够相似于 上面空安全中所讲的 let 函数,假如上面例子中的 holder 是可空类型,那么使用 let 和 apply 就是下面这样:
// let 函数
holder?.let {
it.mTitle.text = "123456"
it.mContent.text = "123456"
it.mTime.text = "123456"
it.mImageView.setBitmap(data.bitmap)
}
// 使用 apply 函数
holder?.apply {
mTitle.text = "123456"
mContent.text = "123456"
mTime.text = "123456"
mImageView.setBitmap(data.bitmap)
}
复制代码
能够看到,在这种状况下 apply 比 let 更优雅,因此 apply 函数在空安全中也大有可为,和 let 函数相比应该用谁,要视具体状况。
Kotlin 做为在 Android 平台上取代 Java 的新语言,拥有大量 Java 无可比拟的语法特性,既然咱们公司的项目已经采用 Kotlin,咱们就争取将 Kotlin 的能力发挥到极致,若是您以为手册中有错误须要指出,或是想要添加新的内容,能够联系我