Google 引入 Kotlin 的目的就是爲了讓 Android 開發更加方便,自從官宣 Kotlin 成爲了 Android 開發的首選語言以後,已經有愈來愈多的人開始使用 Kotlin。java
結合着 Kotlin 的高級函數的特性可讓代碼可讀性更強,更加簡潔,可是呢簡潔的背後是有代價的,使用不當對性能可能會有損耗,這塊每每很容易被咱們忽略,這就須要咱們去研究 kotlin 語法糖背後的魔法,當咱們在開發的時候,選擇合適的語法糖,儘可能避免這些錯誤,關於 Kotlin 性能損失那些事,能夠看一下我另外兩篇文章。python
這兩篇文章都分析了 Kotlin 使用不當對性能的影響,不只如此 Kotlin 當中還有不少讓人傻傻分不清楚的語法糖例如 run, with, let, also, apply 等等,這篇文章將介紹一種簡單的方法來區分它們以及如何選擇使用。android
經過這篇文章你將學習到如下內容,文中會給出相應的答案c++
在 Java 中算術運算符只能用於基本數據類型,+ 運算符能夠與 String 值一塊兒使用,可是不能在集合中使用,在 Kotlin 中能夠應用在任何類型,咱們來看一個例子,利用 plus (+) 和 minus (-) 對 Map 集合作運算,以下所示。git
fun main() {
val numbersMap = mapOf("one" to 1, "two" to 2, "three" to 3)
// plus (+)
println(numbersMap + Pair("four", 4)) // {one=1, two=2, three=3, four=4}
println(numbersMap + Pair("one", 10)) // {one=10, two=2, three=3}
println(numbersMap + Pair("five", 5) + Pair("one", 11)) // {one=11, two=2, three=3, five=5}
// minus (-)
println(numbersMap - "one") // {two=2, three=3}
println(numbersMap - listOf("two", "four")) // {one=1, three=3}
}
複製代碼
其實這裏用到了運算符重載,Kotlin 在 Maps.kt 文件裏面,定義了一系列用關鍵字 operator 聲明的 Map 的擴展函數。github
用 operator 關鍵字聲明 plus 函數,能夠直接使用 + 號來作運算,使用 operator 修飾符聲明 minus 函數,能夠直接使用 - 號來作運算,其實咱們也能夠在自定義類裏面實現 plus (+) 和 minus (-) 作運算。面試
data class Salary(var base: Int = 100){
override fun toString(): String = base.toString()
}
operator fun Salary.plus(other: Salary): Salary = Salary(base + other.base)
operator fun Salary.minus(other: Salary): Salary = Salary(base - other.base)
val s1 = Salary(10)
val s2 = Salary(20)
println(s1 + s2) // 30
println(s1 - s2) // -10
複製代碼
在 Map 集合中,可使用 withDefault 設置一個默認值,當鍵不在 Map 集合中,經過 getValue 返回默認值。正則表達式
val map = mapOf(
"java" to 1,
"kotlin" to 2,
"python" to 3
).withDefault { "?" }
println(map.getValue("java")) // 1
println(map.getValue("kotlin")) // 2
println(map.getValue("c++")) // ?
複製代碼
源碼實現也很是簡單,當返回值爲 null 時,返回設置的默認值。算法
internal inline fun <K, V> Map<K, V>.getOrElseNullable(key: K, defaultValue: () -> V): V {
val value = get(key)
if (value == null && !containsKey(key)) {
return defaultValue()
} else {
@Suppress("UNCHECKED_CAST")
return value as V
}
}
複製代碼
可是這種寫法和 plus 操做符在一塊兒用,有一個 bug ,看一下下面這個例子。編程
val newMap = map + mapOf("python" to 3)
println(newMap.getValue("c++")) // 調用 getValue 時拋出異常,異常信息:Key c++ is missing in the map.
複製代碼
這段代碼的意思就是,經過 plus(+) 操做符合並兩個 map,返回一個新的 map, 可是忽略了默認值,因此看到上面的錯誤信息,咱們在開發的時候須要注意這點。
// 傳統的作法
val age = -1;
if (age <= 0) {
throw IllegalArgumentException("age must not be negative")
}
// 使用 require 去檢查
require(age > 0) { "age must be negative" }
// 使用 checkNotNull 檢查
val name: String? = null
checkNotNull(name){
"name must not be null"
}
複製代碼
那麼咱們如何在項目中使用呢,具體的用法能夠查看我 GitHub 上的項目 DataBindingDialog.kt 當中的用法。
感謝大神 Elye 的這篇文章提供的思路 Mastering Kotlin standard functions。
run, with, let, also, apply 都是做用域函數,這些做用域函數如何使用,以及如何區分呢,咱們將從如下三個方面來區分它們。
首先咱們來看一下 with 和 T.run,這兩個函數很是的類似,他們的區別在於 with 是個普通函數,T.run 是個擴展函數,來看一下下面的例子。
val name: String? = null
with(name){
val subName = name!!.substring(1,2)
}
// 使用以前能夠檢查它的可空性
name?.run { val subName = name.substring(1,2) }?:throw IllegalArgumentException("name must not be null")
複製代碼
在這個例子當中,name?.run 會更好一些,由於在使用以前能夠檢查它的可空性。
咱們在來看一下 T.run 和 T.let,它們都是擴展函數,可是他們的參數不同 T.run 的參數是 this, T.let 的參數是 it。
val name: String? = "hi-dhl.com"
// 參數是 this,能夠省略不寫
name?.run {
println("The length is ${this.length} this 是能夠省略的 ${length}")
}
// 參數 it
name?.let {
println("The length is ${it.length}")
}
// 自定義參數名字
name?.let { str ->
println("The length is ${str.length}")
}
複製代碼
在上面的例子中看似 T.run 會更好,由於 this 能夠省略,調用更加的簡潔,可是 T.let 容許咱們自定義參數名字,使可讀性更強,若是傾向可讀性能夠選擇 T.let。
接下里咱們來看一下 T.let 和 T.also 它們接受的參數都是 it, 可是它們的返回值是不一樣的 T.let 返回最後一行,T.also 返回調用自己。
var name = "hi-dhl"
// 返回調用自己
name = name.also {
val result = 1 * 1
"juejin"
}
println("name = ${name}") // name = hi-dhl
// 返回的最後一行
name = name.let {
val result = 1 * 1
"hi-dhl.com"
}
println("name = ${name}") // name = hi-dhl.com
複製代碼
從上面的例子來看 T.also 彷佛沒有什麼意義,細想一下實際上是很是有意義的,在使用以前能夠進行自我操做,結合其餘的函數,功能會更強大。
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }
複製代碼
固然 T.also 還能夠作其餘事情,好比利用 T.also 在使用以前能夠進行自我操做特色,能夠實現一行代碼交換兩個變量,在後面會有詳細介紹
經過上面三個方面,大體瞭解函數的行爲,接下來看一下 T.apply 函數,T.apply 函數是一個擴展函數,返回值是它自己,而且接受的參數是 this。
// 普通方法
fun createInstance(args: Bundle) : MyFragment {
val fragment = MyFragment()
fragment.arguments = args
return fragment
}
// 改進方法
fun createInstance(args: Bundle)
= MyFragment().apply { arguments = args }
// 普通方法
fun createIntent(intentData: String, intentAction: String): Intent {
val intent = Intent()
intent.action = intentAction
intent.data=Uri.parse(intentData)
return intent
}
// 改進方法,鏈式調用
fun createIntent(intentData: String, intentAction: String) =
Intent().apply { action = intentAction }
.apply { data = Uri.parse(intentData) }
複製代碼
以表格的形式彙總,更方便去理解
函數 | 是不是擴展函數 | 函數參數(this、it) | 返回值(調用自己、最後一行) |
---|---|---|---|
with | 不是 | this | 最後一行 |
T.run | 是 | this | 最後一行 |
T.let | 是 | it | 最後一行 |
T.also | 是 | it | 調用自己 |
T.apply | 是 | this | 調用自己 |
接下來演示的是使用 T.also 函數,實現一行代碼交換兩個變量?咱們先來回顧一下 Java 的作法。
int a = 1;
int b = 2;
// Java - 中間變量
int temp = a;
a = b;
b = temp;
System.out.println("a = "+a +" b = "+b); // a = 2 b = 1
// Java - 加減運算
a = a + b;
b = a - b;
a = a - b;
System.out.println("a = " + a + " b = " + b); // a = 2 b = 1
// Java - 位運算
a = a ^ b;
b = a ^ b;
a = a ^ b;
System.out.println("a = " + a + " b = " + b); // a = 2 b = 1
// Kotlin
a = b.also { b = a }
println("a = ${a} b = ${b}") // a = 2 b = 1
複製代碼
來一塊兒分析 T.also 是如何作到的,其實這裏用到了 T.also 函數的兩個特色。
也就是說 b.also { b = a } 會先將 a 的值 (1) 賦值給 b,此時 b 的值爲 1,而後將 b 原始的值(2)賦值給 a,此時 a 的值爲 2,實現交換兩個變量的目的。
使用 in 和 when 關鍵字結合正則表達式,驗證用戶的輸入,這是一個很酷的技巧。
// 使用擴展函數重寫 contains 操做符
operator fun Regex.contains(text: CharSequence) : Boolean {
return this.containsMatchIn(text)
}
// 結合着 in 和 when 一塊兒使用
when (input) {
in Regex("[0–9]") -> println("contains a number")
in Regex("[a-zA-Z]") -> println("contains a letter")
}
複製代碼
in 關鍵字實際上是 contains 操做符的簡寫,它不是一個接口,也不是一個類型,僅僅是一個操做符,也就是說任意一個類只要重寫了 contains 操做符,均可以使用 in 關鍵字,若是咱們想要在自定義類型中檢查一個值是否在列表中,只須要重寫 contains() 方法便可,Collections 集合也重寫了 contains 操做符。
val input = "kotlin"
when (input) {
in listOf("java", "kotlin") -> println("found ${input}")
in setOf("python", "c++") -> println("found ${input}")
else -> println(" not found ${input}")
}
複製代碼
我彙總了一下目前 Kotlin 單例總共有三種寫法:
代碼:
object WorkSingleton
複製代碼
Kotlin 當中 Object 關鍵字就是一個單例,比 Java 的一坨代碼看起來舒服了不少,來看一下編譯後的 Java 文件。
public final class WorkSingleton {
public static final WorkSingleton INSTANCE;
static {
WorkSingleton var0 = new WorkSingleton();
INSTANCE = var0;
}
}
複製代碼
經過 static 代碼塊實現的單例,優勢:餓漢式且是線程安全的,缺點:類加載時就初始化,浪費內存。
利用伴生對象 和 by lazy 也能夠實現單例,代碼以下所示。
class WorkSingleton private constructor() {
companion object {
// 方式一
val INSTANCE1 by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { WorkSingleton() }
// 方式二 默認就是 LazyThreadSafetyMode.SYNCHRONIZED,能夠省略不寫,以下所示
val INSTANCE2 by lazy { WorkSingleton() }
}
}
複製代碼
lazy 的延遲模式有三種:
上面代碼所示 mode = LazyThreadSafetyMode.SYNCHRONIZED,lazy 默認的模式,能夠省掉,這個模式的意思是:若是有多個線程訪問,只有一條線程能夠去初始化 lazy 對象。
當 mode = LazyThreadSafetyMode.PUBLICATION 表達的意思是:對於尚未被初始化的 lazy 對象,能夠被不一樣的線程調用,若是 lazy 對象初始化完成,其餘的線程使用的是初始化完成的值。
mode = LazyThreadSafetyMode.NONE 表達的意思是:只能在單線程下使用,不能在多線程下使用,不會有鎖的限制,也就是說它不會有任何線程安全的保證以及相關的開銷。
經過上面三種模式,這就能夠理解爲何 by lazy 聲明的變量只能用 val,由於初始化完成以後它的值是不會變的。
可是有的時候,但願在單例實例化的時候傳遞參數,例如:
Singleton.getInstance(context).doSome()
複製代碼
上面這兩種形式都不能知足,來看看大神 Christophe Beyls 在這篇文章給出的方法 Kotlin singletons with argument 代碼以下。
class WorkSingleton private constructor(context: Context) {
init {
// Init using context argument
}
companion object : SingletonHolder<WorkSingleton, Context>(::WorkSingleton)
}
open class SingletonHolder<out T : Any, in A>(creator: (A) -> T) {
private var creator: ((A) -> T)? = creator
@Volatile
private var instance: T? = null
fun getInstance(arg: A): T {
val i = instance
if (i != null) {
return i
}
return synchronized(this) {
val i2 = instance
if (i2 != null) {
i2
} else {
val created = creator!!(arg)
instance = created
creator = null
created
}
}
}
}
複製代碼
有沒有感受這和 Java 中雙重校驗鎖的機制很像,在 SingletonHolder 類中若是已經初始化了直接返回,若是沒有初始化進入 synchronized 代碼塊建立對象,利用了 Kotlin 伴生對象提供的很是強大功能,它可以像其餘任何對象同樣從基類繼承,從而實現了與靜態繼承至關的功能。 因此咱們將 SingletonHolder 做爲單例類伴隨對象的基類,在單例類上重用並公開 getInstance()函數。
參數傳遞給 SingletonHolder 構造函數的 creator,creator 是一個 lambda 表達式,將 WorkSingleton 傳遞給 SingletonHolder 類構造函數。
而且不限制傳入參數的類型,凡是須要傳遞參數的單例模式,只需將單例類的伴隨對象繼承於 SingletonHolder,而後傳入當前的單例類和參數類型便可,例如:
class FileSingleton private constructor(path: String) {
companion object : SingletonHolder<FileSingleton, String>(::FileSingleton)
}
複製代碼
到這裏就結束了,Kotlin 的強大不止於此,後面還會分享更多的技巧,在 Kotlin 的道路上還有不少實用的技巧等着咱們一塊兒來探索。
例如利用 Kotlin 的 inline、reified、DSL 等等語法, 結合着 DataBinding、LiveData 等等能夠設計出更加簡潔並利於維護的代碼,更多技巧能夠查看我 GitHub 上的項目 JDataBinding。
致力於分享一系列 Android 系統源碼、逆向分析、算法、翻譯相關的文章,目前正在翻譯一系列歐美精選文章,請持續關注,除了翻譯還有對每篇歐美文章思考,若是對你有幫助,請幫我點個贊,感謝!!!期待與你一塊兒成長。
因爲 LeetCode 的題庫龐大,每一個分類都能篩選出數百道題,因爲每一個人的精力有限,不可能刷完全部題目,所以我按照經典類型題目去分類、和題目的難易程度去排序
每道題目都會用 Java 和 kotlin 去實現,而且每道題目都有解題思路,若是你同我同樣喜歡算法、LeetCode,能夠關注我 GitHub 上的 LeetCode 題解:Leetcode-Solutions-with-Java-And-Kotlin,一塊兒來學習,期待與你一塊兒成長
正在寫一系列的 Android 10 源碼分析的文章,瞭解系統源碼,不只有助於分析問題,在面試過程當中,對咱們也是很是有幫助的,若是你同我同樣喜歡研究 Android 源碼,能夠關注我 GitHub 上的 Android10-Source-Analysis,文章都會同步到這個倉庫