爲數很少的人知道的 Kotlin 技巧以及 原理解析(二)

文章中沒有奇淫技巧,都是一些在實際開發中經常使用,但很容易被咱們忽略的一些常見問題,源於平時的總結,這篇文章主要對這些常見問題進行分析。java

以前分享過一篇文章 爲數很少的人知道的 Kotlin 技巧以及 原理解析 主要分析了一些讓人傻傻分不清楚的操做符的原理。git

這篇文章主要分析一些常見問題的解決方案,若是使用不當會對 性能內存 形成的那些影響以及如何規避這些問題,文章中涉及的案例來自 Kotlin 官方、Stackoverflow、Medium 等等網站,都是平時看到,而後進行彙總和分析。github

經過這篇文章你將學習到如下內容:面試

  • 使用 toLowerCasetoUpperCase 等等方法會形成那些影響?
  • 如何優雅的處理空字符串?
  • 爲何解構聲明和數據類不能在一塊兒使用?
  • Kotlin 提供的高效的文件處理方法,以及原理解析?
  • SequenceIterator 有那些不一樣之處?
  • 便捷的 joinToString 方法的使用?
  • 如何用一行代碼實現移除字符串的前綴和後綴?

儘可能少使用 toLowerCase 和 toUpperCase 方法

當咱們比較兩個字符串,須要忽略大小寫的時候,一般的寫法是調用 toLowerCase() 方法或者 toUpperCase() 方法轉換成大寫或者小寫,而後在進行比較,可是這樣的話有一個很差的地方,每次調用 toLowerCase() 方法或者 toUpperCase() 方法會建立一個新的字符串,而後在進行比較。算法

調用 toLowerCase() 方法編程

fun main(args: Array<String>) {
//    use toLowerCase()
    val oldName = "Hi dHL"
    val newName = "hi Dhl"
    val result = oldName.toLowerCase() == newName.toLowerCase()

//    or use toUpperCase()
//    val result = oldName.toUpperCase() == newName.toUpperCase()
}
複製代碼

toLowerCase() 編譯以後的 Java 代碼api

如上圖所示首先會生成一個新的字符串,而後在進行字符串比較,那麼 toUpperCase() 方法也是同樣的以下圖所示。數組

toUpperCase() 編譯以後的 Java 代碼性能優化

這裏有一個更好的解決方案,使用 equals 方法來比較兩個字符串,添加可選參數 ignoreCase 來忽略大小寫,這樣就不須要分配任何新的字符串來進行比較了。bash

fun main(args: Array<String>) {
    val oldName = "hi DHL"
    val newName = "hi dhl"
    val result = oldName.equals(newName, ignoreCase = true)
}
複製代碼

equals 編譯以後的 Java 代碼

使用 equals 方法並無建立額外的對象,若是遇到須要比較字符串的時候,可使用這種方法,減小額外的對象建立。

如何優雅的處理空字符串

當字符串爲空字符串的時候,返回一個默認值,常見的寫法以下所示:

val target = ""
val name = if (target.isEmpty()) "dhl" else target
複製代碼

其實有一個更簡潔的方法,可讀性更強,使用 ifEmpty 方法,當字符串爲空字符串時,返回一個默認值,以下所示。

val name = target.ifEmpty { "dhl" }
複製代碼

其原理跟咱們使用 if 表達式是同樣的,來分析一下源碼。

public inline fun <C, R> C.ifEmpty(defaultValue: () -> R): R where C : CharSequence, C : R =
    if (isEmpty()) defaultValue() else this
複製代碼

ifEmpty 方法是一個擴展方法,接受一個 lambda 表達式 defaultValue ,若是是空字符串,返回 defaultValue,不然不爲空,返回調用者自己。

除了 ifEmpty 方法,Kotlin 庫中還封裝不少其餘很是有用的字符串,例如:將字符串轉爲數字。常見的寫法以下所示:

val input = "123"
val number = input.toInt()
複製代碼

其實這種寫法存在必定問題,假設輸入字符串並非純數字,例如 123ddd 等等,調用 input.toInt() 就會報錯,那麼有沒有更好的寫法呢?以下所示。

