【Kotlin】Kotlin专项总结

【Kotlin】Kotlin专项总结

本文介绍了Kotlin语言对比java的优点,以及使用中的一些重难点

本文基于公司内部我写的一篇关于Kotlin的推广文,呼吁在日常开发中更多地使用Kotlin,而不是Java。

Kotlin在Android平台上,最吸引人的一点,就是它在简洁优雅的同时,完全兼容Java,可以与Java的方法,类等无缝地进行互调用。第一章节,先介绍一下对比Java的写法优化。

第二节是Kotlin的一些高级特性,像协程,密封类,内联,noinline等。

对比Java有哪些写法优化

lambda

Java 中也有lambda,在Kotlin中的lambda表达式,是一种更简洁的函数表示方式,它可以代替匿名内部类的使用。lambda表达式的语法如下:

// 无参lambda表达式
val printName = { println("Kotlin") }

// 带参lambda表达式
val sum = { a: Int, b: Int -> a + b }

在这个例子中,printName 是一个无参的lambda表达式,它的函数体只有一行代码。sum 是一个带参的lambda表达式,它接收两个 Int 类型的参数,并返回它们的和。

与匿名内部类相比,lambda表达式的代码更简洁,可读性更好。

其他的用法,例如声明Runnable和线程的时候,可以直接使用lambda写成下面这样:

val runnable = Runnable {
    println("Kotlin")
}

val thread = Thread(runnable)
thread.start()
fun startThread() {
  Thread {
    println("Thread name is ${Thread.currentThread().name}")
  }.start()
}

实现原理

Kotlin和Java的Lambda语法实现均是基于函数式接口(内部只有一个方法的接口)。

函数式接口​​ 是指​​只包含一个抽象方法​​(Single Abstract Method,简称 SAM)​,但是可以有多个默认方法或静态方法的接口。这样的接口可以被 Lambda 表达式 或 方法引用 所实现(或替代)。

Java中没有原生的接口类,而Kotlin则原生定义了很多接口类,归类叫做FunctionN,其中N代表参数的数量,最多支持带22个参数。

public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
}
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
}

...

同时Kotlin还支持带接收者的lambda,可以说是和函数式编程和扩展函数的结合,可以在lambda的域中访问该对象的变量和方法。

在定义使用Lambda时,会默认将lambda参数继承实现 FunctionN 接口,传递到方法中,在方法中调用 invoke方法。

// HighOrderFunction.kt 文件的顶层函数
fun printSomething(print: () -> Unit) {
    print()
}

反编译之后:

public final class HighOrderFunctionKt {
   public static final void printSomething(@NotNull Function0 print) {
      Intrinsics.checkNotNullParameter(print, "print");
      print.invoke();
   }
}

循环中使用lambda的坑

上面的分析可以得知,每一个lambda的调用,不像一般的方法使用指针来调用,而是都会创建出一个匿名内部类,如果在循环中使用的话,会导致性能问题。

这时候一般会在循环中使用inline关键字修饰的内联函数,或者使用crossinline关键字修饰的内联函数,这样在循环中使用lambda时,就不会创建出多个匿名内部类了。

class LambdaTest {
    inline fun testInline(lambdaParams:()->Unit) {
        lambdaParams()
    }
}

fun main() {
    val lambdaTest = LambdaTest()
    for (i in 0..100000) {
        lambdaTest.testInline {
            println("hello world")
        }
    }
}

反编译之后:

public final class LambdaTest {
   public final void testInline(@NotNull Function0 lambdaParams) {
      Intrinsics.checkNotNullParameter(lambdaParams, "lambdaParams");
      lambdaParams.invoke();
   }
}

public final class MainKt {
   public static final void main() {
      LambdaTest lambdaTest = new LambdaTest();
      int $i$iv = 0;
      int var3;
      for(var3 = 100000; $i$iv <= var3; ++$i$iv) {
         System.out.println("hello world");
      }
   }
}

默认函数参数

Kotlin中函数的参数可以有默认值,这样在调用函数时如果没有为该参数传入值,就会使用默认值。

fun printName(name: String = "Unknown") {
    println("My name is $name")
}

printName() // 输出: My name is Unknown
printName("Kotlin") // 输出: My name is Kotlin

自动类型推断

Kotlin 编译器会根据上下文推断变量的类型,这意味着你通常不需要显式地声明变量的类型。

val name = "Kotlin" // 编译器推断 name 为 String 类型

除了变量,函数也可以自动推断参数类型和返回值类型。

fun sum(a: Int, b: Int): Int {
    return a + b
}

在这个例子中,sum 函数的参数 ab 类型都是 Int,返回值类型也是 Int。Kotlin 编译器可以根据函数体推断出这一点,所以你可以省略函数声明中的类型。

fun sum(a: Int, b: Int) = a + b

if else直接返回结果

Java中,对一个变量进行分支判断赋值,往往写成下面这样:

String name;
if (isMale) {
  name = "Mike";
} else {
  name = "Marry";
}

而Kotlin中,使用if else表达式,在一行代码里完成对变量的赋值:

val name = if (isMale) "Mike" else "Marry"

实际上反编译成Java之后,可以看出这段代码仍然使用的是上面Java的那种写法,或者一个三元判断运算符来实现,不过在面向程序员时的写法更优雅了。

when 关键字

Kotlin 中的 when 关键字,它是一个非常强大和灵活的控制流结构,是 Java 中 switch 语句的增强版。 when 在处理多种条件分支时,比传统的 if-else if-else 链更加简洁和表达性强。

when 可以作为一个表达式(有返回值)或一个语句(没有返回值)使用,这使得它比 Java 的 switch 更具通用性。它的基本作用是根据某个值或条件,执行对应的代码块。

1. when 作为表达式

when 作为表达式使用时,它会评估每个分支的条件,然后返回第一个满足条件的分支的结果。所有可能的分支都必须被覆盖(或者有一个 else 分支),以确保 when 总是能返回一个值。

