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 的能力發揮到極致,若是您以爲手冊中有錯誤須要指出,或是想要添加新的內容,能夠聯繫我