为数不多的人知道的 Kotlin 技巧以及 原理解析(二)

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 File.useLines(charset: Charset = Charsets.UTF_8, block: (Sequence) -> 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 {
operator fun iterator(): Iterator
}

interface Sequence {
operator fun iterator(): Iterator
}

除了代码结构之外,Sequences(序列) 和 Iterator(迭代器) 它们的实现完全不一样。

Sequences(序列)

Sequences 是属于懒加载操作类型,在 Sequences 处理过程中,每一个中间操作不会进行任何计算,它们只会返回一个新的 Sequence,经过一系列中间操作之后,会在末端操作 toListcount 等等方法中进行最终的求职运算,如下图所示。

在 Sequences 处理过程中,会对单个元素进行一系列操作,然后在对下一个元素进行一系列操作,直到所有元素处理完毕。

val data = (1…3).asSequence()
.filter { print("FKaTeX parse error: Expected '}', got 'EOF' at end of input: ….map { print("Mit, "); 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.map(transform: (T) -> R): Sequence {
return TransformingSequence(this, transform)
}

// 末端操作 toList 返回的是一个具体的类型(List)
public fun Sequence.toList(): List {
return this.toMutableList().optimizeReadOnlyList()
}

// 末端操作 forEachIndexed 返回的是一个具体的类型(Unit)
public inline fun Sequence.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("FKaTeX parse error: Expected '}', got 'EOF' at end of input: ….map { print("Mit, "); 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 Iterable.filter(predicate: (T) -> Boolean): List {
return filterTo(ArrayList(), predicate)
}

// 每次操作都会开辟一块新的空间,存储计算的结果
public inline fun <T, R> Iterable.map(transform: (T) -> R): List {
return mapTo(ArrayList(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 处理时间如下所示:

SequencesIterator
119723641

这个结果是很让人吃惊的,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
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 的道路上还有很多实用的技巧等着我们一起来探索。

结语

关注公众号:ByteCode,查看一系列 Android 系统源码、逆向分析、算法、译文、Kotlin、Jetpack 源码相关的文章,如果这篇文章对你有帮助,请帮我点个 star,感谢!!!,欢迎一起来学习,在技术的道路上一起前进。


最后推荐我一直在更新维护的项目和网站:

  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看:AndroidX-Jetpack-Practice

  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析

  • 剑指 offer 及国内外大厂面试题解:在线阅读
  • LeetCode 系列题解:在线阅读
  • 最新 Android 10 源码分析系列文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,仓库持续更新,欢迎前去查看 Android10-Source-Analysis
    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

同时我经过多年的收藏目前也算收集到了一套完整的学习资料以及高清详细的Android架构进阶学习导图及笔记分享给大家,希望对想成为架构师的朋友有一定的参考和帮助。

下面是部分资料截图,诚意满满:特别适合有开发经验的Android程序员们学习。

不论遇到什么困难,都不应该成为我们放弃的理由!

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言,一定会认真查询,修正不足,谢谢。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

希望读者们能谨记这一点。

同时我经过多年的收藏目前也算收集到了一套完整的学习资料以及高清详细的Android架构进阶学习导图及笔记分享给大家,希望对想成为架构师的朋友有一定的参考和帮助。

下面是部分资料截图,诚意满满:特别适合有开发经验的Android程序员们学习。

[外链图片转存中…(img-QfQGaBeQ-1712365025994)]

不论遇到什么困难,都不应该成为我们放弃的理由!

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言,一定会认真查询,修正不足,谢谢。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值