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) {
// 编译测试
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) {
// 编译测试
printlnPeople(People(“dhl”, 80, “beijing”))
}
此时没有更改解构声明,也不会有任何错误,编译输出结果如下所示:
name: dhl
city: 80
得到的结果并不是我们期望的,此时我们不得不更改解构声明的地方,如果代码中有多处用到了解构声明,因为增加了新的属性,就要去更改所有使用解构声明的地方,这明显是不合理的,很容易破坏代码的结构,所以一定要避免将解构声明和数据类一起使用。当我们使用不规范的时候,并且编译器也会给出警告,如下图所示。
Kotlin 提供了很多文件扩展方法 :forEachLine
、 readLines
、 readText
、 useLines
等等方法,帮助我们简化文件的操作,而且使用完成之后,它们会自动关闭,例如 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?
为什么 Kolin 会返回 Sequence,而不返回 Iterator?其实这个核心原因由于 Sequence 和 Iterator 实现不同导致 内存 和 性能 有很大的差异。
接下来我们围绕这两个方面来分析它们的性能,Sequences(序列) 和 Iterator(迭代器) 都是一个比较大的概念,本文的目的不是去分析它们,所以在这里不会去详细分析 Sequence 和 Iterator,只会围绕着 内存 和 性能 两个方面去分析它们的区别,让我们有一个直观的印象。
Sequence 和 Iterator 从代码结构上来看,它们非常的相似如下所示:
interface Iterable {
operator fun iterator(): Iterator
}
interface Sequence {
operator fun iterator(): Iterator
}
除了代码结构之外,Sequences(序列) 和 Iterator(迭代器) 它们的实现完全不一样。
Sequences(序列)
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
如上所示:在 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("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 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 处理时间如下所示:
| Sequences | Iterator |
| — | — |
| 1197 | 23641 |
这个结果是很让人吃惊的,Sequences 比 Iterator 快 19 倍,如果数据量越大,它们的时间差距会越来越大,当我们在读取文件的时候,可能会进行一系列的数据操作 drop
、filter
等等,所以 Kotlin 库函数 useLines
等等方法会返回 Sequences,因为它们更加的高效。
Sequences 和 Iterator 内存对比
假设有 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
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后
都说三年是程序员的一个坎,能否晋升或者提高自己的核心竞争力,这几年就十分关键。
技术发展的这么快,从哪些方面开始学习,才能达到高级工程师水平,最后进阶到Android架构师/技术专家?我总结了这 5大块;
我搜集整理过这几年阿里,以及腾讯,字节跳动,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 PDF(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
Java语言与原理;
大厂,小厂。Android面试先看你熟不熟悉Java语言
高级UI与自定义view;
自定义view,Android开发的基本功。
性能调优;
数据结构算法,设计模式。都是这里面的关键基础和重点需要熟练的。
NDK开发;
未来的方向,高薪必会。
前沿技术;
组件化,热升级,热修复,框架设计
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多
当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。
不出半年,你就能看出变化!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
Java语言与原理;
大厂,小厂。Android面试先看你熟不熟悉Java语言
[外链图片转存中…(img-7zvCNXs9-1712365052787)]
高级UI与自定义view;
自定义view,Android开发的基本功。
[外链图片转存中…(img-AHoqpUZ2-1712365052788)]
性能调优;
数据结构算法,设计模式。都是这里面的关键基础和重点需要熟练的。
[外链图片转存中…(img-ywuUgwdo-1712365052788)]
NDK开发;
未来的方向,高薪必会。
[外链图片转存中…(img-F0LWjvOs-1712365052788)]
前沿技术;
组件化,热升级,热修复,框架设计
[外链图片转存中…(img-GAYQdO0v-1712365052789)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多
当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。
不出半年,你就能看出变化!