构造器Constructor
在上一篇文章中,主次构造器有简要介绍,那么在这一章中会继续介绍对主次构造器升级的理解。
主构造器/主构造函数 primary constructor
接收参数的统一入口
主构造器在何处初始化对象?
- 属性的初始赋值:在属性声明的等号右边就可以用这些参数
class User constructor(name:String){
var name=name
}
- init代码块中:在init代码块中可直接使用主构造函数接收到的参数
class User constructor(name:String){
var name=name
init {
println("Name:$name")
}
}
主次构造函数的关系
一旦写了主构造函数,则每个次级构造函数里都要通过 this 关键字调用到这个主构造函数,因为不这么做的话初始化过程就是缺失参数的。
为什么:
- 必须性:创建类的对象时,不管使用哪个构造器,都需要主构造器的参与
- 第一性:在类的初始化过程中,首先执行的就是主构造器
👇
class User constructor(name:String){
👇
var name=name
init {
println("Name:$name")
}
👇
constructor():this(name)
}
主构造函数的便捷写法
- 如果在主构造函数的参数左边写上var或val,则Kotlin会在类中自动创建和参数同名的属性,并用参数值初始化这个属性。
//便捷写法,等价于下面的写法
👇
class User constructor(var name:String){
}
//原写法
class User constructor(name:String){
var name=name
}
- 通常情况下,主构造器中的
constructor关键字可以省略:
👇
class User(name: String) {
var name: String = name
}
但有些场景,constructor 是不可以省略的,例如在主构造器上使用「可见性修饰符」或者「注解」:
class User private constructor(name: String) {
// 👆 主构造器被修饰为私有的,外部就无法调用该构造器
}
主构造函数有没有函数体?
虽然主构造函数没有函数体,但也可以把属性的初始化代码+init代码块看作函数体(如红框所示)。
何时将构造函数写成主构造函数的形式?
- 写成主构造函数的形式会使得类结构更清晰的时候
- 原写法:
- 写成主构造函数:
- 有多个构造函数时,把最基本、最通用的写成主构造函数
- 原写法:
- 写成主构造函数:
总结:类的初始化写法
注意:类的初始化顺序和下面的步骤一样。
- 首先创建一个
User类:
class User {
}
- 添加一个参数为
name与id的主构造器:
class User(name: String, id: String) {
}
- 将主构造器中的
name与id声明为类的属性:
class User(val name: String, val id: String) {
}
- 然后在
init代码块中添加一些初始化逻辑:
class User(val name: String, val id: String) {
init {
...
}
}
- 最后再添加其他次构造器:
class User(val name: String, val id: String) {
init {
...
}
constructor(person: Person) : this(person.name, person.id) {
}
}
函数
函数的简便写法
1. 如果函数体只有一行代码
如果函数体只有一行代码,则可以把大括号去掉,“=”+函数体直接写右边。
//1.简便写法
👇
fun area(width: Int, height: Int): Int = width * height
//1.原写法
fun area(width: Int, height: Int): Int {
return width * height
}
2. 函数的返回类型可以隐藏
这是因为Kotlin 有「类型推断」的特性。(不过,在实际开发中,还是推荐显式地将返回类型写出来,增加代码可读性。)
//2. 隐藏返回类型
fun area(width: Int, height: Int) = width * height
3. 给函数的参数配置默认值
//3.给函数的参数配置默认值
fun say(name:String="Mary")=println("Name:"+name)
...
say() //打印"Name:Mary"
这等价于函数重载。
//3.等价于Java中的函数重载
☕️
public void sayHi(String name) {
System.out.println("Hi " + name);
}
public void sayHi() {
sayHi("world");
}
注意:函数的参数有默认值时,调用函数要注意参数匹配。
4. 局部函数
大函数包小函数,大函数体中可以再定义新函数(如红框所示)
命名参数
前面提到的,函数的参数有默认值时,调用函数要注意参数匹配。「命名参数」就是来解决参数不匹配问题的。
- 用法:在调用函数时,显式地指定参数的名称。
- Kotlin 中的每一个函数参数都可以作为命名参数。
例1:
fun sayHi(name: String = "world", age: Int) {
...
}
👇
sayHi(age = 21)
例2:
fun sayHi(name: String = "world", age: Int, isStudent: Boolean = true, isFat: Boolean = true, isTall: Boolean = true) {
...
}
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) // 👈 这是正确的写法
本地函数(嵌套函数)
嵌套函数中可以访问在它外部的所有变量或常量,例如类中的属性、当前函数中的参数与变量等。
fun login(user: String, password: String, illegalStr: String) {
fun validate(value: String) {
if (value.isEmpty()) {
👇
throw IllegalArgumentException(illegalStr)
}
}
...
}
字符串
字符串模板
- '$' 符号+变量:
val name = "world"
// 👇 用 '$' 符号加参数的方式
println("Hi $name")
字符串模板还支持转义字符,比如使用转义字符 \n 进行换行操作:
val name = "world!\n"
println("Hi $name") // 👈 会多打一个空行
- '$' 符号+表达式:
好处:简化代码的同时增加了字符串的可读性。
// '$'后还可跟表达式,用{}包起来
println("Hi ${name.length}")
其实就跟四则运算的括号一样,提高语法上的优先级,而单个变量的场景可以省略 {}。
原生字符串 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("/"),只不过上方的代码使用的是参数默认值的调用方式
数组与集合
数组与集合的操作函数
首先声明如下 IntArray 和 List:
val intArray = intArrayOf(1, 2, 3)
val strList = listOf("a", "b", "c")
这里是以数组 intArray 为例,集合 strList 也同样有这些操作函数。Kotlin 中还有许多类似的操作函数,这里就不一一列举了。
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 的元素
}
intArray在filter后变成list的原因:
因为 filter 是一个扩展函数,它的定义是这样的:
inline fun IntArray.filter(predicate: (Int) -> Boolean): List<Int>
它接收一个 IntArray 作为接收者,返回一个 List 作为结果。所以 intArray 在 filter 后变成了 list。这样做的好处是可以让 filter 适用于不同类型的数组,而不需要为每种数组类型都定义一个 filter 函数。例如,你也可以对一个 ByteArray 或者一个 CharArray 使用 filter 函数,返回的都是 list。
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"}
val result = intArray.flatMap { i ->
listOf("${i + 1}", "a") // 👈 生成新集合
}
println(result) // [2, a, 3, a, 4, a]
在这个例子里,flatMap 对 intArray 中的每个元素 i 调用了 lambda 表达式,返回了一个 list,即 listOf("${i + 1}", "a")。这些 list 就是 [2, a], [3, a], [4, a]。然后 flatMap 将这些 list 合并为一个新的 list,即 [2, a, 3, a, 4, a]。这就是 flatMap 的结果。
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, ")
}
递减区间
注意:递减没有半开区间的用法。
// 👇 输出:4, 3, 2, 1,
for (i in 4 downTo 1) {
print("$i, ")
}
其中 4 downTo 1 就表示递减的闭区间 [4, 1]。这里的 downTo 以及上面的 step 都叫做「中缀表达式」,之后的文章会做介绍。
Sequence
-
序列
Sequence又被称为「惰性集合操作」。 -
惰性: 只有在需要的时候(比如变量要被用到的时候)才会计算值。
例子:
val sequence = sequenceOf(1, 2, 3, 4)
val result: Sequence<Int> = sequence
.map { i ->
println("Map $i")
i * 2
}
.filter { i ->
println("Filter $i")
i % 3 == 0
}
👇
println(result.first()) // 👈 只取集合的第一个元素
输出结果:
Map 1
Filter 2
Map 2
Filter 4
Map 3
Filter 6
6
在这个例子中,「👇」标注之前的代码运行时不会立即执行,它只是定义了一个执行流程,只有 result 被使用到的时候才会执行。
当「👇」的 println 执行时数据处理流程是这样的:
- 取出元素 1 -> map 为 2 -> filter 判断 2 是否能被 3 整除
- 取出元素 2 -> map 为 4 -> filter 判断 4 是否能被 3 整除
- ...
这里的惰性体现在当出现满足条件的第一个元素的时候,Sequence 就不会执行后面的元素遍历了,即跳过了 4 的遍历。
如果用 list 代替 sequence:
- list 的 map 和 filter 函数会立即执行,而 sequence 的 map 和 filter 函数会延迟执行,只有在请求结果时才会执行。
- list 的 map 和 filter 函数会返回新的 list,而 sequence 的 map 和 filter 函数会返回新的 sequence。
- list 的 map 和 filter 函数会对所有元素进行处理,而 sequence 的 map 和 filter 函数只会对需要的元素进行处理。
如果使用 list
-
声明之后立即执行
-
数据处理流程如下:
- {1, 2, 3, 4} -> {2, 4, 6, 8}
- 遍历判断是否能被 3 整除
-
代码的输出会是:
Map 1
Map 2
Map 3
Map 4
Filter 2
Filter 4
Filter 6
Filter 8
6
Sequence 这种类似懒加载的实现有下面这些优点:
- 一旦满足遍历退出的条件,就可以省略后续不必要的遍历过程。
- 像
List这种实现Iterable接口的集合类,每调用一次函数就会生成一个新的Iterable,下一个函数再基于新的Iterable执行,每次函数调用产生的临时Iterable会导致额外的内存消耗,而Sequence在整个流程中只有一个。
因此,Sequence 这种数据类型可以在数据量比较大或者数据量未知的时候,作为流式处理的解决方案。
条件控制
if/else
- Kotlin的
if/else和Java的使用基本相同,不过还多一个:if语句还可以作为一个表达式赋值给变量:
👇
val max = if (a > b) a else b
- Kotlin 中弃用了三元运算符(条件 ? 然后 : 否则),不过我们可以使用
if/else来代替它。
val max = if (a > b) {
println("max:a")
a // 👈 返回 a
} else {
println("max:b")
b // 👈 返回 b
}
when
相当于Java的switch 语句。
👇
when (x) {
👇
1 -> { println("1") }
2 -> { println("2") }
👇
else -> { println("else") }
}
switch (x) {
case 1: {
System.out.println("1");
break;
}
case 2: {
System.out.println("2");
break;
}
default: {
System.out.println("default");
}
}
这里与 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
遍历的写法
在Kotlin中,数组遍历这么写:
//数组遍历
val array = intArrayOf(1, 2, 3, 4)
👇
for (item in array) {
...
}
Java 对一个集合或数组的遍历:
int[] array = {1, 2, 3, 4};
for (int item : array) {
...
}
这里与 Java 有几处不同:
-
在 Kotlin 中,表示单个元素的
item,不用显式的声明类型 -
Kotlin 使用的是
in关键字,表示item是array里面的一个元素 -
Kotlin 的
in后面的变量可以是任何实现Iterable接口的对象。实现一个 0 到 10 的遍历:
其实使用上面讲过的区间就可以实现啦,代码如下:
for (i in 0..10) {
println(i)
}
捕获异常try-catch
try {
...
}
catch (e: Exception) {
...
}
finally {
...
}
- Kotlin 中的异常是不会被检查的,只有在运行时抛出异常,才会出错。
例:
val user = User()
user.sayHi() // 👈 正常调用,IDE 不会报错,但运行时会出错
在 Java 中,调用一个抛出异常的方法时,我们需要对异常进行处理,否则就会报错:
public class User{
void sayHi() throws IOException {
}
void test() {
sayHi();
// 👆 IDE 报错,Unhandled exception: java.io.IOException
}
}
- Kotlin 中
try-catch语句也可以是一个表达式,允许代码块的最后一行作为返回值:
👇
val a: Int? = try { parseInt(input) }
catch (e: NumberFormatException) { null }
?. 和 ?:
?.:在对象非空时会执行后面的调用,对象为空时就会返回null。- Elvis 操作符
?::如果它左边的表达式不为null,就返回它,否则就返回它右边的表达式。
Elvis 操作符
?:是一个常用的复合符号,它可以让你在判空时更加方便。
例:
val str: String? = "Hello"
var length: Int = str?.length
// 👆 ,IDE 报错,Type mismatch. Required:Int. Found:Int?
报错的原因就是 str 为 null 时我们没有值可以返回给 length,
这时就可以使用 Kotlin 中的 Elvis 操作符 ?: 来兜底,它的意思是如果左侧表达式 str?.length 结果为空,则返回右侧的值 -1。:
val str: String? = "Hello"
👇
val length: Int = str?.length ?: -1
fun validate(user: User) {
val id = user.id ?: return // 👈 验证 user.id 是否为空,为空时 return
}
// 等同于
fun validate(user: User) {
if (user.id == null) {
return
}
val id = user.id
}
相等比较符 == 和 ===
==:可以对基本数据类型以及String等类型进行内容比较,相当于 Java 中的equals===:对引用的内存地址进行比较,相当于 Java 中的==代码示例:
val str1 = "123"
val str2 = "123"
println(str1 == str2) // 👈 内容相等,输出:true
val str1= "字符串"
val str2 = str1
val str3 = str1
print(str2 === str3) // 👈 引用地址相等,输出:true
其实 Kotlin 中的
equals函数是==的操作符重载。
扩展: : 符号
: 符号在多种场合出现过:
- 次构造器依赖于主构造器:
constructor(name: String, id: Int) : this(name) { } - 变量的声明:
var id: Int - 类的继承:
class MainActivity : AppCompatActivity() {} - 接口的实现:
class User : Impl {} - 匿名类的创建:
object: ViewPager.SimpleOnPageChangeListener() {} - 函数的返回值:
fun sum(a: Int, b: Int): Int
可以看出 : 符号在 Kotlin 中非常高频出现,它其实表示了一种依赖关系。