03 Kotlin 里比Java「更方便的」

140 阅读13分钟

构造器Constructor

在上一篇文章中,主次构造器有简要介绍,那么在这一章中会继续介绍对主次构造器升级的理解。

主构造器/主构造函数 primary constructor

接收参数的统一入口

主构造器在何处初始化对象?

  1. 属性的初始赋值:在属性声明的等号右边就可以用这些参数
class User constructor(name:String){
    var name=name
}
  1. 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)
}

主构造函数的便捷写法

  1. 如果在主构造函数的参数左边写上var或val,则Kotlin会在类中自动创建和参数同名的属性,并用参数值初始化这个属性。
//便捷写法,等价于下面的写法
                       👇
class User constructor(var name:String){

}

//原写法
class User constructor(name:String){
    var name=name
}
  1. 通常情况下,主构造器中的 constructor 关键字可以省略:
         👇
class User(name: String) {
    var name: String = name
}

但有些场景,constructor 是不可以省略的,例如在主构造器上使用「可见性修饰符」或者「注解」:

class User private constructor(name: String) {
//           👆 主构造器被修饰为私有的,外部就无法调用该构造器
}

主构造函数有没有函数体?

虽然主构造函数没有函数体,但也可以把属性的初始化代码+init代码块看作函数体(如红框所示)。

image.png

何时将构造函数写成主构造函数的形式?

  1. 写成主构造函数的形式会使得类结构更清晰的时候
  • 原写法:

image.png

  • 写成主构造函数:

image.png

  1. 有多个构造函数时,把最基本、最通用的写成主构造函数
  • 原写法:

image.png

  • 写成主构造函数:

image.png

总结:类的初始化写法

注意:类的初始化顺序和下面的步骤一样。

  • 首先创建一个 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. 局部函数

大函数包小函数,大函数体中可以再定义新函数(如红框所示)

image.png

命名参数

前面提到的,函数的参数有默认值时,调用函数要注意参数匹配。「命名参数」就是来解决参数不匹配问题的。

  • 用法:在调用函数时,显式地指定参数的名称。
  • 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 相比的不同点有:

  1. 省略了 case 和 break,前者比较好理解,后者的意思是 Kotlin 自动为每个分支加上了 break 的功能,防止我们像 Java 那样写错
  2. 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 中非常高频出现,它其实表示了一种依赖关系