fun getColorName(colorCode: Int): String {
    return when (colorCode) {
        0xFF0000 -> "Red"
        0x00FF00 -> "Green"
        0x0000FF -> "Blue"
        else -> "Unknown Color" // else 分支是必需的,因为 when 是表达式
    }
}

val color1 = getColorName(0xFF0000) // color1 = "Red"
val color2 = getColorName(0x00FFFF) // color2 = "Unknown Color"
println(color1)
println(color2)

2. when 作为语句

when 作为语句使用时,它会执行第一个满足条件的分支的代码,但不会返回任何值。在这种情况下,else 分支是可选的,除非编译器无法确定所有可能的情况都已覆盖(例如处理 sealed 类时)。

fun printColorInfo(colorCode: Int) {
    when (colorCode) {
        0xFF0000 -> println("This is the color Red.")
        0x00FF00 -> println("This is the color Green.")
        0x0000FF -> println("This is the color Blue.")
        // else 分支在这里是可选的
    }
}

printColorInfo(0x00FF00) // 输出: This is the color Green.

匹配多个值 (逗号分隔)

如果多个分支需要执行相同的操作,可以将它们用逗号 , 分隔开。

val character = 'a'
when (character) {
    'a', 'e', 'i', 'o', 'u' -> println("It's a vowel.")
    in 'b'..'z' -> println("It's a consonant.") // 后面会介绍范围匹配
    else -> println("Not a letter.")
}

范围 (Ranges) 匹配 (in!in)

可以使用 in 运算符检查值是否在一个范围内,或使用 !in 检查是否不在一个范围内。

val age = 25
val category = when (age) {
    in 0..12 -> "Child"
    in 13..19 -> "Teenager"
    in 20..64 -> "Adult"
    else -> "Senior"
}
println("Age $age is a $category.") // 输出: Age 25 is a Adult.

类型检查 (is!is)

可以使用 is 运算符检查一个值是否是某种类型,或使用 !is 检查是否不是某种类型。这在处理多态性或检查未知对象类型时非常有用。

fun describe(obj: Any) {
    when (obj) {
        1 -> println("One")
        "Hello" -> println("Greeting")
        is Long -> println("Long type value: $obj") // obj 会被智能转换为 Long
        !is String -> println("Not a String")
        else -> println("Unknown type or value")
    }
}

describe(1)      // 输出: One
describe("Hello") // 输出: Greeting
describe(1000L)  // 输出: Long type value: 1000
describe(2.5)    // 输出: Not a String
describe("Kotlin") // 输出: Unknown type or value

when 无参数

when 也可以在没有参数的情况下使用。在这种情况下,它会评估每个分支的布尔表达式,然后执行第一个为 true 的分支。这类似于一个更可读的 if-else if-else 链。

val temperature = 28
val isRaining = true

when {
    temperature > 30 -> println("It's very hot!")
    temperature > 20 && !isRaining -> println("It's warm and sunny.")
    isRaining -> println("It's raining.")
    else -> println("Normal weather.")
}
// 输出: It's warm and sunny.

处理密封类 (Sealed Classes) 或枚举 (Enums)

when 在处理密封类 (Sealed Classes)枚举 (Enums) 时特别有用。如果 when 表达式覆盖了密封类或枚举的所有可能子类/值,那么不需要 else 分支,因为编译器可以验证所有情况都已处理。

// 定义一个密封类
sealed class Result {
    data class Success(val data: String) : Result()
    data class Error(val message: String) : Result()
    object Loading : Result() // 单例对象
}

fun handleResult(result: Result) {
    when (result) {
        is Result.Success -> println("Success: ${result.data}")
        is Result.Error -> println("Error: ${result.message}")
        Result.Loading -> println("Loading data...") // 注意这里直接引用单例对象
    }
}

handleResult(Result.Success("Data fetched!")) // 输出: Success: Data fetched!
handleResult(Result.Error("Network failed.")) // 输出: Error: Network failed.
handleResult(Result.Loading)                 // 输出: Loading data...

这种用法在 Android 中处理网络请求状态、UI 事件或不同的视图状态时非常常见和强大,因为它提供了编译时安全,确保你不会遗漏任何一种情况。

范围限制coerceIn

coerceIn 是一个扩展函数,作用是将接收者对象的值“强制”限定在一个指定的范围内。如果原始值在这个范围内,就返回原始值;如果原始值小于范围的最小值,就返回最小值;如果原始值大于范围的最大值,就返回最大值。

这个函数通常用于任何实现了 Comparable 接口的类型,比如数字(Int, Double, Float, Long 等)、字符串,甚至自定义的可比较对象。

fun main() {
    // 1. 限定整数范围
    val num1 = 5.coerceIn(1, 10)  // 5 在 [1, 10] 之间,返回 5
    val num2 = 0.coerceIn(1, 10)  // 0 小于 1,返回 1
    val num3 = 12.coerceIn(1, 10) // 12 大于 10,返回 10
    println("Int coercing: $num1, $num2, $num3") // 输出: Int coercing: 5, 1, 10

    // 2. 限定浮点数范围
    val float1 = 3.5f.coerceIn(1.0f, 5.0f) // 3.5f 在 [1.0f, 5.0f] 之间,返回 3.5f
    val float2 = 0.5f.coerceIn(1.0f, 5.0f) // 0.5f 小于 1.0f,返回 1.0f
    println("Float coercing: $float1, $float2") // 输出: Float coercing: 3.5, 1.0

    // 3. 限定字符串范围 (按字典顺序)
    val str1 = "banana".coerceIn("apple", "orange") // banana 在 apple 和 orange 之间,返回 banana
    val str2 = "cat".coerceIn("apple", "banana")    // cat 大于 banana,返回 banana
    val str3 = "zoo".coerceIn("apple", "orange")    // zoo 大于 orange,返回 orange
    println("String coercing: $str1, $str2, $str3") // 输出: String coercing: banana, banana, orange

    // 4. 处理负数或范围倒置(注意:如果 min > max,会抛出 IllegalArgumentException)
    // val invalidRange = 5.coerceIn(10, 1) // 这会抛出 IllegalArgumentException
}