val input = "123"
//    val input = "123ddd"
//    val input = ""
val number = input.toIntOrNull() ?: 0
複製代碼

避免將解構聲明和數據類一塊兒使用

這是 Kotlin 團隊一個建議:避免將解構聲明和數據類一塊兒使用,若是之後往數據類添加新的屬性,很容易破壞代碼的結構。咱們一塊兒來思考一下,爲何 Kotlin 官方會這麼說,我先來看一個例子:數據類和解構聲明的使用。

// 數據類
data class People(
        val name: String,
        val city: String
)

fun main(args: Array<String>) {
    // 編譯測試
    printlnPeople(People("dhl", "beijing"))
}

fun printlnPeople(people: People) {
    // 解構聲明,獲取 name 和 city 並將其輸出
    val (name, city) = people
    println("name: ${name}")
    println("city: ${city}")
}
複製代碼

輸出結果以下所示:

name: dhl
city: beijing
複製代碼

隨着需求的變動,須要給數據類 People 添加一個新的屬性 age。

// 數據類,增長了 age
data class People(
        val name: String,
        val age: Int,
        val city: String
)

fun main(args: Array<String>) {
    // 編譯測試
    printlnPeople(People("dhl", 80, "beijing"))
}
複製代碼

此時沒有更改解構聲明,也不會有任何錯誤,編譯輸出結果以下所示:

name: dhl
city: 80
複製代碼

獲得的結果並非咱們指望的,此時咱們不得不更改解構聲明的地方,若是代碼中有多處用到了解構聲明,由於增長了新的屬性,就要去更改全部使用解構聲明的地方,這明顯是不合理的,很容易破壞代碼的結構,因此必定要避免將解構聲明和數據類一塊兒使用。當咱們使用不規範的時候,而且編譯器也會給出警告,以下圖所示。

文件的擴展方法

Kotlin 提供了不少文件擴展方法 Extensions for java.io.ReadeforEachLinereadLinesreadTextuseLines 等等方法,幫助咱們簡化文件的操做,並且使用完成以後,它們會自動關閉,例如 useLines 方法:

File("dhl.txt").useLines { line ->
    println(line)
}
複製代碼

useLines 是 File 的擴展方法,調用 useLines 會返回一個文件中全部行的 Sequence,當文件內容讀取完畢以後,它會自動關閉,其源碼以下。

public inline fun <T> File.useLines(charset: Charset = Charsets.UTF_8, block: (Sequence<String>) -> T): T =
    bufferedReader(charset).use { block(it.lineSequence()) }
複製代碼
  • useLines 是 File 的一個擴展方法
  • useLines 接受一個 lambda 表達式 block
  • 調用了 BufferedReader 讀取文件內容,以後調用 block 返回文件中全部行的 Sequence 給調用者

那它是如何在讀取完畢自動關閉的呢,核心在 use 方法裏面,在 useLines 方法內部調用了 use 方法,use 方法也是一個擴展方法,源碼以下所示。

public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
    var exception: Throwable? = null
    try {
        return block(this)
    } catch (e: Throwable) {
        exception = e
        throw e
    } finally {
        when {
            apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
            this == null -> {}
            exception == null -> close()
            else ->
                try {
                    close()
                } catch (closeException: Throwable) {
                    // cause.addSuppressed(closeException) // ignored here
                }
        }
    }
}
複製代碼

其實很簡單,調用 try...catch...finally 最後在 finally 內部進行 close。其實咱們也能夠根據源碼實現一個通用的異常捕獲方法。

inline fun <T, R> T.dowithTry(block: (T) -> R) {
    try {
        block(this)
    } catch (e: Throwable) {
        e.printStackTrace()
    }
}

// 使用方式
dowithTry {
    // 添加會出現異常的代碼, 例如
    val result = 1 / 0
}
複製代碼

固然這只是一個很是簡單的異常捕獲方法,在實際項目中還有不少須要去處理的,好比說異常信息需不須要返回給調用者等等。

在上文中提到了調用 useLines 方法返回一個文件中全部行的 Sequence,爲何 Kolin 會返回 Sequence,而不返回 Iterator?

