(三)Kotlin—高级特性

文章目录

  • (三)Kotlin------高级特性
    • 1.泛型
      • [1.1 泛型使用](#1.1 泛型使用)
      • [1.2 官方高阶扩展函数](#1.2 官方高阶扩展函数)
      • [1.3 协变与逆变](#1.3 协变与逆变)
      • [1.4 泛型界限](#1.4 泛型界限)
      • [1.5 类型擦除](#1.5 类型擦除)
    • 2.数组
      • [2.1 数组的创建](#2.1 数组的创建)
      • [2.2 数组的操作](#2.2 数组的操作)
      • [2.3 可变长参数](#2.3 可变长参数)
      • [2.4 原生类型数组](#2.4 原生类型数组)
      • [2.5 多维数组](#2.5 多维数组)
    • 3.集合类
      • [3.1 List 集合](#3.1 List 集合)
      • [3.2 Set 集合](#3.2 Set 集合)
      • [3.3 Map 集合](#3.3 Map 集合)
      • [3.4 迭代器](#3.4 迭代器)
      • [3.5 集合与数组扩展操作](#3.5 集合与数组扩展操作)
      • [3.6 序列](#3.6 序列)
    • 4.特殊类型
      • [4.1 数据类](#4.1 数据类)
      • [4.2 枚举类](#4.2 枚举类)
      • [4.3 匿名类](#4.3 匿名类)
      • [4.4 单例类](#4.4 单例类)
      • [4.5 伴生对象](#4.5 伴生对象)
      • [4.6 委托模式](#4.6 委托模式)
      • [4.7 密封类](#4.7 密封类)
    • 5.异常
      • [5.1 异常使用](#5.1 异常使用)
      • [5.2 异常处理](#5.2 异常处理)

(三)Kotlin------高级特性

1.泛型

泛型用于在定义类、接口或函数时暂时不指定具体类型,而是在使用时再确定类型。它可以提高代码复用性,并减少不必要的类型转换。

1.1 泛型使用

在类名后使用 <T> 定义泛型参数,T 可以理解为一个"待定类型"。

kotlin 复制代码
class Score<T>(var name: String, var id: String, var value: T)
// 定义类型参数后,T 就可以作为属性、函数参数或返回值的类型使用

fun main() {
    val score = Score<String>("数据结构与算法基础", "EP074512", "优秀")
    // 使用时通过 <String> 明确 value 的类型

    val value: String = score.value
    println(value)
}

如果 Kotlin 能根据传入的参数推断类型,泛型类型可以省略:

kotlin 复制代码
fun main() {
    val score = Score("数据结构与算法基础", "EP074512", 95)
    // Kotlin 会自动推断 T 为 Int

    val value: Int = score.value
    println(value)
}

泛型参数可以有多个:

kotlin 复制代码
class Test<K, V>(val key: K, val value: V)

fun main() {
    val mapItem = Test<String, Int>("age", 18)
    println(mapItem.key)
    println(mapItem.value)
}

接口、抽象类也可以使用泛型:

kotlin 复制代码
interface Repository<T> {
    fun getById(id: String): T
}

子类继承泛型父类时,可以直接指定具体类型,也可以继续保留泛型:

kotlin 复制代码
abstract class A<T> {
    abstract fun test(): T
}

class B : A<String>() {
    // 父类泛型明确为 String 后,返回值也必须是 String
    override fun test(): String = "Hello World"
}

abstract class C<D> : A<D>() {
    // 子类继续保留泛型 D
    abstract override fun test(): D
}

fun main() {
    val b = B()
    println(b.test())
}

函数也可以定义泛型,写在函数名之前:

kotlin 复制代码
fun <T> test(t: T): T = t

fun main() {
    val value: String = test("Hello World")
    println(value)
}

泛型也可以和函数类型一起使用:

kotlin 复制代码
fun <T> test(func: (Int) -> T): T {
    // func 接收 Int,返回值类型由 T 决定
    return func(10)
}

fun <T> test2(value: T, func: T.() -> Unit) {
    // func 是 T 类型的扩展函数
    value.func()
}

1.2 官方高阶扩展函数

Kotlin 标准库中有很多常用的高阶扩展函数,例如 applyletalsoruntakeIftakeUnlesswith。它们本质上都是对 Lambda 和泛型的封装。

apply:用于配置对象,最后返回对象本身。常用于对象初始化。

kotlin 复制代码
public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()      // 执行传入的 Lambda
    return this  // 返回当前对象本身
}

原来可能这样写:

kotlin 复制代码
class Student(var name: String, var age: Int) {
    fun hello() {
        println("大家好,我叫$name")
    }
}

fun main() {
    val student: Student = Student("小明", 18)
    student.name = "大明"
    student.hello()
}

使用 apply 后可以写得更紧凑:

kotlin 复制代码
fun main() {
    Student("小明", 18).apply {
        // apply 中的 this 指向当前 Student 对象
        this.name = "大明"
    }.hello()
}
kotlin 复制代码
Student("小明", 18).apply {
    // 这里编写对 Student 对象的配置逻辑
}

几个比较常用的:

let:把当前对象作为 it 传入 Lambda,并返回 Lambda 的结果。适合做空值判断后的继续处理。

kotlin 复制代码
public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}
kotlin 复制代码
fun main() {
    val name = "Kotlin"
    val length = name.let {
        // it 指向 name
        it.length
    }
    println(length)
}

also:把当前对象作为 it 传入 Lambda,最后返回对象本身。常用于插入日志、调试等附加操作。(跟 apply 功能很像)

kotlin 复制代码
public inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}
kotlin 复制代码
fun main() {
    val student = Student("小明", 18).also {
        // also 中的 it 指向当前 Student 对象
        println("创建了学生:${it.name}")
    }
    student.hello()
}

run:在当前对象作用域内执行 Lambda,并返回 Lambda 的结果。它和 let 类似,但对象通过 this 使用。

kotlin 复制代码
public inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}
kotlin 复制代码
fun main() {
    val student = Student("小明", 18)
    val text = student.run {
        // run 中可以直接访问 name 和 age
        "姓名:$name,年龄:$age"
    }
    println(text)
}

takeIf:如果条件成立,返回对象本身;否则返回 null

kotlin 复制代码
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
    return if (predicate(this)) this else null
}

public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
    return if (!predicate(this)) this else null // 跟上面相反
}
kotlin 复制代码
fun main() {
    val str = "Hello World"
    // 字符串长度大于 7 时重复一次,否则返回原字符串
    val myStr = str.takeIf { it.length > 7 }?.let { it + it } ?: str
    println(myStr)
}

with:不是扩展函数,需要手动传入对象,然后在 Lambda 中使用该对象。

kotlin 复制代码
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}
kotlin 复制代码
fun main() {
    val str = "Hello World"
    val len = with(str) {
        // this 指向 str
        this.length
    }
    println(len)
}

简单记忆:

函数 Lambda 中对象名称 返回值
apply this 对象本身
also it 对象本身
let it Lambda 结果
run this Lambda 结果
with this Lambda 结果
takeIf it 满足条件返回对象,否则 null
takeUnless it 不满足条件返回对象,否则 null

1.3 协变与逆变

Java 中如果 IntegerNumber 的子类,那么 Integer 可以赋值给 Number。但对于泛型,List<Integer> 默认并不是 List<Number> 的子类。Kotlin 也是类似的,默认情况下泛型是不变的。

假设 IntNumber 的子类,泛型类型 Test<T> 可能有三种关系:

  • 协变(Covariance):如果 IntNumber 的子类,那么 Test<Int> 也可以看作 Test<Number> 的子类。
  • 逆变(Contravariance):如果 IntNumber 的子类,那么 Test<Number> 可以看作 Test<Int> 的子类。
  • 不变(Invariant):Test<Int>Test<Number> 没有继承关系,这是默认情况。

Kotlin 提供了两个关键字:

  • out:表示协变,主要用于"只生产数据"的场景,可以实现子类到父类的转换。
  • in:表示逆变,主要用于"只消费数据"的场景,可以实现父类到子类的转换。

协变示例:

kotlin 复制代码
class Test<T>(val data: T)

fun main() {
    val test1: Test<Int> = Test(888)
    // out Number 表示可以接收 Number 及其子类的 Test 对象
    val test2: Test<out Number> = test1

    val data: Number = test2.data
    println(data)
}

逆变示例:

kotlin 复制代码
class Consumer<T>(private var data: T) {
    fun set(value: T) {
        data = value
    }
}

fun main() {
    val test1: Consumer<Any> = Consumer(888)
    // in Number 表示可以接收 Number 及其父类的 Consumer 对象
    val test2: Consumer<in Number> = test1

    test2.set(10)
}

out 修饰后,读取时可以当作上界类型使用:

kotlin 复制代码
class Test<T>(val data: T)

fun main() {
    val test: Test<out Number> = Test(888)
    val data: Number = test.data
    println(data)
}

in 修饰后,读取时具体类型不确定,只能当作 Any? 使用:

kotlin 复制代码
class Test<T>(var data: T)

fun main() {
    val test: Test<in Number> = Test<Any>(888)
    val data: Any? = test.data
    println(data)
}

协变如果允许随意写入,会产生类型安全问题:

kotlin 复制代码
open class A
class B: A()
class C: A()

fun main() {
    val test1: Test<B> = Test(B())  //这里存放的都是B类型的数据
    val test2: Test<out A> = test1  //此时test2与test1是同一个对象,但是test2是out A
    test2.data = C()  //由于C是A的子类,按照正常情况来说可以直接用(但实际上这句会报错)
  	val data: B = test1.data  //这下搞笑了,拿到的类型应该是C,结果接收的类型是B
}

因此可以这样理解:

  • out:使用 out 修饰的泛型不能用作函数的参数,对应类型的成员变量 setter 也会被限制,适合作为生产者。
  • in:使用 in 修饰的泛型不能用作函数的返回值,对应类型的成员变量 getter 也会被限制,适合作为消费者。

星号投影 * 表示暂时不关心具体泛型类型,可以接收任意类型的泛型对象:

kotlin 复制代码
class Test<T>(val data: T)

fun main() {
    var test: Test<*> = Test(888)
    test = Test("Hello")

    // 读取出来的类型只能确定为 Any?
    val data: Any? = test.data
    println(data)
}

1.4 泛型界限

泛型界限用于限制泛型参数的范围。例如成绩只允许是数字类型,就可以把泛型上界限制为 Number

kotlin 复制代码
// T 必须是 Number 或 Number 的子类
class Score<T : Number>(private val name: String, private val id: String, val value: T)

fun main() {
    val score1 = Score("数学", "EP001", 95)
    val score2 = Score("英语", "EP002", 88.5)

    println(score1.value)
    println(score2.value)
}

如果需要多个上界,可以使用 where

kotlin 复制代码
class Score<T>(private val name: String, private val id: String, val value: T)
        where T : Comparable<T>, T : Number
// T 必须同时是 Number 的子类,并且实现 Comparable<T>

fun main() {
    val score: Score<Int> = Score("数据结构与算法", "EP710214", 96)
    println(score.value)
}

也可以在定义类或接口时直接声明协变或逆变,这叫声明处型变。

kotlin 复制代码
interface Test<out T> {
    fun test(): T
    // out T 适合作为返回值
}

interface Consumer<in T> {
    fun test(t: T)
    // in T 适合作为参数
}

这样在使用时就可以自动适配类型:

kotlin 复制代码
interface Test<out T> {
    fun test(): T
}

fun test(test: Test<Int>) {
    val a: Test<Number> = test
    // 因为 Test 使用了 out T,所以 Test<Int> 可以赋值给 Test<Number>
}

* 类型投影在不同情况下的默认含义不同:

  • 对于 Foo<out T : TUpper>Foo<*> 等价于 Foo<out TUpper>,读取时可以当作上界类型。

    kotlin 复制代码
    class Test<out T : Number>(val data: T)
    // 因为 T 使用 out,只能作为生产者,所以属性使用 val
    
    fun main() {
        val test: Test<*> = Test(10)
        val data: Number = test.data
        println(data)
    }
  • 对于 Foo<in T>Foo<*> 等价于 Foo<in Nothing>,无法安全传入任何具体值。

    kotlin 复制代码
    class Test<in T> {
        fun set(t: T) { }
    }
    
    fun main() {
        val test: Test<*> = Test<Int>()
        // 编译错误:参数类型相当于 Nothing,不能传入 10
        // test.set(10)
    }
  • 对于 Foo<T : TUpper>,读取时 Foo<*> 可看作 Foo<out TUpper>,写入时可看作 Foo<in Nothing>

    kotlin 复制代码
    class Test<T : Number>(var data: T)
    
    fun main() {
        val test: Test<*> = Test(10)
        val data: Number = test.data
        println(data)
    
        // 编译错误:星号投影后,setter 被限制
        // test.data = 10
    }

如果一个泛型类有多个类型参数,每个类型参数都可以单独使用 *

  • Function<*, String> 等价于 Function<in Nothing, String>
  • Function<Int, *> 等价于 Function<Int, out Any?>
  • Function<*, *> 等价于 Function<in Nothing, out Any?>

1.5 类型擦除

泛型主要在编译阶段进行类型检查。代码编译后,泛型的具体类型信息通常不会继续保留,这就是类型擦除。它和 Java 泛型的基本思想类似。

例如下面的泛型类:

kotlin 复制代码
class Test<T>(private var data: T) {
    fun test(t: T): T {
        val tmp = data
        data = t
        return tmp
    }
}

编译后可以简单理解为类似下面的形式:

kotlin 复制代码
class Test(private var data: Any?) { // 最后还是全部变成 Any? 类型了
    fun test(t: Any?): Any? {
        val tmp = data
        data = t
        return tmp
    }
}

如果泛型存在上界,擦除后会变成上界类型:

kotlin 复制代码
class Test<T : Number>(private var data: T)

// 可以理解为擦除后接近下面的形式
class Test(private var data: Number)

内联函数的泛型稍有不同。因为 inline 函数会在编译时把函数体复制到调用处,所以在某些场景下可以保留更具体的类型信息。

kotlin 复制代码
inline fun <T> test(value: T): T {
    val value2: T = value
    return value2
}

fun main() {
    val data: String = test("Hello World!")
    println(data)
}

编译后可以理解为调用处直接展开:

kotlin 复制代码
fun main() {
    val value: String = "Hello World!"
    val value2: String = value
    val data: String = value2
    println(data)
}

如果希望在函数体内部判断泛型类型,可以使用 inline + reifiedreified 表示具化类型参数,只能用于内联函数。

kotlin 复制代码
inline fun <reified T> isType(value: Any): Boolean {
    // 因为 T 被 reified 修饰,所以这里可以使用 is T
    return value is T
}

fun main() {
    println(isType<String>("666"))
    println(isType<Int>("666"))
}

普通泛型函数不能直接这样写:

kotlin 复制代码
fun <T> isType(value: Any): Boolean {
    // 编译错误:普通泛型 T 在运行时已经被擦除
    // return value is T
    return false
}

2.数组

2.1 数组的创建

Kotlin 中创建数组有两种方式:

  • 工具函数:arrayOf()arrayOfNulls()emptyArray()

  • Array 构造函数创建

kotlin 复制代码
/**
 * size: 数组大小
 * init: 初始化函数,循环调用size次,下标作为参数,返回值填充对应位置
 */
public inline constructor(size: Int, init: (Int) -> T)
kotlin 复制代码
val array: Array<Int> = arrayOf(7, 3, 9, 1, 6)  // 直接传入元素
array.size = 10   // 编译错误,数组长度不可修改
val arr: Array<String> = array  // 编译错误,类型不匹配
println(array[0])   // 使用[]访问指定下标元素
val array: Array<Int> = emptyArray<Int>()   // 创建空数组
val array: Array<Int?> = arrayOfNulls(10)   // 创建可空元素数组
kotlin 复制代码
fun main() {
    val array: Array<String> = Array(5) { "我是元素$it" }   // 自定义填充
    for (s in array) {
        println(s)
    }
}
val array: Array<Double> = Array(5) { 1.5 }  // 全部填充相同值
val array: Array<Int> = Array(10) { it * it }   // 按规则生成:0, 1, 4, 9, 16...

遍历方式:

kotlin 复制代码
fun main() {
    val array: Array<Int> = arrayOf(7, 3, 9, 1, 6)
    for (i in 0..<array.size) {   // 按下标遍历
        println(array[i])
    }
}

fun main() {
    val array: Array<Int> = arrayOf(7, 3, 9, 1, 6)
    for (element in array) {      // 直接遍历元素
        println(element)
    }
}

val array: Array<Int> = arrayOf(7, 3, 9, 1, 6)
for (i in array.indices) {       // indices返回有效索引范围
    println(array[i])
}

for ((index, item) in array.withIndex()) {  // 同时获取索引和元素
    println("元素$item,位置: $index")
}

array.forEach { println(it) }   // forEach遍历元素

array.forEachIndexed { index, item ->   // 带索引的forEach
    println("元素$item,位置: $index")
}

打印输出:

kotlin 复制代码
val array: Array<Int> = arrayOf(7, 3, 9, 1, 6)
println(array.joinToString())  // 默认逗号分隔:7, 3, 9, 1, 6
println(array.joinToString(" - ", "> ", " <"))  // 自定义分隔符、前后缀: > 7 - 3 - 9 - 1 - 6 <
println(array.joinToString(limit = 1, truncated = "..."))  // 限制数量: 7, ...
println(array.joinToString() { (it * it).toString() })   // 自定义元素转换

2.2 数组的操作

内容比较:

kotlin 复制代码
fun main() {
    val array1: Array<Int> = arrayOf(1, 2, 3, 4, 5)
    val array2: Array<Int> = arrayOf(1, 2, 3, 4, 5)
    println(array1 == array2)   // == 不比较内容
    println(array1.contentEquals(array2))   // 使用contentEquals比较内容
}

数组拷贝:

kotlin 复制代码
fun main() {
    val array1: Array<Int> = arrayOf(1, 2, 3, 4, 5)
    val array2: Array<Int> = array1.copyOf()   // 完整拷贝
    println(array2 === array1)  // false,不同对象
}

val array2: Array<Int?> = array1.copyOf(10)
// 指定拷贝长度,超出部分填充null

val array2: Array<Int> = array1.copyOfRange(1, 3)  // 拷贝指定范围:[1,3)

分割与拼接:

kotlin 复制代码
val array1 = arrayOf(1, 2, 3, 4, 5)
val array2 = array1.sliceArray(1..3)   // 截取指定范围

val array1 = arrayOf(1, 2, 3, 4, 5)
val array2 = arrayOf(6, 7, 8, 9, 10)
val array3 = array1 + array2   // 拼接数组

元素查找:

kotlin 复制代码
val array = arrayOf(13, 16, 27, 32, 38)
println(array.contains(13))   // 判断是否包含
println(13 in array)          // in运算符,等价于contains
println(array.indexOf(26))    // 查找元素下标
println(array.binarySearch(16))    // 二分查找(要求数组有序,效率更高)

快速判断与获取:

kotlin 复制代码
val array = arrayOf(1, 2, 3, 4, 5)
println(array.any())   // 判断是否非空
println(array.first())   // 获取首个元素
println(array.last())    // 获取最后一个元素

数组翻转:

kotlin 复制代码
val array1: Array<Int> = arrayOf(1, 2, 3, 4, 5)
val array2: Array<Int> = array1.reversedArray()   // 翻转并生成新数组

val array1: Array<Int> = arrayOf(1, 2, 3, 4, 5)
array1.reverse()   // 原地翻转
array1.reverse(1, 3)   // 翻转指定范围

打乱与排序:

kotlin 复制代码
val array1: Array<Int> = arrayOf(1, 2, 3, 4, 5)
array1.shuffle()  // 随机打乱顺序

array1.sort()   // 排序(可自定义规则)
array1.sort(1, 3)   // 仅排序指定范围
array1.sortDescending()   // 降序排序

填充数组:

kotlin 复制代码
val array1 = arrayOf(1, 2, 3, 4, 5)
array1.fill(10)   // 全部填充为指定值

2.3 可变长参数

使用 vararg 关键字声明可变长参数:

kotlin 复制代码
fun test(vararg strings: String) {
	// 可变长参数在函数内可当作Array使用
}
fun main() {
    test("AAA", "BBB", "CCC")   // 可传入任意数量同类型参数
}

可变长参数在参数列表中只能有一个:

kotlin 复制代码
fun test(vararg strings: String, a: Int) { ... }   // 编译通过
fun test(a: Int, vararg strings: String) { ... }   // 编译通过
fun test(vararg a: Int, vararg strings: String) { ... }    // 编译错误,多个可变长参数

可变长参数本质是数组,但不能直接传入数组:

kotlin 复制代码
fun main() {
    val array = arrayOf("AAA", "BBB", "CCC")
    test(array)   // 编译错误,类型不匹配
}

使用扩展运算符(*)将数组展开为单个参数:

kotlin 复制代码
fun main() {
    val array = arrayOf("AAA", "BBB", "CCC")
    test(*array)   // 展开数组
}
test("111", *array, "DDD", "EEE")   // 可与普通参数混合使用

2.4 原生类型数组

为避免装箱开销,Kotlin 为基本类型提供专用数组:

原生类型数组 对应 Java
BooleanArray boolean[]
ByteArray byte[]
CharArray char[]
DoubleArray double[]
FloatArray float[]
IntArray int[]
LongArray long[]
ShortArray short[]
kotlin 复制代码
fun main() {
    // 使用 arrayOf 的变种 intArrayOf 快速生成 IntArray(其他同理)
    val array: IntArray = intArrayOf(7, 3, 9, 1, 6)
    array.forEach { println(it) }
}

2.5 多维数组

以二维数组为例,二维数组创建:

kotlin 复制代码
val arr: Array<IntArray> = arrayOf(intArrayOf(1, 2), intArrayOf(3, 4), intArrayOf(5, 6))
val arr: Array<Array<Int>> = Array(9) { Array(4) { 0 } }  // 9×4矩阵,初始化为0

访问多维数组元素:

kotlin 复制代码
val arr: Array<IntArray> = arrayOf(intArrayOf(1, 2), intArrayOf(3, 4), intArrayOf(5, 6))
val item: Int = arr[0][0]  // 连续使用[]访问

多维数组内容比较:

kotlin 复制代码
fun main() {
    val arr1: Array<IntArray> = arrayOf(intArrayOf(1, 2), intArrayOf(3, 4))
    val arr2: Array<IntArray> = arrayOf(intArrayOf(1, 2), intArrayOf(3, 4))
    println(arr1.contentEquals(arr2))   // false 只比较第一层
    println(arr1.contentDeepEquals(arr2))  // true 深度比较所有层级
}

多维数组各子数组长度可不同,类型也可混合:

kotlin 复制代码
val arr: Array<IntArray> = arrayOf(intArrayOf(1, 3, 4, 5), intArrayOf(2, 9))
val arr: Array<Array<out Any>> = arrayOf(arrayOf(1, 3), arrayOf("AAA", "BBB"))

3.集合类

Kotlin 集合分为三大类:

  • List:有序集合,通过索引访问,可包含重复元素

  • Set:无重复元素集合,通常不维护顺序

  • Map:键值对映射,键唯一,值可重复

所有集合(Map 除外)都继承自 Collection 接口:

kotlin 复制代码
public interface Collection<out E> : Iterable<E> {
    public val size: Int                    // 集合大小
    public fun isEmpty(): Boolean           // 是否为空
    public operator fun contains(element: E): Boolean  // 是否包含元素
    override fun iterator(): Iterator<E>    // 生成迭代器
    public fun containsAll(elements: Collection<E>): Boolean // 是否包含另一个集合中所有的内容
}

3.1 List 集合

创建可变 List:

kotlin 复制代码
fun main() {
    val list: MutableList<Int> = mutableListOf(1, 2, 3, 4)
    list[0] = 10   // 修改元素
    println(list[1])   // 访问元素
    println(list)    // 输出: [10, 2, 3, 4]
}

添加元素:

kotlin 复制代码
val list = mutableListOf(1, 2, 3, 4)
list.add(5)   // 末尾添加
list.add(2, 666)   // 指定位置插入

删除元素:

kotlin 复制代码
val list = mutableListOf("AAA", "BBB", "CCC", "DDD")
list.removeAt(2)  // 按索引删除
list.remove("DDD")    // 按元素删除

创建只读 List:

kotlin 复制代码
val list: List<String> = listOf("AAA", "BBB", "CCC", "DDD")  // 不可修改
list[0] = "XXX"   // ❌ 编译错误

遍历 List:

kotlin 复制代码
val list: List<String> = listOf("AAA", "BBB", "CCC", "DDD")
for (s in list) {
    println(s)
}
for ((index, item) in list.withIndex()) {
    println("元素$item, 下标: $index")
}

3.2 Set 集合

创建 Set:

kotlin 复制代码
fun main() {
    val set: Set<String> = mutableSetOf("AAA", "BBB", "BBB", "CCC")
    println(set)   // 自动去重: [AAA, BBB, CCC]
}

Set 不支持按索引访问,只能添加 / 删除:

kotlin 复制代码
val set: MutableSet<String> = mutableSetOf("AAA", "DDD", "CCC")
set.add("BBB")
println(set.elementAt(1))   // 按迭代顺序获取第N个元素

集合运算:

kotlin 复制代码
val set1 = mutableSetOf("AAA", "DDD", "CCC")
val set2 = mutableSetOf("BBB", "CCC", "EEE")
println(set1 union set2)        // 并集: [AAA, DDD, CCC, BBB, EEE]
println(set1 intersect set2)    // 交集: [CCC]
println(set1 subtract set2)     // 差集: [AAA, DDD]

各种 Set 实现:

kotlin 复制代码
val set = hashSetOf("AAA", "DDD", "BBB")      // 哈希实现,无序
val set = linkedSetOf("AAA", "DDD", "BBB")    // 链表实现,保持插入顺序
val set = setOf("AAA", "DDD", "BBB")         // 只读Set
val set = sortedSetOf("AAA", "DDD", "BBB")    // 自动排序
val set = setOfNotNull("AAA", "DDD", "BBB", null)   // 自动过滤Null元素
val hashSet = HashSet<String>()  // 不重复且无序的Set集合
val linkedHashSet = LinkedHashSet<String>()   // 跟mutableSetOf一样得到一个不重复且有序的Set集合 

3.3 Map 集合

使用 to 中缀函数创建键值对:

kotlin 复制代码
val pair: Pair<Int, String> = 10001 to "小明"

创建 Map:

kotlin 复制代码
val map: MutableMap<Int, Student> = mutableMapOf(
    10001 to Student("小明", 18),
    10002 to Student("小红", 17),
    10003 to Student("小刚", 19)
)

访问 Map:

kotlin 复制代码
val student: Student? = map[10001]   // 使用[]运算符通过Key查找Value,key不存在返回null
val keys: MutableSet<Int> = map.keys   // 获取所有key
val values: Collection<Student> = map.values    // 获取所有value

遍历 Map:

kotlin 复制代码
map.forEach { (k, v) -> println("键: $k, 值 $v") }

for ((key, value) in map) {
    println("键: $key, 值 $value")
}

for (entry in map) {   // 这里得到的是一个entry
    entry.key
    entry.value
}

添加键值对:

kotlin 复制代码
map[10004] = Student("小强", 18)   // 直接赋值
map.put(10004, Student("小强", 18))  // 使用函数
val newMap = map + (10004 to Student("小强", 18))   // 添加新的键值对并生成一个新的Map
map += (10004 to Student("小强", 18))  // 直接添加键值对到当前Map里面
map += mapOf(10004 to Student("小强", 18))  // 或者添加其他Map到此Map中
map.putAll(mapOf(10004 to Student("小强", 18)))   // 使用函数
map.putAll(setOf(10004 to Student("小强", 18)))   // 键值对集合也可以

key 重复时会覆盖,返回被覆盖的值:

kotlin 复制代码
val old = map.put(10003, Student("小强", 18))   // put的返回值如果没有覆盖元素返回null,否则返回被覆盖的元素

删除键值对:

kotlin 复制代码
val old = map.remove(10001)
map -= 10001
map -= listOf(10001, 10002)   // 批量删除

val map: MutableMap<Int, Student> = mutableMapOf(
    10001 to Student("小明", 18),
    10002 to Student("小红", 17),
    10003 to Student("小明", 18)   // 这里存在两个一样的元素
)
map.values.remove(Student("小明", 18))   // 通过这种方式移除也只会移除按顺序下来的第一个
println(map)  // {10002=小红 (17), 10003=小明 (18)}

安全获取值:

kotlin 复制代码
var student: Student = map.getOrDefault(10004, Student("小强", 16))  // 不存在返回默认值
var student: Student = map.getOrElse(10004){ Student("小强", 16) }  // 函数式默认值
var student: Student = map.getOrPut(10004){ Student("小强", 16) }   // 不存在则添加并返回

各种 Map 实现:

kotlin 复制代码
val map1 = mapOf(1 to "AAA")              // 只读Map
val map2 = hashMapOf(1 to "AAA")          // 哈希实现,无序
val map3 = linkedMapOf(1 to "AAA")        // 链表实现,保持插入顺序
val map4 = sortedMapOf(1 to "AAA")        // 自动按key排序
val map5 = emptyMap<Int, String>()   // 空Map
val hashMap = HashMap<Int, String>()   // 采用构造函数创建的HashMap,不保存Key顺序的Map,同map2
val linkedHashSet = LinkedHashMap<Int, String>()   // 采用构造函数创建的LinkedHashMap,保存Key顺序的Map,同map3

3.4 迭代器

所有集合和数组都可生成迭代器,用于统一遍历:

kotlin 复制代码
val list = listOf("AAA", "BBB", "CCC")
val iterator: Iterator<String> = list.iterator()

迭代器接口定义:

kotlin 复制代码
public interface Iterator<out T> {
    public operator fun next(): T          // 获取下一个元素
    public operator fun hasNext(): Boolean  // 如果还有元素没有遍历,那么返回true否则返回false
}

使用迭代器遍历:

kotlin 复制代码
val iterator: Iterator<String> = list.iterator()
while (iterator.hasNext()) {
    println(iterator.next())
}

迭代器统一了所有集合的遍历方式:

kotlin 复制代码
fun <T> forEach(iterator: Iterator<T>) {
    while (iterator.hasNext()) {
        println(iterator.next())
    }
}
forEach(listOf("A", "B", "C").iterator())
forEach(setOf("A", "B", "C").iterator())
forEach(arrayOf("A", "B", "C").iterator())
forEach(mapOf(1 to "AAA", 2 to "BBB",3 to "CCC").iterator())

迭代器是一次性的,重新遍历需生成新迭代器。只要是重写了 operator fun iterator() 函数的类型,都可以使用 for..in 这样的语法去进行遍历,实际上,所有使用 for..in 语法的操作,最后都会被编译为使用迭代器的 while 操作。

List 专属迭代器:

kotlin 复制代码
val iterator: ListIterator<String> = list.listIterator()
println(iterator.next())        // 正向迭代
println(iterator.nextIndex())   // 下一个元素索引
println(iterator.previous())    // 反向迭代

可变迭代器(支持遍历时删除):

在 JVM 环境下,Kotlin 默认不支持在迭代时修改集合里面的内容,无论是插入新的元素还是移除元素,都会 触发并发修改异常

为了解决这个问题,Kotlin 为所有的 MutableCollection(所有非只读集合类)提供了一个特殊的用于生成 MutableIterator 的函数,只要使用的不是只读的集合类,都可以获得这个特殊的迭代器,它支持在遍历时对元素进行删除:

kotlin 复制代码
val list = mutableListOf("AAA", "BBB", "CCC")
val iterator: MutableIterator<String> = list.iterator()
while (iterator.hasNext()) {
    iterator.next()
    iterator.remove()   // 删除当前元素
}

3.5 集合与数组扩展操作

数组转集合:

kotlin 复制代码
val array = arrayOf("AAA", "BBB", "CCC")
val list: List<String> = array.toList()
val list: MutableList<String> = array.toMutableList()
val set: Set<String> = array.toSet()
val set: MutableSet<String> = array.toMutableSet()

映射(map):

kotlin 复制代码
val list = listOf("AAA", "BB", "CCCCC")
val lenList: List<Int> = list.map { it.length }   // 元素转换
val mapList: List<String> = list.mapIndexed { index, it -> "$index.$it" }  // 带索引

对于 Map 类型,还可以单独对所有 Key 或是 Value 进行操作:

kotlin 复制代码
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3)
println(numbersMap.mapKeys { it.key.uppercase() })   //对所有的Key进行Map操作
println(numbersMap.mapValues { it.value + it.key.length })

压缩(zip)与解压(unzip):

kotlin 复制代码
val list1 = listOf(1, 2, 3)
val list2 = listOf("AAA", "BBB", "CCC")
val pairs: List<Pair<Int, String>> = list1.zip(list2)  // 按索引配对

val list = listOf(1 to "AAA", 2 to "BBB", 3 to "CCC")
val unzipList: Pair<List<Int>, List<String>> = list.unzip()  // 拆分Pair

普通集合转 Map:

kotlin 复制代码
val list = listOf("AAA", "BBB", "CCC")
val map1: Map<String, Int> = list.associateWith { it.length }  // 元素为key
val map2: Map<Int, String> = list.associateBy { it.length }    // 元素为value
val associate: Map<String, Int> = list.associate { it to it.length }  //返回一个Pair

扁平化(flatten/flatMap):

kotlin 复制代码
val list = listOf(listOf("AAA", "BBB"), listOf("CCC", "DDD"))
val flatten: List<String> = list.flatten()   // 嵌套集合展平 [AAA, BBB, CCC, DDD]

val list = listOf(Container(listOf("A", "B")), Container(listOf("C", "D")))
val flatList: List<String> = list.flatMap { it.list }  // 先映射再展平,通过Lambda将每一个Container映射为List

过滤(filter):

kotlin 复制代码
val list = listOf("AAA", "BB", "CCC")
val filterList: List<String> = list.filter { it.length > 2 }
val filterList: List<String> = list.filterNotNull()    // 过滤null
val filterList: List<String> = list.filterIsInstance<String>()  // 过滤指定类型

// Map filter操作
val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
val filteredMap = numbersMap.filter { (key, value) -> key.endsWith("1") && value > 10}
println(filteredMap)

分区(partition):

kotlin 复制代码
val list = listOf("AA", "BBB", "CC", "DDD")
//分区操作得到一个匹配列表和一个不匹配列表
val (match, mismatch) = list.partition { it.length > 2 }
println("匹配的列表: $match")
println("不匹配的列表: $mismatch")

条件判断:

kotlin 复制代码
val list = listOf("AA", "BBB", "CC", "DDD")
list.any { it.length > 2 }  // 判断集合中是否至少存在一个元素满足条件
list.none { it.length > 2 }  // 判断集合中是否不存在任何一个元素满足条件
list.all { it.length > 2 }   // 判断集合中是否每一个元素都满足条件

分组(groupBy):

kotlin 复制代码
val list = listOf("AA", "BBB", "CC", "DDD")
println(list.groupBy { it.length })  // 按照字符串的长度进行分组 {2=[AA, CC], 3=[BBB, DDD]}

切片与裁剪:

kotlin 复制代码
list.slice(1..2)    // 截取指定范围
list.take(2)        // 取前2个
list.takeLast(2)    // 取后2个
list.drop(2)        // 跳过前2个
list.dropLast(2)    // 跳过后2个
list.takeWhile { it.length > 2 } // 从第一个元素开始,依次按照给定的函数进行判断,如果判断成功则添加到返回列表中,直到遇到一个不满足条件的就返回

重新分块:

kotlin 复制代码
val list = listOf("AA", "BBB", "CC", "DDD")
//使用chunked进行分块,这里2个元素为一组进行分块,得到一个嵌套的集合
println(list.chunked(2))   //结果 [[AA, BBB], [CC, DDD]]

更多集合特性请参考官网:https://kotlinlang.org/docs/collections-overview.html

3.6 序列

序列(Sequence)只在需要时才生成和处理元素,处理大量数据时性能更好。

kotlin 复制代码
public interface Sequence<out T> {
    public operator fun iterator(): Iterator<T>
}

创建序列:

kotlin 复制代码
val sequence: Sequence<Int> = generateSequence {
    println("生成一个新的元素")
    10 // 返回值就是生成的元素
}

普通集合与序列对比:

  • 普通集合(立即求值)
kotlin 复制代码
fun main() {
    val list = listOf("AA", "BBB", "CCC", "DD", "EEE", "FF", "GGG", "HH")
    // 以下操作用于获取前两个长度大于2的字符串,并进行小写转换操作
    val result = list.filter {
        println("进行过滤操作: $it")
        it.length > 2
    }.map {
        println("进行小写转换操作")
        it.lowercase()
    }.take(2)

}
  • 序列(惰性求值)
kotlin 复制代码
fun main() {
    val list = listOf("AA", "BBB", "CCC", "DD", "EEE", "FF", "GGG", "HH")
    // 以下操作用于获取前两个长度大于2的字符串,并进行小写转换操作
    val result = list.asSequence().filter {
        println("进行过滤操作: $it")
        it.length > 2
    }.map {
        println("进行小写转换操作")
        it.lowercase()
    }.take(2)
    println(result.joinToString())
}

4.特殊类型

4.1 数据类

数据类既可以像普通类一样使用,也会由编译器自动生成一些常用成员函数,例如输出对象信息、比较实例、解构以及复制实例等。

声明一个数据类:

kotlin 复制代码
// 在class前面添加data关键字表示为一个数据类
data class User(val name: String, val age: Int)

数据类必须满足以下要求:

  • 主构造函数中至少有一个参数。
  • 主构造函数中的参数必须标记为 valvar
  • 数据类不能是抽象的(abstract)、开放的(open)、密封的(sealed)或内部的(inner)。

数据类声明后,编译器会根据主构造函数中声明的所有属性自动生成以下函数:

  • .equals()/.hashCode()
  • .toString(),生成的字符串格式类似于 "User(name=John, age=42)"
  • .componentN(),按声明顺序生成,用于解构
  • .copy(),用于复制对象
kotlin 复制代码
data class User(val name: String, val age: Int)

fun main() {
    val user1 = User("小明", 18)
    val user2 = User("小明", 18)
    println(user1)   // 打印结果为格式化字符串:User(name=小明, age=18)
    println(user1 == user2)   // true,因为自动重写了equals函数
    val (name, age) = user1   // 自动添加componentN函数,因此支持解构操作
    println("名称: $name, 年龄: $age")
}

数据类成员函数的生成遵循以下规则:

  • 如果数据类主体中已经显式(手动)实现了 .equals().hashCode().toString() 等函数,或者父类中存在对应的 final 实现,则不会自动生成这些函数,而是使用已有实现。

    kotlin 复制代码
    data class User(val name: String, val age: Int) {
        // 如果已经存在toString,则不会自动生成
        override fun toString(): String = "我是自定义的toString"
    }
    
    fun main() {
        val user = User("小明", 18)
        println(user)   // 结果: 我是自定义的toString
    }
  • 如果超类型具有 open.componentN() 函数,并且返回类型兼容,则数据类会生成相应函数并覆盖它;如果因关键字限制等原因无法覆盖,则会直接报错。

    kotlin 复制代码
    abstract class AbstractUser {
        // 此函数必须是open的,否则无法被数据类继承
        open operator fun component1() = "父类的component1"
    }
    
    data class User(val name: String, val age: Int): AbstractUser()  // 自动覆盖父类的component1函数
  • 不允许为 .componentN().copy() 函数提供显式实现。

注意:编译器只会根据主构造函数中定义的属性生成对应函数。如果不希望某些属性参与自动生成的函数,就需要将其移出主构造函数。此时生成的所有函数都不会再考虑 age 属性:

kotlin 复制代码
data class Person(val name: String) {
    var age: Int = 0   // age属性不会被处理
}

数据类自带 .copy() 函数,可以复制对象,并允许在复制时修改其中一部分属性,其余属性保持不变。以上述 User 类为例:

kotlin 复制代码
data class User(val name: String, val age: Int)

fun main() {
    val user = User("小明", 18)
    val copyUser = user.copy()   // 使用拷贝函数生成一个内容完全一样的新对象
    println(user == copyUser)
    println(user === copyUser)
}

在调用 copy 函数时,也可以手动指定某些属性的新值:

kotlin 复制代码
val user = User("小明", 18)
println(user.copy(age = 17))   // 结果为 User(name=小明, age=17)

4.2 枚举类

当需要存储和表示多种固定状态时,枚举类型非常适合:

kotlin 复制代码
// 在类前面添加enum表示这是一个枚举类型
enum class LightState {
    GREEN, YELLOW, RED   // 直接在枚举类内部写上所有枚举的名称,一般全部用大写字母命名
}

使用枚举类对象时,可以通过类名直接获取已定义的枚举值:

kotlin 复制代码
fun main() {
    val state: LightState = LightState.RED  // 直接得到红灯
    println(state.name)   // 自带name属性,也就是我们编写的枚举名称,这里是RED
}

枚举类也可以拥有成员:

kotlin 复制代码
// 同样可以定义成员变量,但是不能命名为name,因为name用于返回枚举名称
enum class LightState(val color: String) {
    GREEN("绿灯"), YELLOW("黄灯"), RED("红灯");  // 枚举在定义时也必须填写参数;如果后面还要编写成员函数等内容,末尾需要添加分号
  
    fun isGreen() = this == LightState.GREEN  // 定义一个函数也是没问题的
}

可以像普通类一样使用枚举类的成员:

kotlin 复制代码
val state: LightState = LightState.RED
println("信号灯的颜色是: ${state.color}")
println("信号灯是否可以通行: ${state.isGreen()}")

枚举类型的状态是有限的,因此很适合配合 when 表达式进行判断:

kotlin 复制代码
val state: LightState = LightState.RED
val message: String = when(state) {
    LightState.RED -> "禁止通行"
    LightState.YELLOW -> "减速通行"
    LightState.GREEN -> "正常通行"
}
println(message)   // 结果为: 禁止通行

枚举类中也可以声明抽象函数,并由每个枚举值自行实现:

kotlin 复制代码
enum class LightState(val color: String) {
    GREEN("绿灯"){
        override fun test() = println("我是绿灯,表示可以通过")
    }, YELLOW("黄灯") {
        override fun test() = println("我是黄灯,表示需要减速")
    }, RED("红灯") {
        override fun test() = println("我是红灯,禁止通行")
    };
    abstract fun test()   // 抽象函数
}

fun main() {
    val state: LightState = LightState.RED
    state.test()   // 调用函数: 我是红灯,禁止通行
}

如果枚举类实现了某个接口,也可以按以下方式实现:

kotlin 复制代码
interface Message {
    fun test()
}

enum class LightState(val color: String) : Message {
    GREEN("绿灯"){
        override fun test() = println("我是绿灯,表示可以通过")
    }, YELLOW("黄灯") {
        override fun test() = println("我是黄灯,表示需要减速")
    }, RED("红灯") {
        override fun test() = println("我是红灯,禁止通行")
    };
}

// 或者
enum class LightState(val color: String) : Message {
    GREEN("绿灯"), YELLOW("黄灯"), RED("红灯");
    override fun test() = println("")
}

枚举类还提供了许多常用属性和函数:

kotlin 复制代码
val state: LightState = LightState.valueOf("RED")   // 通过valueOf函数将字符串名称转换为对应枚举
val state: LightState = enumValueOf<LightState>("RED")   // 同上
println(state)
println(state.ordinal)   // 枚举所在的位置
println(state.name)   // 枚举名称
kotlin 复制代码
val entries: EnumEntries<LightState> = LightState.entries  // 一键获取全部枚举,结果是EnumEntries类型,它是List的子接口,因此可以当做List使用
val values: Array<LightState> = enumValues<LightState>()   // 或者像这样以Array形式获取所有枚举
println(entries)
values.forEach { println(it.color) }

4.3 匿名类

匿名类除了没有名字之外,也可以定义成员。由于它是直接创建出来的,因此不能定义任何构造函数。这种写法也称为 对象表达式

kotlin 复制代码
val obj = object {   // 使用object关键字声明一个匿名类并创建其对象,可以直接使用变量接收得到的对象
    val name: String = ""
    override fun toString(): String = "我是一个匿名类"   // 匿名类默认继承于Any,可以直接重写其toString
}
println(obj)

匿名类不仅可以直接定义,也可以作为某个类的子类,或者作为某个接口的实现:

kotlin 复制代码
interface Person {
    fun chat()
}

fun main() {
    val obj: Person = object : Person {   // 直接实现Person接口
        override fun chat() = println("你好")
    }
    obj.chat()  // 当做Person的实现类使用
}
kotlin 复制代码
interface Person
open class Human(val name: String)

fun main() {
    val obj: Human = object : Human("小明"), Person {   // 继承类时,同样需要调用其构造函数
        override fun toString() = "我叫$name"   // 因为是子类,直接使用父类的属性也是没问题的
    }
    println(obj)
}

特别地,只存在一个抽象函数的接口称为 函数式接口 ,也叫 单一抽象方法(SAM)接口。函数式接口可以有多个非抽象成员,但只能有一个抽象成员。对于函数式接口,可以使用前面介绍的 Lambda 表达式来简化代码:

kotlin 复制代码
fun interface KRunnable {  // 在接口声明前面添加fun关键字
    fun invoke()
}

...

fun main() {
    val runnable = KRunnable {   // 支持使用Lambda替换
        println("我是函数invoke的实现")
    }
    runnable.invoke()
}
kotlin 复制代码
fun interface Printer {
    fun print()
}

fun test(printer: Printer) {   // 需要Printer接口实现对象
    printer.print()
}

fun main() {
    test {   // 直接Lambda一步到位
        println("Hello World")
    }
}
kotlin 复制代码
open class Human(val name: String)

fun test() = object: Human("小明") {  // 返回一个匿名类对象
    val age: Int = 10
    override fun toString() = "我叫$name"
}

fun main() {
    println(test().name)
    println(test().age)  // 编译错误,因为返回类型是Human;由于匿名特性,只能当做Human使用
}

4.4 单例类

object 关键字除了用于声明匿名类型,也可以用于声明单例类。单例类在整个程序中只能存在一个对象,也就是单个实例,不能再创建其他对象。

kotlin 复制代码
object Singleton {   // 声明一个单例类
    private var name = "小明"
    override fun toString() = "我叫$name"
}

fun main() {
    val singleton = Singleton  // 通过类名直接得到此单例类的对象
    // 不可以通过构造函数的形式创建对象
    println(singleton)
}
kotlin 复制代码
object Singleton {
    fun test() = println("单例函数被调用")
}

fun main() {
    Singleton.test()   // 单例定义的函数直接使用类名即可调用
}

4.5 伴生对象

伴生对象可以让一个类既能像普通 class 一样创建对象,也能像单例类一样通过类名直接调用某些成员。本质上,就是把一个单例对象写到某个类的内部

kotlin 复制代码
class Student(val name: String, val age: Int) {
    // 使用companion关键字在内部编写一个伴生对象,它同样是单例的
    companion object Tools {
        // 伴生对象定义的函数可以直接通过外部类名调用
        fun create(name: String, age: Int) = Student(name, age)
    }
}

fun main() {
    // 现在Student不仅具有对象的函数,还可以通过类名直接调用其伴生对象提供的函数
    val student = Student.create("小明", 18)
    println(student.toString())
}

4.6 委托模式

委托模式是继承之外的一种良好替代方案。

kotlin 复制代码
interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() = println(x)
}

替代方案:委托

kotlin 复制代码
class Derived(val base: Base): Base {   // 将一个Base的实现类作为属性保存到类中,同样实现Base接口
    override fun print() = base.print()   // 真正实现接口的并不是当前类,而是被委托进来的对象
}

Kotlin 对这种模式提供了原生支持:

kotlin 复制代码
interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() = println(x)
}

class Derived(val b: Base): Base by b  // 使用by关键字将接口中待实现的操作委托给指定成员

类的成员属性也可以委托给其他对象:

kotlin 复制代码
import kotlin.reflect.KProperty

class Example {
    var p: String by Delegate()  // 属性也可以使用by关键字委托给其他对象
}

// 委托的类
class Delegate { 
    // 需要重载属性的获取和设置两个运算
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, 这里委托了 ${property.name} 属性"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$thisRef 的 ${property.name} 属性赋值为 $value")
    }
}

fun main() {
    println(Example().p)
}

Kotlin 标准库中也提供了许多预设委托函数:

kotlin 复制代码
class Example {
    val p: String by lazy { "延迟初始化的值" }   // lazy会生成一个委托对象,首次获取属性值时才会执行代码块;由于只能获取,所以只支持val变量
}

fun main() {
    println(Example().p)
}

也可以设置观察者,实时观察变量变化:

kotlin 复制代码
class Example {
    var p: String by Delegates.observable("我是初始值") {
            prop, old, new ->
        println("检测到$prop 的值发生变化,旧值:$old -> 新值:$new")
    }
}

fun main() {
    Example().p = "新值"
}

属性还可以直接委托给另一个属性:

kotlin 复制代码
class Example(var a: String) {
    var p: String by ::a   // 使用双冒号来委托给其他属性
}

fun main() {
    val example = Example("初始值")
    println(example.p)
}

属性也可以被委托给一个 Map 来存储:

kotlin 复制代码
class User(map: MutableMap<String, Any>) {
    var name: String by map   // 直接委托给外部传入的Map集合
    var age: Int by map   // 变量的值从Map中进行存取
    override fun toString(): String = "名称: $name, 年龄: $age"
}

fun main() {
    val map: MutableMap<String, Any> = mutableMapOf(
        "name" to "John Doe",
        "age"  to 25
    )
    val user = User(map)
    println(user)   // 名称: John Doe, 年龄: 25
    map["age"] = 10   // 映射的值修改会直接影响变量的值
    println(user)  // 名称: John Doe, 年龄: 10
}

注意:使用不可变的 Map 时,只能委托给 val 类型变量,因为不可变 Map 无法修改。

4.7 密封类

有时我们会编写一些类供他人使用,但不希望这些类被随意继承,只希望它们在当前框架或模块内部使用。此时可以将类或接口设定为密封的。

kotlin 复制代码
package com.test

sealed class A   // 声明密封类很简单,直接添加sealed关键字即可
sealed class B: A()   // 密封类在同一个模块或包中可以被继承,并且子类也可以继续是密封的

当我们在其他包中使用这个密封类时,就无法继续继承它:

kotlin 复制代码
class C: A()   // 编译错误,不在同一个模块

fun main() {
    val b = B()   // 编译错误,不可以实例化
}

密封类本身也是抽象的,不能直接实例化,并且可以具有 abstract 成员:

kotlin 复制代码
sealed class A
sealed class B: A() {
    abstract fun test()
}

密封类被继承后,也可以让子类不再继续保持密封,使外部可以正常使用:

kotlin 复制代码
sealed class A
class B: A()
class C: A()
class D: A() // 不添加sealed关键字,使其不再密封

由于类 A 是密封的,因此所有直接继承自 A 的类只能由我们自己编写,其他人无法编写新的直接子类;除非我们将某个子类设定为 open,允许它继续被继承。这也说明,密封类在设计之初就确定了可用的子类范围。

由于密封类的子类范围有限,因此在使用 when 进行类型判断时,也可以进行穷尽判断:

kotlin 复制代码
fun main() {
    val a: A = C()
    when(a) {
        is B -> println("是B")
        is C -> println("是C")
        is D -> println("是D")
    }
}

5.异常

5.1 异常使用

Kotlin 中的异常都继承自 Throwable 类。

kotlin 复制代码
// Exception继承自Throwable类,作为普通的异常类型
// 自定义异常
class TestException(message: String) : Exception(message)

fun main() {
    // 抛出异常
    throw TestException("自定义异常")
}

5.2 异常处理

可以使用 try-catch 语句块来处理异常:

kotlin 复制代码
fun main() {
    val a: Array<Int> = arrayOf(1, 2, 3)
    try {
        a[5] = 5
        println(array[1])
    }catch (e: Exception){
        // 异常本身也是一个对象,catch中实际上就是用一个局部变量去接收异常
        println(e.stackTraceToString())
    }finally {
        // 无论是否出现异常,都会在最后执行
        println("finally")
    }
    println("Hello World!")
}

catch 中捕获的类型只能是 Throwable 的子类,也就是说,要么是异常,要么是错误,不能是其他类型。

当代码可能出现多种类型的异常时,如果希望针对不同异常分别处理,可以使用多重异常捕获:

kotlin 复制代码
try {
    //....
} catch (e: Exception) {  // 父类型在前,会将子类异常一并捕获
  
} catch (e: NullPointerException) {   // 因为NullPointerException是Exception的子类型,永远都不会进入这里
  
} catch (e: IndexOutOfBoundsException) {   // 永远都不会进入这里
  
}

try 也可以作为表达式使用,这意味着它可以有返回值:

kotlin 复制代码
fun main() {
    val input = readln()
    val a: Int? = try { input.toInt() } catch (e: NumberFormatException) { null }
    println(a)
}

针对可空类型,也可以在判断为空时直接抛出异常:

kotlin 复制代码
val s = person.name ?: throw IllegalArgumentException("Name required")

✨✨✨