Android 应用 coerceIn

在 Android 开发中,coerceIn 在很多场景下都能派上用场:

  1. UI 元素的滑动或拖拽限制: 当用户拖拽一个视图时,你可能需要限制其位置在屏幕的某个特定区域内。
    val newX = event.rawX.coerceIn(0f, screenWidth - viewWidth)
    view.x = newX
    
  2. 进度条或评分: 确保进度值或评分值始终在有效的 0 到 100(或 1 到 5)范围内。
    val progress = (rawProgressValue * 100).toInt().coerceIn(0, 100)
    progressBar.progress = progress
    
  3. 动画插值: 限制动画的起始或结束值,防止超出预期。
  4. 游戏开发: 限制玩家角色的移动范围,或限制敌人 AI 的行为范围。
  5. 数据验证: 在处理用户输入或从外部来源获取数据时,确保数值符合预期的业务规则。
    val quantity = inputString.toIntOrNull()?.coerceIn(1, 99) ?: 1 // 如果解析失败或超出范围,默认为1
    
  6. 数值计算: 避免计算结果超出合理的物理或逻辑限制。

coerceAtLeastcoerceAtMost

Kotlin 还提供了两个更细粒度的“强制”函数:

  • value.coerceAtLeast(minimumValue): 返回 valueminimumValue较大的那个。它只设定下限。
    val score = 80.coerceAtLeast(90) // 返回 90 (因为它不能低于 90)
    val score2 = 95.coerceAtLeast(90) // 返回 95
    
  • value.coerceAtMost(maximumValue): 返回 valuemaximumValue较小的那个。它只设定上限。
    val speed = 120.coerceAtMost(100) // 返回 100 (因为它不能高于 100)
    val speed2 = 90.coerceAtMost(100) // 返回 90
    

这两个函数在你只需要限制单边范围(只有上限或只有下限)时非常方便。

空安全

这一点是Kotlin的核心设计,也是它的一大卖点。Kotlin的空安全设计对于开发者来说是一种福利,它可以在编译阶段就发现很多空指针异常,而不是在运行时才发现。

作为一名 Android 开发者,你肯定深知 NullPointerException (NPE) 是 Java 开发中常见且令人头疼的问题。Kotlin 的设计目标之一就是消除这种运行时错误,通过在编译时强制进行空安全检查来解决这个问题。

1. 可空类型与非空类型

  • 非空类型 (Non-nullable Types): 默认情况下,Kotlin 中的所有类型都是非空的。这意味着你声明的变量如果没有明确标记为可空,就不能被赋值为 null。如果你尝试将 null 赋值给非空类型的变量,编译器会报错。
    var name: String = "Kotlin"
    // name = null // 编译错误:Null can not be a value of a non-null type String
    
  • 可空类型 (Nullable Types): 如果你确实需要一个变量可以持有 null 值,你必须在其类型后面加上问号 ? 来明确声明它为可空类型。
    var name: String? = "Kotlin"
    name = null // 编译通过
    

    这样,编译器就知道 name 这个变量可能为 null,并在你访问它的成员时强制你进行空检查。

Java里的空指针(NPE)报错,Kotlin 中也有类似的,就是使用 lateinit var 的不可空变量时,需要注意初始化和使用时机不对的情况下,有可能会报 UnInitializedPropertyAccessException 异常。所以在一些不确定是否在初始化完毕之后调用的方法里,使用变量时,最好加一层初始化判断。

// 延迟初始化变量
lateinit var str: String

// 使用时进行初始化判断
if(::str.isInitialized) {
    print(str.length)
}

2. 安全调用操作符 (?.)

当处理可空类型的变量时,你不能直接访问它的成员(例如调用方法或访问属性)。你需要使用安全调用操作符 ?.

  • 如果 ?. 左边的表达式不为 null,则会正常执行右边的操作。
  • 如果 ?. 左边的表达式为 null,则整个表达式的结果为 null,并且不会执行右边的操作,从而避免了 NPE。
    val name: String? = null
    val length: Int? = name?.length // 如果 name 为 null,则 length 也为 null
    println(length) // 输出: null
    
    val name2: String? = "Hello"
    val length2: Int? = name2?.length
    println(length2) // 输出: 5
    

3. Elvis 操作符 (?:)

Elvis 操作符 ?: 提供了一种简洁的方式来处理可空值,当左边的表达式为 null 时,提供一个默认值。

  • 如果 ?: 左边的表达式不为 null,则返回左边的值。
  • 如果 ?: 左边的表达式为 null,则返回 ?: 右边的默认值。
    val name: String? = null
    val length: Int = name?.length ?: 0 // 如果 name?.length 为 null,则 length 为 0
    println(length) // 输出: 0
    
    val name2: String? = "World"
    val length2: Int = name2?.length ?: 0
    println(length2) // 输出: 5
    

4. 非空断言操作符 (!!)

非空断言操作符 !! 允许你将任何可空类型的值转换为非空类型。然而,如果 !! 左边的表达式为 null,它会抛出一个 NullPointerException

val name: String? = null
// val length: Int = name!!.length // 运行时会抛出 NullPointerException

这个操作符应该慎用,只有当你非常确定某个值在特定时刻不可能为 null 时才使用。它的作用是告诉编译器“我保证这里不会是 null,如果错了,就让它崩溃吧”。

5. let 函数

let 是一个作用域函数,常用于对非空对象执行操作。如果接收者对象不为 nulllet 函数会执行给定的 lambda 表达式,并将接收者作为 it 参数传入。

val name: String? = "Kotlin"
name?.let {
    // 只有当 name 不为 null 时才执行这里的代码
    println("The name is ${it.toUpperCase()}")
}

val name2: String? = null
name2?.let {
    // 这段代码不会执行
    println("This will not be printed if name2 is null")
}

6. 安全类型转换 (as?)