Sequence 和 Iterator 不一樣之處

爲何 Kolin 會返回 Sequence,而不返回 Iterator?其實這個核心緣由因爲 Sequence 和 Iterator 實現不一樣致使 內存性能 有很大的差別。

接下來咱們圍繞這兩個方面來分析它們的性能,Sequences(序列) 和 Iterator(迭代器) 都是一個比較大的概念,本文的目的不是去分析它們,因此在這裏不會去詳細分析 Sequence 和 Iterator,只會圍繞着 內存性能 兩個方面去分析它們的區別,讓咱們有一個直觀的印象。更多信息能夠查看國外一位大神寫的文章 Prefer Sequence for big collections with more than one processing step

Sequence 和 Iterator 從代碼結構上來看,它們很是的類似以下所示:

interface Iterable<out T> {
    operator fun iterator(): Iterator<T>
}

interface Sequence<out T> {
    operator fun iterator(): Iterator<T>
}
複製代碼

除了代碼結構以外,Sequences(序列) 和 Iterator(迭代器) 它們的實現徹底不同。

Sequences(序列)

Sequences 是屬於懶加載操做類型,在 Sequences 處理過程當中,每個中間操做不會進行任何計算,它們只會返回一個新的 Sequence,通過一系列中間操做以後,會在末端操做 toListcount 等等方法中進行最終的求職運算,以下圖所示。

在 Sequences 處理過程當中,會對單個元素進行一系列操做,而後在對下一個元素進行一系列操做,直到全部元素處理完畢。

val data = (1..3).asSequence()
        .filter { print("F$it, "); it % 2 == 1 }
        .map { print("M$it, "); it * 2 }
        .forEach { print("E$it, ") }
println(data)

// 輸出 F1, M1, E2, F2, F3, M3, E6
複製代碼

Sequences

如上所示:在 Sequences 處理過程當中,對 1 進行一系列操做輸出 F1, M1, E2, 而後對 2 進行一系列操做,依次類推,直到全部元素處理完畢,輸出結果爲 F1, M1, E2, F2, F3, M3, E6

在 Sequences 處理過程當中,每個中間操做( map、filter 等等 )不進行任何計算,只有在末端操做( toList、count、forEach 等等方法 ) 進行求值運算,如何區分是中間操做仍是末端操做,看方法的返回類型,中間操做返回的是 Sequence,末端操做返回的是一個具體的類型( List、int、Unit 等等 )源碼以下所示。

// 中間操做 map ,返回的是  Sequence
public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
    return TransformingSequence(this, transform)
}

// 末端操做 toList 返回的是一個具體的類型(List)
public fun <T> Sequence<T>.toList(): List<T> {
    return this.toMutableList().optimizeReadOnlyList()
}

// 末端操做 forEachIndexed 返回的是一個具體的類型(Unit)
public inline fun <T> Sequence<T>.forEachIndexed(action: (index: Int, T) -> Unit): Unit {
    var index = 0
    for (item in this) action(checkIndexOverflow(index++), item)
}
複製代碼
  • 若是是中間操做 map、filter 等等,它們返回的是一個 Sequence,不會進行任何計算
  • 若是是末端操做 toList、count、forEachIndexed 等等,返回的是一個具體的類型( List、int、Unit 等等 ),會作求值運算

Iterator(迭代器)

在 Iterator 處理過程當中,每一次的操做都是對整個數據進行操做,須要開闢新的內存來存儲中間結果,將結果傳遞給下一個操做,代碼以下所示:

val data = (1..3).asIterable()
        .filter { print("F$it, "); it % 2 == 1 }
        .map { print("M$it, "); it * 2 }
        .forEach { print("E$it, ") }
println(data)

// 輸出 F1, F2, F3, M1, M3, E2, E6
複製代碼

Iterator

如上所示:在 Iterator 處理過程當中,調用 filter 方法對整個數據進行操做輸出 F1, F2, F3,將結果存儲到 List 中, 而後將結果傳遞給下一個操做 ( map ) 輸出 M1, M3 將新的結果在存儲的 List 中, 直到全部操做處理完畢。

