文章中沒有奇淫技巧,都是一些在實際開發中經常使用,但很容易被咱們忽略的一些常見問題,源於平時的總結,這篇文章主要對這些常見問題進行分析。java
以前分享過一篇文章 爲數很少的人知道的 Kotlin 技巧以及 原理解析 主要分析了一些讓人傻傻分不清楚的操做符的原理。git
這篇文章主要分析一些常見問題的解決方案,若是使用不當會對 性能 和 內存 形成的那些影響以及如何規避這些問題,文章中涉及的案例來自 Kotlin 官方、Stackoverflow、Medium 等等網站,都是平時看到,而後進行彙總和分析。github
經過這篇文章你將學習到如下內容:面試
toLowerCase
和 toUpperCase
等等方法會形成那些影響?Sequence
和 Iterator
有那些不一樣之處?joinToString
方法的使用?當咱們比較兩個字符串,須要忽略大小寫的時候,一般的寫法是調用 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.Reade
:forEachLine
、 readLines
、 readText
、 useLines
等等方法,幫助咱們簡化文件的操做,並且使用完成以後,它們會自動關閉,例如 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那它是如何在讀取完畢自動關閉的呢,核心在 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?
爲何 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 處理過程當中,每個中間操做不會進行任何計算,它們只會返回一個新的 Sequence,通過一系列中間操做以後,會在末端操做 toList
或 count
等等方法中進行最終的求職運算,以下圖所示。
在 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 處理過程當中,對 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)
}
複製代碼
在 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 處理過程當中,調用 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 調用它們各自的 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 倍,若是數據量越大,它們的時間差距會愈來愈大,當咱們在讀取文件的時候,可能會進行一系列的數據操做 drop
、filter
等等,因此 Kotlin 庫函數 useLines
等等方法會返回 Sequences,由於它們更加的高效。
這裏使用了 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
方法提供了一組豐富的可選擇項( 分隔符,前綴,後綴,數量限制等等 )可用於將可迭代對象轉換爲字符串。
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 源碼,能夠關注我 GitHub 上的 Android10-Source-Analysis,文章都會同步到這個倉庫。
目前正在整理和翻譯一系列精選國外的技術文章,不只僅是翻譯,不少優秀的英文技術文章提供了很好思路和方法,每篇文章都會有譯者思考部分,對原文的更加深刻的解讀,能夠關注我 GitHub 上的 Technical-Article-Translation,文章都會同步到這個倉庫。