安全类型转换 as? 尝试将一个值转换为指定的类型,如果转换失败,则返回 null,而不是抛出 ClassCastException

val obj: Any = "Hello"
val str: String? = obj as? String // str 为 "Hello"

val num: Any = 123
val str2: String? = num as? String // str2 为 null

平台类型 (Platform Types)

Kotlin 的空安全设计非常严格,但当它需要与 Java 代码交互时,就面临一个挑战。Java 不像 Kotlin 那样在类型系统中强制空安全,Java 的引用可以是 null,也可以是非 null,这在编译时是无法确定的。

为了解决这个问题,Kotlin 引入了平台类型 (Platform Types)

平台类型是指 Kotlin 编译器 无法确定其空性 的类型,通常是来自 Java 代码的类型。当你从 Java 代码中调用方法或访问字段时,Kotlin 编译器无法知道这些值是否可能为 null

平台类型在 Kotlin 中用 T! 的形式表示(例如 String!),但你不能在代码中显式地声明一个平台类型。它只会在编译器推断出类型时出现。

例如,如果你有一个 Java 类:

// JavaClass.java
public class JavaClass {
    public String getName() {
        return null; // Java 中可以返回 null
    }

    public void printValue(String value) {
        System.out.println(value.length()); // 如果 value 为 null,这里会抛出 NPE
    }
}

在 Kotlin 中使用 JavaClass

// Kotlin code
val javaClass = JavaClass()
val name = javaClass.name // name 的类型会被推断为 String! (平台类型)

平台类型的特点和处理

当你操作一个平台类型的值时,Kotlin 编译器不会强制进行空安全检查。这意味着你可以像在 Java 中那样使用它,但这也意味着你可能会遇到 NullPointerException,因为它可能在运行时为 null

对于平台类型,Kotlin 将空性的责任交给了开发者。你可以选择将其视为可空类型 (String?) 或非空类型 (String)。

  • 如果你确定它不会是 null,可以将其赋值给非空类型。如果运行时是 null,就会抛出 NPE。
  • 如果你不确定它是否为 null,最好将其赋值给可空类型,并使用安全调用操作符或其他空处理机制。

在Java代码中,为了帮助 Kotlin 编译器更好地理解 Java 代码的空性,Java 库可以使用空性注解(如 @Nullable, @NotNull,来自 JetBrains、AndroidX、JSR-305 等)。如果 Java 代码使用了这些注解,Kotlin 编译器可以根据注解信息 将 Java 类型映射为 Kotlin 的可空或非空类型 ,从而避免平台类型带来的不确定性。

总结来说,平台类型是 Kotlin 和 Java 互操作性中的一个“妥协点”,它允许你在 Kotlin 中使用 Java 代码,但同时也提醒你,在这些特定情况下,Kotlin 的编译时空安全保护可能会失效,你需要更加小心地处理潜在的 null 值。

单例类

Java中比较通用的单例类写法一般为static关键字声明的懒加载同步方法。

public class Singleton {
    private static Singleton instance;