// 每次操做都會開闢一塊新的空間,存儲計算的結果
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

// 每次操做都會開闢一塊新的空間,存儲計算的結果
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
複製代碼

對於每次操做都會開闢一塊新的空間,存儲計算的結果,這是對內存極大的浪費,咱們每每只關心最後的結果,而不是中間的過程。

瞭解完 Sequences 和 Iterator 不一樣之處,接下里咱們從 性能內存 兩個方面來分析 Sequences 和 Iterator。

Sequences 和 Iterator 性能對比

分別使用 Sequences 和 Iterator 調用它們各自的 filter、map 方法,處理相同的數據的狀況下,比較它們的執行時間。

使用 Sequences :

val time = measureTimeMillis {
    (1..10000000 * 10).asSequence()
            .filter { it % 2 == 1 }
            .map { it * 2 }
            .count()
}

println(time) // 1197
複製代碼

使用 Iterator :

val time2 = measureTimeMillis {
    (1..10000000 * 10).asIterable()
            .filter { it % 2 == 1 }
            .map { it * 2 }
            .count()
}

println(time2) // 23641
複製代碼

Sequences 和 Iterator 處理時間以下所示:

Sequences Iterator
1197 23641

這個結果是很讓人吃驚的,Sequences 比 Iterator 快 19 倍,若是數據量越大,它們的時間差距會愈來愈大,當咱們在讀取文件的時候,可能會進行一系列的數據操做 dropfilter 等等,因此 Kotlin 庫函數 useLines 等等方法會返回 Sequences,由於它們更加的高效。

Sequences 和 Iterator 內存對比

這裏使用了 Prefer Sequence for big collections with more than one processing step 文章的一個例子。

有 1.53 GB 犯罪分子的數據存儲在文件中,從文件中找出有多少犯罪分子攜帶大麻,分別使用 Sequences 和 Iterator,咱們先來看一下若是使用 Iterator 處理會怎麼樣(這裏調用 readLines 函返回 List<String>

File("ChicagoCrimes.csv").readLines()
   .drop(1) // Drop descriptions of the columns
   .mapNotNull { it.split(",").getOrNull(6) } 
    // Find description
   .filter { "CANNABIS" in it } 
   .count()
   .let(::println)
複製代碼

運行完以後,你將會獲得一個意想不到的結果 OutOfMemoryError

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
複製代碼

調用 readLines 函返回一個集合,有 3 箇中間操做,每個中間操做都須要一塊空間存儲 1.53 GB 的數據,它們須要佔用超過 4.59 GB 的空間,每次操做都開闢了一塊新的空間,這是對內存巨大浪費。若是咱們使用序列 Sequences 會怎麼樣呢?(調用 useLines 方法返回的是一個 Sequences)。

File("ChicagoCrimes.csv").useLines { lines ->
// The type of `lines` is Sequence<String>
   lines
       .drop(1) // Drop descriptions of the columns
       .mapNotNull { it.split(",").getOrNull(6) } 
       // Find description
       .filter { "CANNABIS" in it } 
       .count()
       .let { println(it) } // 318185
複製代碼

沒有出現 OutOfMemoryError 異常,共耗時 8.3 s,因而可知對於文件操做使用序列不只能提升性能,還能減小內存的使用,從性能和內存這兩面也解釋了爲何 Kotlin 庫的擴展方法 useLines 等等,讀取文件的時候使用 Sequences 而不使用 Iterator。

便捷的 joinToString 方法的使用

joinToString 方法提供了一組豐富的可選擇項( 分隔符,前綴,後綴,數量限制等等 )可用於將可迭代對象轉換爲字符串。

val data = listOf("Java", "Kotlin", "C++", "Python")
        .joinToString(
                separator = " | ",
                prefix = "{",
                postfix = "}"
        ) {
            it.toUpperCase()
        }

println(data) // {JAVA | KOTLIN | C++ | PYTHON}
複製代碼

這是很常見的用法,將集合轉換成字符串,高效利用便捷的joinToString 方法,開發的時候事半功倍,既然能夠添加前綴,後綴,那麼能夠移除它們嗎? 能夠的,Kotlin 庫函數提供了一些方法,幫助咱們實現,以下代碼所示。

var data = "**hi dhl**"

// 移除前綴
println(data.removePrefix("**")) //  hi dhl**
// 移除後綴
println(data.removeSuffix("**")) //  **hi dhl
// 移除前綴和後綴
println(data.removeSurrounding("**")) // hi dhl

// 返回第一次出現分隔符後的字符串
println(data.substringAfter("**")) // hi dhl**
// 若是沒有找到,返回原始字符串
println(data.substringAfter("--")) // **hi dhl**
// 若是沒有找到,返回默認字符串 "no match"
println(data.substringAfter("--","no match")) // no match

data = "{JAVA | KOTLIN | C++ | PYTHON}"

// 移除前綴和後綴
println(data.removeSurrounding("{", "}")) // JAVA | KOTLIN | C++ | PYTHON
複製代碼

有了這些 Kotlin 庫函數,咱們就不須要在作 startsWith()endsWith() 的檢查了,若是讓咱們本身來實現上面的功能,咱們須要花多少行代碼去實現呢,一塊兒來看一下 Kotlin 源碼是如何實現的,上面的操做符最終都會調用如下代碼,進行字符串的檢查和截取。

public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > value.length) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    int subLen = endIndex - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return ((beginIndex == 0) && (endIndex == value.length)) ? this
            : new String(value, beginIndex, subLen);
}
複製代碼

