文章中没有奇淫技巧,都是一些在实际开发中经常使用,但很容易被咱们忽略的一些常见问题,源于平时的总结,这篇文章主要对这些常见问题进行分析。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,文章都会同步到这个仓库。