    private Singleton() {
        // 私有构造函数,防止外部实例化
    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

在Kotlin中,想要定义一类为全局单例模式,只需要使用 object 关键字来声明类就可以了。

object Singleton {
    fun doSomething() {
        // 单例对象的方法
    }
}

// 使用
Singleton.doSomething()

这个写法等同于Java中的饿汉单例模式,对于开发者写起来更简洁,反编译之后的java代码如下:

public final class Singleton {
   @NotNull
   public static final Singleton INSTANCE = new Singleton();

   private Singleton() {
   }
}

伴生对象

在Kotlin中,每个类都可以有一个伴生对象。伴生对象的成员可以直接通过类名调用,而不需要实例化类。

class MyClass {
    companion object {
        fun doSomething() {
            // 伴生对象的方法
        }
    }
}

// 使用
MyClass.doSomething()

与Java中的静态方法类似,Kotlin中的伴生对象方法在反编译后的Java代码中也会被转换为静态方法。

在Android中,可以把类的TAG,和这个类强相关的一些常量,都定义在这个类的伴生对象中。

字符串模板

Java中,字符串和变量的结合需要使用加号+,而Kotlin中可以使用字符串模板来简化这个过程。在Kotlin中,可以使用字符串模板来动态构建字符串。字符串模板以$开头,在其中可以嵌入变量或表达式。

val name = "Kotlin"
val message = "Hello, $name!" // 字符串模板,结果为 "Hello, Kotlin!"

如果是和常量拼接,在编译器就会直接内联优化为字符串。如果是变量拼接,最后运行时实际上还是使用StringBuilder来拼接字符串。

扩展函数

在Kotlin中,可以为现有的类添加新的函数,而不需要修改类的源代码。这些新的函数被称为扩展函数。扩展函数允许你在不继承类的情况下,向类添加新的行为。

例如,String 类并没有一个内置的 isPalindrome() 方法来检查一个字符串是否是回文,但你可以通过扩展函数为它添加这个功能:

fun String.isPalindrome(): Boolean {
    val cleanedString = this.lowercase().replace(Regex("[^a-z0-9]"), "")
    return cleanedString == cleanedString.reversed()
}

fun main() {
    val word = "madam"
    println(word.isPalindrome()) // 输出: true
}

在这个例子中:

  • fun String.isPalindrome(): Boolean 定义了一个扩展函数。
  • String. 表示这个函数是 String 类的扩展。
  • 在函数内部,this 关键字引用了调用该函数的 String 实例。

扩展函数让代码看起来更自然。比如 string.isPalindrome()StringUtils.isPalindrome(string) 更直观。扩展函数可以把这些“工具”方法直接挂载到它们所操作的类上,减少了Utils工具类的数量,使得代码结构更清晰。

实现原理

Kotlin 的扩展函数实际上是一个静态函数。当 Kotlin 编译器处理扩展函数时,它会将其转换为一个普通的静态方法,这个静态方法会将接收者对象作为第一个参数。

例如,上面的 String.isPalindrome() 扩展函数在编译后,大致等价于一个 Java 中的静态方法:

// 编译后的伪 Java 代码
public final class StringExtensionsKt { // 自动生成的文件名,通常是文件名 + Kt
    public static final boolean isPalindrome(@NotNull String $receiver) {
        // 函数体内部的 this 对应于这里的 $receiver 参数
        String cleanedString = $receiver.toLowerCase().replaceAll("[^a-z0-9]", "");
        return cleanedString.equals(new StringBuilder(cleanedString).reverse().toString());
    }
}

然后,当你调用 word.isPalindrome() 时,编译器会将其转换为对这个静态方法的调用:

// 编译后的伪 Java 代码
StringExtensionsKt.isPalindrome(word);

这就是为什么扩展函数不能访问其接收者的 privateprotected 成员——因为它并不是真正意义上的成员函数,它只是一个方便的语法糖。

Android 开发中的常见应用

在 Android 开发中,扩展函数无处不在,极大地简化了代码:

  • View 扩展: 为 View 添加方便的函数,比如 View.show()View.hide()View.gone()
    fun View.show() {
        this.visibility = View.VISIBLE
    }
    
    fun View.hide() {
        this.visibility = View.INVISIBLE
    }
    
    fun View.gone() {
        this.visibility = View.GONE
    }
    
  • Context 扩展: 简化 Toast 显示、资源获取等操作。
    fun Context.toast(message: String, duration: Int = Toast.LENGTH_SHORT) {
        Toast.makeText(this, message, duration).show()
    }
    // 使用: context.toast("Hello!")
    
  • Fragment/Activity 扩展: 简化 FragmentTransactionIntent 的使用。
  • 数据类型转换: 比如为 IntLong 添加 toPx()toDp() 转换函数。

高阶函数 let、with、apply、run、also

也叫操作域函数,它们是 Kotlin 标准库中非常强大且常用的高阶函数。作为 Android 开发者,你肯定会在日常工作中频繁遇到和使用它们,因为它们能让你的代码更简洁、更易读,尤其是处理对象的配置、转换或安全调用时。

作用域函数是一种特殊的函数,它们的主要目的是在你提供的 lambda 表达式内部创建一个 临时作用域 。在这个作用域内,你可以直接访问(或引用)你所操作的对象,从而避免重复写对象名,让代码更紧凑。

Kotlin 提供了五种主要的作用域函数:let、run、with、apply 和 also。它们之间的主要区别在于:

  • 引用上下文对象的方式:使用 this 还是 it。
  • 返回值:返回上下文对象本身还是 lambda 表达式的结果。

apply

apply 函数,它的 lambda 表达式的 最后一行代码会自动作为返回值 返回。不同之处在于 apply 函数始终返回 上下文对象本身

val result = "Kotlin".apply {
    println("Length: $length") // 可以直接访问 String 的属性
}
// 输出: Length: 6
// 因为 apply 始终返回上下文对象本身,所以可以直接链式调用
"Kotlin".apply {
    println("Length: $length")
}.also {
    println("Also: $it") // 输出: Also: Kotlin
}

在Android中,apply通常用于对一个对象进行初始化或设置属性。例如,在RecyclerView的初始化过程中。

recyclerView.apply {
    layoutManager = LinearLayoutManager(context)
    addItemDecoration(MyDecoration(context)) // 添加分隔线装饰器
    setHasFixedSize(true) // 固定大小,提高性能
    adapter = myAdapter // 设置适配器
}

let

上面的空安全有提到一次,最常用于判空场景,非空后执行let中的代码。返回值 lambda 表达式的最后一行结果。还可以很方便地在链式调用中对结果进行操作或转换。

val name: String? = "Alice"

// 传统空检查
if (name != null) {
    println(name.length)
}

// 使用 let 进行空安全操作
name?.let {
    // 这里的 it 就是非空的 name
    println(it.length)
}

// 链式调用和转换
val result = "Hello Kotlin"
    .length
    .let { it * 2 } // 将长度乘以2
    .let { "Double length: $it" } // 转换为字符串
println(result) // 输出: Double length: 24

run

内部引用方式为 this ,返回值为 lambda 表达式的最后一行结果。

run 主要有两种应用形式:

作为扩展函数调用 (在对象上调用)

val user = User("Bob", 30)

val userDescription = user.run {
    // 这里的 this 就是 user 对象
    "Name: ${this.name}, Age: ${this.age}" // 返回这个字符串
}
println(userDescription) // 输出: Name: Bob, Age: 30

// 结合空安全 (类似 let)
val greeting: String? = "Hello"
val finalMessage = greeting?.run {
    // 这里的 this 就是非空的 "Hello"
    toUpperCase() + "!" // 返回 "HELLO!"
} ?: "No greeting" // 如果 greeting 为 null,则返回 "No greeting"
println(finalMessage) // 输出: HELLO!

独立调用

val message = run {
    val x = 10
    val y = 20
    "Sum: ${x + y}" // 返回这个字符串
}
println(message) // 输出: Sum: 30

with

引用方式为 this ,返回值 lambda 表达式的最后一行结果。 已知非空的对象执行一系列操作,而不需要链式调用。与 run 作为扩展函数类似,但 with 不是扩展函数写法,它将对象作为第一个参数传入。

val configuration = Configuration("Debug", 1024)

val configDetails = with(configuration) {
    // 这里的 this 就是 configuration 对象
    println("Configuring system...")
    "Mode: ${mode}, Size: ${maxSize}MB" // 返回这个字符串
}
println(configDetails) // 输出: Mode: Debug, Size: 1024MB

also

内部引用方式为 it 。返回 上下文对象本身 。主要用于 执行对象的附加操作,不影响对象本身,通常用于副作用 (side-effects)。例如,日志记录、调试输出或在对象准备好后执行一些不影响其状态的操作。

val numbers = mutableListOf(1, 2, 3)

val processedNumbers = numbers.also {
    // 这里的 it 就是 numbers 列表
    println("Before adding: $it") // 打印当前列表状态
    it.add(4)
}.also {
    println("After adding: $it") // 再次打印列表状态
}
// also 返回 numbers 列表本身,所以 processedNumbers 仍然是 numbers
println(processedNumbers) // 输出: [1, 2, 3, 4]

also 适用于你想在不改变原始对象的情况下,对其执行一些额外操作的场景。

操作域函数小结

  • let: 如果你想在代码块中对一个可空对象执行操作,或者想对结果进行转换,并返回转换后的值。
  • run:
    • 作为扩展函数: 如果你想配置一个对象并计算一个结果,或者结合空安全和 this 引用。
    • 作为非扩展函数: 如果你想封装一段语句,并返回其结果。
  • with: 如果你有一个非空对象,并且想在其作用域内执行一系列操作并返回一个结果
  • apply: 如果你想配置一个对象并返回该对象本身。非常适合链式设置多个属性。
  • also: 如果你想在不改变对象的情况下,对它执行一些额外操作或副作用(例如日志记录、调试打印),并返回该对象本身。
val originSting = "Kotlin"
val letString = originSting.let {
    it.uppercase()
}
val applyString = originSting.apply {
    uppercase()
}
val withString = with(originSting) {
    uppercase()
}
val runString = originSting.run {
    uppercase()
}
val alsoString = originSting.also {
    it.uppercase()
}
/**
letString: KOTLIN
applyString: Kotlin
withString: KOTLIN
runString: KOTLIN
alsoString: Kotlin
*/

根据运行结果可以看出,apply和also都是返回操作的对象本身的,另外的三个,都是返回最后一行表达式的结果。在使用操作域函数时,需要注意这一点以免拿到不符合预期的数据。

map扩展函数

map 可以对List,Map,Set等集合对象中的元素进行转换,生成一个新的集合。

例如:

val numbers = listOf(1, 2, 3, 4)
val doubled = numbers.map { it * 2 } 
// 结果: [2, 4, 6, 8]

// 或显式命名参数
val squared = numbers.map { number -> number * number }
// 结果: [1, 4, 9, 16]

在Android中,也可以用在初始化要显示的数据集上,语法更简洁:

private val functionList = listOf(
    "壁纸取色测试" to {
        startActivity(Intent(this, WallpaperTestActivity::class.java))
    },
    "弹一个Toast" to {
        Toast.makeText(this, "一个普通的Toast", Toast.LENGTH_SHORT).show()
    },
    "设备Root状态" to {
        startActivity(Intent(this, RootInfoActivity::class.java))
    },
    "CPU信息" to {
        startActivity(Intent(this, CpuInfoActivity::class.java))
    },
).map { (name, task) -> FunctionItem(name, task) }

还可以和 Flow 数据流一起作用,在数据发送之前使用 map 预处理一遍:

fun mapTest() {
    CoroutineScope(Dispatchers.IO).launch {
        flowOf(1, 2, 3, 4, 5).map {
            it + 1
        }.collectLatest {
            Log.i(TAG, "mapTest collect $it")
        }
    }
}

use扩展函数

use 函数是 Kotlin 标准库为实现了 Closeable 或 AutoCloseable 接口的类(如 FileInputStream、BufferedReader 等)提供的扩展函数。

它主要用于​资源管理​​(如文件、网络连接、数据库连接等),它可以确保资源在使用完毕后被正确关闭,即使发生异常也能保证资源释放,防止内存泄露。其底层实现实际上也是对try-catch-finally的封装。

用法举例,独取一个文件的内容:

fun readFile() {
    val file = File("example.txt")
    FileInputStream(file).use { inputStream ->
        val bytes = inputStream.readBytes()
        println(String(bytes))
    } // inputStream 自动关闭
}

数据库连接:

fun queryDatabase() {
    val connection: Connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db")
    connection.use { conn ->
        // 执行 SQL 查询
        conn.createStatement().use { statement ->
            statement.executeQuery("SELECT * FROM users").use { resultSet ->
                while (resultSet.next()) {
                    println(resultSet.getString("name"))
                }
            }
        }
    } // conn 自动关闭
}

Kotlin集合

对于Kotlin和Java中的集合简要对比,专门提取来一篇来记录:

Kotlin中的集合

data class数据类

data class 是 Kotlin 中的一个重要概念,通常用于表示数据对象。

数据类是专门为存储数据而设计的类。 Kotlin 编译器会自动为数据类生成许多有用的成员函数,从而省去了你手动编写这些函数的麻烦。这使得你的代码更简洁、更安全、更易读。

在 Java 中,为了实现一个简单的数据持有类,你需要写大量的模板代码(构造函数、getter/setter、equals()hashCode()toString() 等),而 Kotlin 的数据类用一个关键字就搞定了这一切。

data class User(val name: String, val age: Int)
  1. 所有属性的 Getters (以及 var 属性的 Setters): 尽管在 Kotlin 中我们通常直接访问属性,但底层它们依然是存在的。

    val user = User("Alice", 30)
    println(user.name) // 访问 name 属性
    
  2. equals(other: Any?): 用于比较两个数据类对象是否相等。当且仅当两个对象的类型相同,并且主构造函数中声明的所有属性的值都相等时,它们才被认为是相等的。

    val user1 = User("Alice", 30)
    val user2 = User("Alice", 30)
    val user3 = User("Bob", 25)
    
    println(user1 == user2) // 输出: true (因为属性值相同)
    println(user1 == user3) // 输出: false
    

    注意: 传统的类比较的是内存地址(引用相等),而数据类比较的是内容(结构相等)。

  3. hashCode(): 返回一个基于主构造函数中所有属性的哈希码。这在将数据类对象存储在哈希集合(如 HashSetHashMap)中时至关重要。equals()hashCode() 必须保持一致性(如果两个对象 equals 返回 true,它们的 hashCode 也必须相同)。

    val userSet = hashSetOf(user1)
    println(userSet.contains(user2)) // 输出: true (因为 user2 的 equals 和 hashCode 与 user1 相同)
    
  4. toString(): 返回一个包含类名和所有属性及其值的字符串表示。这对于日志记录和调试非常有用。

    val user = User("Alice", 30)
    println(user) // 输出: User(name=Alice, age=30)
    
  5. componentN() 函数: 为每个在主构造函数中声明的属性生成一个 componentN() 函数,其中 N 是属性在声明时的顺序(component1() 对应第一个属性,component2() 对应第二个,以此类推)。这些函数使得数据类可以支持解构声明 (Destructuring Declarations)

    val (name, age) = User("Alice", 30) // 解构声明
    println("Name: $name, Age: $age") // 输出: Name: Alice, Age: 30
    
  6. copy(): 创建一个新对象,复制现有对象的所有属性,同时允许你选择性地修改某些属性的值。这对于创建对象的一个副本但需要轻微修改时非常有用,因为数据类通常是不可变的(尽管也可以有 var 属性)。

    val originalUser = User("Alice", 30)
    val copiedUser = originalUser.copy(age = 31) // 复制 originalUser,只改变 age 属性
    val anotherCopiedUser = originalUser.copy(name = "Bob") // 改变 name 属性
    
    println(originalUser)    // 输出: User(name=Alice, age=30)
    println(copiedUser)      // 输出: User(name=Alice, age=31)
    println(anotherCopiedUser) // 输出: User(name=Bob, age=30)
    

数据类使用注意事项

  1. 主构造函数必须至少有一个参数:所有自动生成的函数都是基于主构造函数中声明的属性。
  2. 主构造函数的所有参数都必须标记为 valvar:这是为了确保它们是类中的属性,而不是仅仅是构造函数参数。
  3. 不能是 abstractopensealedinner:数据类通常是最终的,不适合继承层次结构。
  4. 可以有其他成员: 除了自动生成的函数,你也可以在数据类中定义自己的函数、属性或伴生对象。

    data class Product(val id: String, val name: String, var price: Double) {
        // 自定义函数
        fun displayInfo() {
            println("Product ID: $id, Name: $name, Price: $price")
        }
    
        // 伴生对象
        companion object {
            const val DEFAULT_CURRENCY = "USD"
        }
    }
    
  5. 属性的默认值: 你可以为数据类的主构造函数属性提供默认值。

    data class Settings(val theme: String = "dark", val notificationsEnabled: Boolean = true)
    val defaultSettings = Settings() // 使用默认值
    val customSettings = Settings(theme = "light") // 覆盖默认值
    

Android中常用场景

在 Android 开发中,数据类无处不在:

  • API 响应模型: 当你从 RESTful API 获取数据时,通常会定义数据类来映射 JSON 或 XML 结构。
    data class Post(val userId: Int, val id: Int, val title: String, val body: String)
    
  • 数据库实体: 当使用 Room Persistence Library 或其他 ORM 框架时,数据类可以很好地表示数据库表中的一行数据。
    @Entity(tableName = "users")
    data class UserEntity(@PrimaryKey val id: Long, val name: String, val email: String)
    
  • UI 状态: 在 MVVM 或 MVI 架构中,数据类常用于表示 UI 的当前状态,方便进行状态的更新和比较。
    data class UserProfileState(
        val isLoading: Boolean = false,
        val user: User? = null,
        val errorMessage: String? = null
    )
    
  • 事件 (Events): 在事件驱动的架构中,数据类可以很好地表示各种事件。
    sealed class LoginEvent {
        data class Success(val userId: String) : LoginEvent()
        data class Error(val message: String) : LoginEvent()
        object Loading : LoginEvent()
    }
    

Kotlin高级特性

sealed class和sealed interface

Kotlin 的密封类 (Sealed Class) 是一个非常棒的特性,尤其是在处理有限的、受限的类继承结构时。它能让你的代码更安全、更具表达力,并且在与 when 表达式结合使用时,能提供强大的编译时检查。

密封类是一种限制类继承层次结构的特殊抽象类。 它的主要目的是声明一个受限的类层次结构,其中所有可能的子类都必须在同一文件内声明(Kotlin 1.5 之后可以在同一个模块内的任何文件中声明,但通常仍推荐在同一文件内以保持紧凑性)。

这就意味着,编译器在编译时就知道了这个密封类的所有可能直接子类。这种“已知子类”的特性是密封类最有价值的地方。

// 定义一个密封类来表示网络请求的结果
sealed class NetworkResult {
    data class Success(val data: String) : NetworkResult() // 子类可以是数据类
    data class Error(val message: String) : NetworkResult()   // 子类可以是数据类
    object Loading : NetworkResult()                          // 子类可以是单例对象
    class Idle : NetworkResult()                              // 子类也可以是普通类
}

在这个例子中:

  • NetworkResult 是一个密封类。
  • SuccessErrorLoadingIdleNetworkResult 的直接子类。
  • 重要: 所有的这些子类都必须在定义 NetworkResult同一文件内(或者在 Kotlin 1.5+ 中,在同一模块内),这样编译器才能“知道”它们。

1. 确保穷举性检查 (Exhaustiveness Checking) 与 when 表达式

这是密封类最强大的特性。当你在 when 表达式中使用密封类的实例时,如果 when 覆盖了所有可能的子类型,Kotlin 编译器会强制你处理所有可能的子类,并且不需要 else 分支。如果遗漏了某个子类,编译器会报错,从而防止运行时错误。

fun handleNetworkResult(result: NetworkResult) {
    when (result) {
        is NetworkResult.Success -> {
            println("数据加载成功: ${result.data}")
        }
        is NetworkResult.Error -> {
            println("加载失败: ${result.message}")
        }
        NetworkResult.Loading -> { // 注意:对于 object,直接引用即可
            println("正在加载中...")
        }
        is NetworkResult.Idle -> {
            println("网络请求处于空闲状态。")
        }
        // 不需要 else 分支,因为编译器知道所有可能的子类型都被处理了
    }
}

这对于构建健壮的应用程序至关重要,特别是在处理 UI 状态、事件或网络响应时。

2. 更好的类型安全和代码可读性

密封类提供了一种清晰的方式来建模有限的状态。例如,一个 UI 组件的状态可能只有“加载中”、“显示数据”或“显示错误”几种。使用密封类可以明确地表示这些状态,使得代码的意图一目了然,并减少了引入无效状态的可能性。

3. 作为枚举的替代(更强大)

虽然枚举 (enum class) 也能表示一组有限的值,但枚举的每个成员都是一个简单的实例,不能携带额外的状态。而密封类的每个子类可以是独立的类,可以拥有自己的属性和行为,这使得它比枚举更加灵活和强大。

// 枚举无法携带额外数据
enum class Color { RED, GREEN, BLUE }

// 密封类可以携带额外数据
sealed class Shape {
    data class Circle(val radius: Double) : Shape()
    class Square(val side: Double) : Shape()
    object Triangle : Shape() // 也可以是无状态的单例
}

密封类与枚举 (Enum Class) 的场景选择

  • 使用枚举: 如果你只需要表示一组固定且不携带额外数据的常量集(例如方向:上、下、左、右;或简单的状态:开启、关闭)。
  • 使用密封类: 如果你需要表示一组有限的、可携带不同数据或具有不同行为的子类型(例如网络请求结果、UI 状态、事件)。

密封类在 Android 开发中的常见应用

在 Android 开发中,密封类几乎无处不在,是管理复杂状态和事件的利器:

  1. 网络请求结果: 如上面示例所示,表示 API 调用的不同状态(成功、失败、加载中)。
    sealed class Resource<out T> { // 可以是泛型
        data class Success<out T>(val data: T) : Resource<T>()
        data class Error(val message: String, val errorCode: Int) : Resource<Nothing>()
        object Loading : Resource<Nothing>()
    }
    
  2. UI 状态: 定义一个屏幕可能拥有的所有状态。
    sealed class UserViewState {
        object Loading : UserViewState()
        data class Loaded(val user: User) : UserViewState()
        data class Error(val errorMessage: String) : UserViewState()
        object Empty : UserViewState()
    }
    
  3. 用户交互事件: 表示用户在界面上的各种操作。
    sealed class ProfileEvent {
        object LoadProfile : ProfileEvent()
        data class UpdateName(val newName: String) : ProfileEvent()
        object Logout : ProfileEvent()
    }
    
  4. RecyclerView 列表项: 如果一个 RecyclerView 可以显示不同类型的列表项(Header, Item, Footer),可以用密封类来建模。

协程

Kotlin的协程也是广为开发者讨论的一个异步框架,在Android应用开发过程中,几乎可以完全替代线程的使用,并且以同步方式写异步代码看起来也比较优雅。

详细的有多篇文章介绍过:

Kotlin协程的基础使用

Kotlin协程浅谈

Kotlin协程的取消与异常处理

Kotlin协程挂起恢复源码解析

内联函数 (Inline Functions) 与交叉内联 (Crossinline)/无内联 (Noinline)

关于这几个内联相关的关键字,由另一篇文章也记录过:

Kotlin的inline&crossinline&noinline关键字

泛型的 in out 和 Reified 关键字

Kotlin协变和逆变

委托

在 Kotlin 中,委托(Delegation) 是一种强大的设计模式,它允许对象将部分功能委托给另一个辅助对象来实现。Kotlin 原生支持多种委托方式,主要分为以下几种:

类委托(Class Delegation)

通过 by 关键字,将类的接口实现委托给另一个对象,常用于 “装饰器模式” 或 “代理模式”。

示例:委托接口实现

interface Printer {
    fun print(message: String)
}

class DefaultPrinter : Printer {
    override fun print(message: String) {
        println("Default Printer: $message")
    }
}

// 委托给 printer 对象
class CustomPrinter(private val printer: Printer) : Printer by printer {
    // 可以覆盖部分方法
    override fun print(message: String) {
        println("Before Printing...")
        printer.print(message) // 调用委托对象的方法
        println("After Printing...")
    }
}

fun main() {
    val defaultPrinter = DefaultPrinter()
    val customPrinter = CustomPrinter(defaultPrinter)
    customPrinter.print("Hello, Kotlin!")
}

输出:

Before Printing… Default Printer: Hello, Kotlin! After Printing…

适用场景:

  • 增强或修改现有类的行为(如日志、缓存、权限控制)。
  • 避免继承,使用组合代替。

属性委托(Property Delegation)

Kotlin 提供标准库委托(如 lazy、observable),也可以自定义委托。

(1) lazy 延迟初始化

val lazyValue: String by lazy {
    println("Computed only once!")
    "Hello"
}

fun main() {
    println(lazyValue) // 第一次访问时计算
    println(lazyValue) // 直接返回缓存值
}

输出:

Computed only once! Hello Hello

(2) observable 监听属性变化

import kotlin.properties.Delegates

var observedValue: Int by Delegates.observable(0) { _, old, new ->
    println("Value changed from $old to $new")
}

fun main() {
    observedValue = 10  // 触发回调
    observedValue = 20  // 再次触发
}

输出:

Value changed from 0 to 10 Value changed from 10 to 20

(3) vetoable 可拦截修改

var positiveNumber: Int by Delegates.vetoable(0) { _, old, new ->
    new > 0  // 只有 new > 0 时才允许修改
}

fun main() {
    positiveNumber = 10  // 允许
    println(positiveNumber)  // 10
    positiveNumber = -5     // 拒绝修改
    println(positiveNumber)  // 仍然是 10
}

(4) 自定义属性委托

class StringDelegate(private var initValue: String) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("Getting value: $initValue")
        return initValue
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("Setting value: $value")
        initValue = value
    }
}

fun main() {
    var text by StringDelegate("Default")
    println(text)  // 调用 getValue
    text = "New Value"  // 调用 setValue
}

输出:

Getting value: Default Default Setting value: New Value