这里的 world
是参数 name
的默认值,当调用该函数时不传参数,就会使用该默认值。
这就等价于上面 Java 写的重载方法,当调用 sayHi
函数时,参数是可选的:
🏝️
sayHi(“kaixue.io”)
sayHi() // 使用了默认值 “world”
既然与重载函数的效果相同,那 Kotlin 中的参数默认值有什么好处呢?仅仅只是少写了一些代码吗?
其实在 Java 中,每个重载方法的内部实现可以各不相同,这就无法保证重载方法内部设计上的一致性,而 Kotlin 的参数默认值就解决了这个问题。
不过参数默认值在调用时也不是完全可以放飞自我的。
来看下面这段代码,这里函数中有默认值的参数在无默认值参数的前面:
🏝️
fun sayHi(name: String = “world”, age: Int) {
…
}
sayHi(10)
// 👆 这时想使用默认值进行调用,IDE 会报以下两个错误
// The integer literal does not conform to the expected type String
// No value passed for parameter ‘age’
这个错误就是告诉你参数不匹配,说明我们的「打开方式」不对,其实 Kotlin 里是通过「命名参数」来解决这个问题的。
命名参数
具体用法如下:
🏝️
fun sayHi(name: String = “world”, age: Int) {
…
}
👇
sayHi(age = 21)
在调用函数时,显式地指定了参数 age
的名称,这就是「命名参数」。Kotlin 中的每一个函数参数都可以作为命名参数。
再来看一个有非常多参数的函数的例子:
🏝️
fun sayHi(name: String = “world”, age: Int, isStudent: Boolean = true, isFat: Boolean = true, isTall: Boolean = true) {
…
}
当函数中有非常多的参数时,调用该函数就会写成这样:
🏝️
sayHi(“world”, 21, false, true, false)
当看到后面一长串的布尔值时,我们很难分清楚每个参数的用处,可读性很差。通过命名参数,我们就可以这么写:
🏝️
sayHi(name = “wo”, age = 21, isStudent = false, isFat = true, isTall = false)
与命名参数相对的一个概念被称为「位置参数」,也就是按位置顺序进行参数填写。
当一个函数被调用时,如果混用位置参数与命名参数,那么所有的位置参数都应该放在第一个命名参数之前:
🏝️
fun sayHi(name: String = “world”, age: Int) {
…
}
sayHi(name = “wo”, 21) // 👈 IDE 会报错,Mixing named and positioned arguments is not allowed
sayHi(“wo”, age = 21) // 👈 这是正确的写法
讲完了命名参数,我们再看看 Kotlin 中的另一种常见函数:嵌套函数。
本地函数(嵌套函数)
首先来看下这段代码,这是一个简单的登录的函数:
🏝️
fun login(user: String, password: String, illegalStr: String) {
// 验证 user 是否为空
if (user.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
// 验证 password 是否为空
if (password.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
}
该函数中,检查参数这个部分有些冗余,我们又不想将这段逻辑作为一个单独的函数对外暴露。这时可以使用嵌套函数,在 login
函数内部声明一个函数:
🏝️
fun login(user: String, password: String, illegalStr: String) {
👇
fun validate(value: String, illegalStr: String) {
if (value.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
}
👇
validate(user, illegalStr)
validate(password, illegalStr)
}
这里我们将共同的验证逻辑放进了嵌套函数 validate
中,并且 login
函数之外的其他地方无法访问这个嵌套函数。
这里的 illegalStr
是通过参数的方式传进嵌套函数中的,其实完全没有这个必要,因为嵌套函数中可以访问在它外部的所有变量或常量,例如类中的属性、当前函数中的参数与变量等。
我们稍加改进:
🏝️
fun login(user: String, password: String, illegalStr: String) {
fun validate(value: String) {
if (value.isEmpty()) {
👇
throw IllegalArgumentException(illegalStr)
}
}
…
}
这里省去了嵌套函数中的 illegalStr
参数,在该嵌套函数内直接使用外层函数 login
的参数 illegalStr
。
上面 login
函数中的验证逻辑,其实还有另一种更简单的方式:
🏝️
fun login(user: String, password: String, illegalStr: String) {
require(user.isNotEmpty()) { illegalStr }
require(password.isNotEmpty()) { illegalStr }
}
其中用到了 lambda 表达式以及 Kotlin 内置的 require
函数,这里先不做展开,之后的文章会介绍。
字符串
讲完了普通函数的简化写法,Kotlin 中字符串也有很多方便写法。
字符串模板
在 Java 中,字符串与变量之间是使用 +
符号进行拼接的,Kotlin 中也是如此:
🏝️
val name = “world”
println("Hi " + name)
但是当变量比较多的时候,可读性会变差,写起来也比较麻烦。
Java 给出的解决方案是 String.format
:
☕️
System.out.print(String.format(“Hi %s”, name));
Kotlin 为我们提供了一种更加方便的写法:
🏝️
val name = “world”
// 👇 用 ‘$’ 符号加参数的方式
println(“Hi $name”)
这种方式就是把 name
从后置改为前置,简化代码的同时增加了字符串的可读性。
除了变量,$
后还可以跟表达式,但表达式是一个整体,所以我们要用 {}
给它包起来:
🏝️
val name = “world”
println(“Hi ${name.length}”)
其实就跟四则运算的括号一样,提高语法上的优先级,而单个变量的场景可以省略 {}
。
字符串模板还支持转义字符,比如使用转义字符 \n
进行换行操作:
🏝️
val name = “world!\n”
println(“Hi $name”) // 👈 会多打一个空行
字符串模板的用法对于我们 Android 工程师来说,其实一点都不陌生。
首先,Gradle 所用的 Groovy 语言就已经有了这种支持:
def name = “world”
println “Hi ${name}”
在 Android 的资源文件里,定义字符串也有类似用法:
Hi %s
☕️
getString(R.id.hi, “world”);
raw string (原生字符串)
有时候我们不希望写过多的转义字符,这种情况 Kotlin 通过「原生字符串」来实现。
用法就是使用一对 """
将字符串括起来:
🏝️
val name = “world”
val myName = “kotlin”
👇
val text = “”"
Hi $name!
My name is $myName.\n
“”"
println(text)
这里有几个注意点:
\n
并不会被转义- 最后输出的内容与写的内容完全一致,包括实际的换行
$
符号引用变量仍然生效
这就是「原生字符串」。输出结果如下:
Hi world!
My name is kotlin.\n
但对齐方式看起来不太优雅,原生字符串还可以通过 trimMargin()
函数去除每行前面的空格:
🏝️
val text = “”"
👇
|Hi world!
|My name is kotlin.
“”".trimMargin()
println(text)
输出结果如下:
Hi world!
My name is kotlin.
这里的 trimMargin()
函数有以下几个注意点:
|
符号为默认的边界前缀,前面只能有空格,否则不会生效- 输出时
|
符号以及它前面的空格都会被删除 - 边界前缀还可以使用其他字符,比如
trimMargin("/")
,只不过上方的代码使用的是参数默认值的调用方式
字符串的部分就先到这里,下面来看看数组与集合有哪些更方便的操作。
数组和集合
数组与集合的操作符
在之前的文章中,我们已经知道了数组和集合的基本概念,其实 Kotlin 中,还为我们提供了许多使数组与集合操作起来更加方便的函数。
首先声明如下 IntArray
和 List
:
🏝️
val intArray = intArrayOf(1, 2, 3)
val strList = listOf(“a”, “b”, “c”)
接下来,对它们的操作函数进行讲解:
forEach
:遍历每一个元素
🏝️
// 👇 lambda 表达式,i 表示数组的每个元素
intArray.forEach { i ->
print(i + " ")
}
// 输出:1 2 3
除了「lambda」表达式,这里也用到了「闭包」的概念,这又是另一个话题了,这里先不展开。
filter
:对每个元素进行过滤操作,如果 lambda 表达式中的条件成立则留下该元素,否则剔除,最终生成新的集合
🏝️
// [1, 2, 3]
⬇️
// {2, 3}
// 👇 注意,这里变成了 List
val newList: List = intArray.filter { i ->
i != 1 // 👈 过滤掉数组中等于 1 的元素
}
map
:遍历每个元素并执行给定表达式,最终形成新的集合
🏝️
// [1, 2, 3]
⬇️
// {2, 3, 4}
val newList: List = intArray.map { i ->
i + 1 // 👈 每个元素加 1
}
flatMap
:遍历每个元素,并为每个元素创建新的集合,最后合并到一个集合中
🏝️
// [1, 2, 3]
⬇️
// {“2”, “a” , “3”, “a”, “4”, “a”}
intArray.flatMap { i ->
listOf(“${i + 1}”, “a”) // 👈 生成新集合
}
关于为什么数组的 filter
之后变成 List
,就留作思考题吧~
这里是以数组 intArray
为例,集合 strList
也同样有这些操作函数。Kotlin 中还有许多类似的操作函数,这里就不一一列举了。
除了数组和集合,Kotlin 中还有另一种常用的数据类型: Range
。
Range
在 Java 语言中并没有 Range
的概念,Kotlin 中的 Range
表示区间的意思,也就是范围。区间的常见写法如下:
🏝️
👇 👇
val range: IntRange = 0…1000
这里的 0..1000
就表示从 0 到 1000 的范围,包括 1000,数学上称为闭区间 [0, 1000]。除了这里的 IntRange
,还有 CharRange
以及 LongRange
。
Kotlin 中没有纯的开区间的定义,不过有半开区间的定义:
🏝️
👇
val range: IntRange = 0 until 1000
这里的 0 until 1000
表示从 0 到 1000,但不包括 1000,这就是半开区间 [0, 1000) 。
Range
这个东西,天生就是用来遍历的:
🏝️
val range = 0…1000
// 👇 默认步长为 1,输出:0, 1, 2, 3, 4, 5, 6, 7…1000,
for (i in range) {
print("$i, ")
}
这里的 in
关键字可以与 for
循环结合使用,表示挨个遍历 range
中的值。关于 for
循环控制的使用,在本期文章的后面会做具体讲解。
除了使用默认的步长 1,还可以通过 step
设置步长:
🏝️
val range = 0…1000
// 👇 步长为 2,输出:0, 2, 4, 6, 8, 10,…1000,
for (i in range step 2) {
print("$i, ")
}
以上是递增区间,Kotlin 还提供了递减区间 downTo
,不过递减没有半开区间的用法:
🏝️
// 👇 输出:4, 3, 2, 1,
for (i in 4 downTo 1) {
print("$i, ")
}
其中 4 downTo 1
就表示递减的闭区间 [4, 1]。这里的 downTo
以及上面的 step
都叫做「中缀表达式」,之后的文章会做介绍。
Sequence
在上一期中我们已经熟悉了 Sequence
的基本概念,这次让我们更加深入地了解 Sequence
序列的使用方式。
序列 Sequence
又被称为「惰性集合操作」,关于什么是惰性,我们通过下面的例子来理解:
🏝️
val sequence = sequenceOf(1, 2, 3, 4)
val result: List = sequence
.map { i ->
println(“Map $i”)
i * 2
}
.filter { i ->
println(“Filter $i”)
i % 3 == 0
}
👇
println(result.first()) // 👈 只取集合的第一个元素
-
惰性的概念首先就是说在「👇」标注之前的代码运行时不会立即执行,它只是定义了一个执行流程,只有
result
被使用到的时候才会执行 -
当「👇」的
println
执行时数据处理流程是这样的: -
取出元素 1 -> map 为 2 -> filter 判断 2 是否能被 3 整除
-
取出元素 2 -> map 为 4 -> filter 判断 4 是否能被 3 整除
-
…
惰性指当出现满足条件的第一个元素的时候,Sequence
就不会执行后面的元素遍历了,即跳过了 4
的遍历。
而 List
是没有惰性的特性的:
🏝️
val list = listOf(1, 2, 3, 4)
val result: List = list
.map { i ->
println(“Map $i”)
i * 2
}
.filter { i ->
println(“Filter $i”)
i % 3 == 0
}
👇
println(result.first()) // 👈 只取集合的第一个元素
包括两点:
- 声明之后立即执行
- 数据处理流程如下:
- {1, 2, 3, 4} -> {2, 4, 6, 8}
- 遍历判断是否能被 3 整除
Sequence
这种类似懒加载的实现有下面这些优点:
- 一旦满足遍历退出的条件,就可以省略后续不必要的遍历过程。
- 像
List
这种实现Iterable
接口的集合类,每调用一次函数就会生成一个新的Iterable
,下一个函数再基于新的Iterable
执行,每次函数调用产生的临时Iterable
会导致额外的内存消耗,而Sequence
在整个流程中只有一个。
因此,Sequence
这种数据类型可以在数据量比较大或者数据量未知的时候,作为流式处理的解决方案。
条件控制
相比 Java 的条件控制,Kotlin 中对条件控制进行了许多的优化及改进。
if/else
首先来看下 Java 中的 if/else
写法:
☕️
int max;
if (a > b) {
max = a;
} else {
max = b;
}
在 Kotlin 中,这么写当然也可以,不过,Kotlin 中 if
语句还可以作为一个表达式赋值给变量:
🏝️
👇
val max = if (a > b) a else b
另外,Kotlin 中弃用了三元运算符(条件 ? 然后 : 否则),不过我们可以使用 if/else
来代替它。
上面的 if/else
的分支中是一个变量,其实还可以是一个代码块,代码块的最后一行会作为结果返回:
🏝️
val max = if (a > b) {
println(“max:a”)
a // 👈 返回 a
} else {
println(“max:b”)
b // 👈 返回 b
}
when
在 Java 中,用 switch
语句来判断一个变量与一系列值中某个值是否相等:
☕️
switch (x) {
case 1: {
System.out.println(“1”);
break;
}
case 2: {
System.out.println(“2”);
break;
}
default: {
System.out.println(“default”);
}
}
在 Kotlin 中变成了 when
:
🏝️
👇
when (x) {
👇
1 -> { println(“1”) }
2 -> { println(“2”) }
👇
else -> { println(“else”) }
}
这里与 Java 相比的不同点有:
- 省略了
case
和break
,前者比较好理解,后者的意思是 Kotlin 自动为每个分支加上了break
的功能,防止我们像 Java 那样写错 - Java 中的默认分支使用的是
default
关键字,Kotlin 中使用的是else
与 if/else
一样,when
也可以作为表达式进行使用,分支中最后一行的结果作为返回值。需要注意的是,这时就必须要有 else
分支,使得无论怎样都会有结果返回,除非已经列出了所有情况:
🏝️
val value: Int = when (x) {
1 -> { x + 1 }
2 -> { x * 2 }
else -> { x + 5 }
}
在 Java 中,当多种情况执行同一份代码时,可以这么写:
☕️
switch (x) {
case 1:
case 2: {
System.out.println(“x == 1 or x == 2”);
break;
}
default: {
System.out.println(“default”);
}
}
而 Kotlin 中多种情况执行同一份代码时,可以将多个分支条件放在一起,用 ,
符号隔开,表示这些情况都会执行后面的代码:
🏝️
when (x) {
👇
1, 2 -> print(“x == 1 or x == 2”)
else -> print(“else”)
}
在 when
语句中,我们还可以使用表达式作为分支的判断条件:
- 使用
in
检测是否在一个区间或者集合中:
🏝️
when (x) {
👇
in 1…10 -> print(“x 在区间 1…10 中”)
👇
in listOf(1,2) -> print(“x 在集合中”)
👇 // not in
!in 10…20 -> print(“x 不在区间 10…20 中”)
else -> print(“不在任何区间上”)
}
- 或者使用
is
进行特定类型的检测:
🏝️
val isString = when(x) {
👇
is String -> true
else -> false
}
- 还可以省略
when
后面的参数,每一个分支条件都可以是一个布尔表达式:
🏝️
when {
👇
str1.contains(“a”) -> print(“字符串 str1 包含 a”)
👇
str2.length == 3 -> print(“字符串 str2 的长度为 3”)
}
当分支的判断条件为表达式时,哪一个条件先为 true
就执行哪个分支的代码块。
for
我们知道 Java 对一个集合或数组可以这样遍历:
☕️
int[] array = {1, 2, 3, 4};
for (int item : array) {
…
}
而 Kotlin 中 对数组的遍历是这么写的:
🏝️
val array = intArrayOf(1, 2, 3, 4)
👇
for (item in array) {
…
}
这里与 Java 有几处不同:
- 在 Kotlin 中,表示单个元素的
item
,不用显式的声明类型 - Kotlin 使用的是
in
关键字,表示item
是array
里面的一个元素
另外,Kotlin 的 in
后面的变量可以是任何实现 Iterable
接口的对象。
在 Java 中,我们还可以这么写 for
循环:
☕️
for (int i = 0; i <= 10; i++) {
// 遍历从 0 到 10
}
但 Kotlin 中没有这样的写法,那应该怎样实现一个 0 到 10 的遍历呢?
其实使用上面讲过的区间就可以实现啦,代码如下:
🏝️
for (i in 0…10) {
println(i)
}
try-catch
关于 try-catch
我们都不陌生,在平时开发中难免都会遇到异常需要处理,那么在 Kotlin 中是怎样处理的呢,先来看下 Kotlin 中捕获异常的代码:
🏝️
try {
…
}
catch (e: Exception) {
…
}
finally {
…
}
可以发现 Kotlin 异常处理与 Java 的异常处理基本相同,但也有几个不同点:
- 我们知道在 Java 中,调用一个抛出异常的方法时,我们需要对异常进行处理,否则就会报错:
☕️
public class User{
void sayHi() throws IOException {
}
void test() {
sayHi();
// 👆 IDE 报错,Unhandled exception: java.io.IOException
}
}
但在 Kotlin 中,调用上方 User
类的 sayHi
方法时:
🏝️
val user = User()
user.sayHi() // 👈 正常调用,IDE 不会报错,但运行时会出错
为什么这里不会报错呢?因为 Kotlin 中的异常是不会被检查的,只有在运行时如果 sayHi
抛出异常,才会出错。
- Kotlin 中
try-catch
语句也可以是一个表达式,允许代码块的最后一行作为返回值:
🏝️
👇
val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }
?.
和 ?:
我们在之前的文章中已经讲过 Kotlin 的空安全,其实还有另外一个常用的复合符号可以让你在判空时更加方便,那就是 Elvis 操作符 ?:
。
我们知道空安全调用 ?.
,在对象非空时会执行后面的调用,对象为空时就会返回 null
。如果这时将该表达式赋值给一个不可空的变量:
🏝️
val str: String? = “Hello”
var length: Int = str?.length
// 👆 ,IDE 报错,Type mismatch. Required:Int. Found:Int?
报错的原因就是 str
为 null 时我们没有值可以返回给 length
这时就可以使用 Kotlin 中的 Elvis 操作符 ?:
来兜底:
🏝️
val str: String? = “Hello”
👇
val length: Int = str?.length ?: -1
它的意思是如果左侧表达式 str?.length
结果为空,则返回右侧的值 -1
。
Elvis 操作符还有另外一种常见用法,如下:
总结
其实要轻松掌握很简单,要点就两个:
- 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
- 多练。 (视频优势是互动感强,容易集中注意力)
你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。
以上就是总结的关于在面试的一些总结,希望对大家能有些帮助,除了这些面试中需要注意的问题,当然最重要的就是刷题了,这里放上我之前整理的一份超全的面试专题PDF
还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
这里只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢在关注一下~
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
t = str?.length ?: -1
它的意思是如果左侧表达式 str?.length
结果为空,则返回右侧的值 -1
。
Elvis 操作符还有另外一种常见用法,如下:
总结
其实要轻松掌握很简单,要点就两个:
- 找到一套好的视频资料,紧跟大牛梳理好的知识框架进行学习。
- 多练。 (视频优势是互动感强,容易集中注意力)
你不需要是天才,也不需要具备强悍的天赋,只要做到这两点,短期内成功的概率是非常高的。
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。
以上就是总结的关于在面试的一些总结,希望对大家能有些帮助,除了这些面试中需要注意的问题,当然最重要的就是刷题了,这里放上我之前整理的一份超全的面试专题PDF
还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
[外链图片转存中…(img-VMSIi5mN-1715027686817)]
这里只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢在关注一下~
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!