參考源碼的實現,若是之後遇到相似的需求,可是 Kotlin 庫函數有沒法知足咱們,咱們能夠以源碼爲基礎進行擴展。

全文到這裏就結束了,Kotlin 的強大不止於此,後面還會分享更多的技巧,在 Kotlin 的道路上還有不少實用的技巧等着咱們一塊兒來探索。

正在創建一個最全、最新的 AndroidX Jetpack 相關組件的實戰項目 以及 相關組件原理分析文章,目前已經包含了 App Startup、Paging三、Hilt 等等,正在逐漸增長其餘 Jetpack 新成員,倉庫持續更新,能夠前去查看:AndroidX-Jetpack-Practice, 若是這個倉庫對你有幫助,請倉庫右上角幫我點個贊。

結語

致力於分享一系列 Android 系統源碼、逆向分析、算法、翻譯、Jetpack 源碼相關的文章,正在努力寫出更好的文章,若是這篇文章對你有幫助給個 star,文章中有什麼沒有寫明白的地方,或者有什麼更好的建議歡迎留言,一塊兒來學習,期待與你一塊兒成長。

算法

因爲 LeetCode 的題庫龐大,每一個分類都能篩選出數百道題,因爲每一個人的精力有限,不可能刷完全部題目,所以我按照經典類型題目去分類、和題目的難易程度去排序。

  • 數據結構: 數組、棧、隊列、字符串、鏈表、樹……
  • 算法: 查找算法、搜索算法、位運算、排序、數學、……

每道題目都會用 Java 和 kotlin 去實現,而且每道題目都有解題思路、時間複雜度和空間複雜度,若是你同我同樣喜歡算法、LeetCode,能夠關注我 GitHub 上的 LeetCode 題解:Leetcode-Solutions-with-Java-And-Kotlin,一塊兒來學習,期待與你一塊兒成長。

Android 10 源碼系列

正在寫一系列的 Android 10 源碼分析的文章,瞭解系統源碼,不只有助於分析問題,在面試過程當中,對咱們也是很是有幫助的,若是你同我同樣喜歡研究 Android 源碼,能夠關注我 GitHub 上的 Android10-Source-Analysis,文章都會同步到這個倉庫。

Android 應用系列

精選譯文

目前正在整理和翻譯一系列精選國外的技術文章,不只僅是翻譯,不少優秀的英文技術文章提供了很好思路和方法,每篇文章都會有譯者思考部分,對原文的更加深刻的解讀,能夠關注我 GitHub 上的 Technical-Article-Translation,文章都會同步到這個倉庫。

工具系列

相關文章
相關標籤/搜索