Kotlin面试题总结

下面是我面试过程中遇到的题,记录总结,我会持续更新

目录

空安全机制

[val和 var的区别?lateinit var和 by lazy的区别?](#val和 var的区别?lateinit var和 by lazy的区别?)

扩展函数与属性

[数据类(Data Class)](#数据类(Data Class))

[什么是密封类(Sealed Class)?它的使用场景?](#什么是密封类(Sealed Class)?它的使用场景?)

let,also,with,run,apply的对比区别

核心区别与使用场景

[1. 引用对象的方式:it还是 this?](#1. 引用对象的方式:it还是 this?)

[2. 返回值:对象本身还是操作结果?](#2. 返回值:对象本身还是操作结果?)

如何选择与实用技巧

协程(Coroutines)

协程创建的几种方式

关键概念与组件

[挂起函数(Suspend Function)](#挂起函数(Suspend Function))

协程作用域(CoroutineScope)

调度器(Dispatcher)

协程构建器

高频面试题与答案

简单说明Kotlin协程是什么,以及它的主要特点。

[协程中的 launch和 async构建器有什么区别?](#协程中的 launch和 async构建器有什么区别?)

[解释 withContext函数的作用,并与线程切换进行对比。](#解释 withContext函数的作用,并与线程切换进行对比。)

什么是结构化并发?它在Android开发中有何重要性?

在协程中,如何处理异常?

如何实现多个网络请求的并行执行?


空安全机制

这是Kotlin最显著的特性之一。它将类型系统分为可空类型 (如 String?)和非空类型 (如 String),在编译期就强制开发者处理潜在的空值问题。

  • 安全调用操作符(?. :当对象不为null时执行操作,否则返回null。例如:user?.address?.city

  • Elvis操作符(?: :提供空值情况下的默认值。例如:val name = nullableName ?: "Unknown"

  • 非空断言(!! :明确告诉编译器该对象不为空,如果为null则抛出NullPointerException。应谨慎使用。

valvar的区别?lateinit varby lazy的区别?

  • val声明只读变量(类似Java的final),var声明可变变量。lateinit var用于延迟初始化非空变量,不能用于原始类型,且必须在使用前初始化。by lazy是用于val的惰性初始化,线程安全,首次访问时执行初始化代码

扩展函数与属性

允许你为已存在的类(包括第三方库的类)添加新的函数或属性,而无需通过继承或修改其源代码。这极大地提升了代码的表现力和可维护性。

Kotlin 复制代码
// 扩展函数示例:为String类添加一个新函数
fun String.addExclamation(): String = this + "!"

// 使用
"Hello".addExclamation() // 结果为 "Hello!"

数据类(Data Class)

使用 data关键字标记的类,编译器会自动为其生成 equals(), hashCode(), toString(), copy()等标准方法。这专门用于持有数据的类,能大幅减少模板代码。

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

val user1 = User("Alice", 30)
val user2 = user1.copy(age = 31) // 使用copy函数创建一个新对象

什么是密封类(Sealed Class)?它的使用场景?

  • 密封类表示一种受限的继承结构,其所有子类必须在同一文件中声明。它常与when表达式结合使用,用于完美地表达如网络请求结果、UI状态等场景,因为编译器能检查when语句是否覆盖了所有情况,避免遗漏

let,also,with,run,apply的对比区别

在 Kotlin 中,let, also, with, run, 和 apply这五个作用域函数确实容易让人混淆。它们的核心目标都是在某个对象的上下文中执行一段代码块,从而让代码更简洁清晰。它们之间的主要区别在于引用对象的方式返回值类型

下面这个表格可以帮你快速把握它们的核心区别。

函数 上下文对象引用 返回值 是否是扩展函数
let it Lambda 表达式结果
also it 上下文对象本身
run this Lambda 表达式结果
with this Lambda 表达式结果
apply this 上下文对象本身

核心区别与使用场景

1. 引用对象的方式:it还是 this

这是选择函数时的一个重要考量,主要影响代码的书写方式和可读性。

  • 使用 this的函数:run, with, apply

    在代码块内,上下文对象作为 Lambda 接收者 (this) 。你可以直接访问对象的属性和方法,无需显式地写出 this.,使得代码非常简洁,尤其在需要对对象进行多次配置时。

    Kotlin 复制代码
    val list = mutableListOf<String>().apply {
        // `this` 是 MutableList,可以省略
        add("Item1") // 相当于 this.add("Item1")
        add("Item2")
    }
  • 使用 it的函数:let, also

    在代码块内,上下文对象作为 Lambda 的参数 (it) 。当你需要显式地使用对象名时(例如,对象本身作为参数传递给其他函数,或需要避免与外部作用域的 this混淆时),这种方式会更清晰。

    Kotlin 复制代码
    val result = "Hello".let { greeting ->
        // 可以给 `it` 重命名,提高可读性
        println(greeting.length)
        greeting.uppercase() // 返回值是 lambda 的最后一行
    }
2. 返回值:对象本身还是操作结果?

这决定了你能否进行链式调用,以及整个表达式的值是什么。

  • 返回上下文对象本身的函数:apply, also

    它们返回的是调用函数对象本身。这非常适用于 链式调用,让你能在配置对象后继续对其进行操作。

    applyalso都返回对象本身,但根据引用方式的不同,适用场景略有差异:

    • apply :通常用于对象的初始化或配置。在代码块内,你可以直接配置对象的属性。

      Kotlin 复制代码
      val paint = Paint().apply {
          color = Color.RED // 直接访问属性
          strokeWidth = 5f
          style = Paint.Style.FILL
      } // paint 是配置好的 Paint 对象
    • also :通常用于执行一些附加效应,如打印日志、数据验证等。它不改变对象本身,但允许你在链式调用中"偷看"一眼对象的状态。

      Kotlin 复制代码
      val list = mutableListOf("A", "B", "C")
          .also { println("列表在添加元素前是:$it") } // 打印日志
          .apply { add("D") } // 修改列表
  • 返回 Lambda 表达式结果的函数:let, run, with

    它们返回的是代码块中最后一行 的值。这适用于需要对对象进行转换或计算并得到一个新结果的场景。

    • let :非常适合配合安全调用操作符(?.)进行空安全检查,或者对调用链的中间结果进行操作。

      Kotlin 复制代码
      val length: Int? = nullableString?.let {
          println("字符串是:$it")
          it.length // 返回值是长度,而不是字符串本身
      }
    • run :结合了 letwith的特点。既可以进行空安全调用(因为是扩展函数),又能在代码块内直接使用 this访问对象。适合需要计算一个返回值的场景。

      Kotlin 复制代码
      val result = someObject?.run {
          doSomething() // 直接调用 someObject 的方法
          computeFinalValue() // 返回值是计算结果
      }
    • with :它不是扩展函数,因此需要一个对象作为参数传入。通常用于对一个已知非空的对象进行一系列操作,并返回一个结果。

      Kotlin 复制代码
      val builder = StringBuilder()
      val text = with(builder) {
          append("Hello")
          append(" ")
          append("World")
          toString() // 返回值是构建好的字符串
      }

如何选择与实用技巧

  1. 实用速记口诀

    • 初始化或配置 对象?用 apply

    • 进行空安全检查转换 对象?用 let

    • 计算一个结果 ?用 runwith

    • 在调用链中执行额外操作 (如日志)?用 also

    • 对象非空 ,且要集中操作 ?用 with

  2. 避免过度嵌套

    虽然这些函数可以链式调用,但过度嵌套会降低可读性。如果逻辑变得复杂,考虑使用传统的条件语句或临时变量。

协程(Coroutines)

协程是Kotlin用于异步编程的轻量级解决方案。你可以将其理解为一种更高效、更简洁的"线程"。

  • 轻量:一个线程中可以运行成千上万个协程,切换开销远小于线程。

  • 简化异步代码:以看似同步的方式编写异步代码,有效避免"回调地狱"。

  • 结构化并发:提供了更好的生命周期管理,避免任务泄漏。

协程创建的几种方式

协程创建方式及其核心特点,以及它们的主要区别和适用场景如下:

创建方式 核心特点 典型用例 关键说明
launch "即发即忘" :启动一个不直接返回结果的新协程,返回 Job对象用于管理协程生命周期 。 执行不需要返回值的后台任务,例如日志上传、本地数据预处理 。 属于最常用的协程启动方式 。
async 返回结果 :启动一个会返回结果的协程,返回 Deferred<T>对象(继承自 Job),可通过 await()获取结果 。 执行需要获取结果的异步计算,如并发进行多个网络请求 。 多个 async协程默认是并行 执行的;await()是挂起函数 。
withContext 不创新协程,切换上下文:是一个挂起函数,不创建新的协程,而是将协程的上下文切换到指定的调度器上执行代码块,并返回结果 。 临时切换协程执行的线程,例如在主线程中切换到 IO 线程执行网络请求后再切回主线程更新 UI 。 挂起当前协程,直到代码块执行完毕 。多个 withContext任务默认是串行执行的。
runBlocking 阻塞线程 :是一个普通的函数(非挂起函数),会阻塞当前线程,直到其内部的协程体及所有子协程执行完毕 。 主要用于 main函数、测试代码或需要在非协程环境中以阻塞方式运行协程代码的场景 。 应避免在 Android 等生产环境的主线程中使用,以免导致界面无响应 。
coroutineScope 挂起函数,创建作用域 :是一个挂起函数,会创建一个新的协程作用域,会挂起当前协程,并等待其内部所有子协程执行完毕后才会继续执行 。 在挂起函数中需要并发执行多个任务,并且需要等待所有任务完成后再继续的场景,适用于"结构化并发" 。 runBlocking的关键区别在于 coroutineScope只会挂起当前协程,而不会阻塞底层线程
supervisorScope 监督作用域 :与 coroutineScope类似,但有一个重要区别:在此作用域下,一个子协程的失败不会导致其他子协程的取消 适用于子任务相对独立,即使其中一个失败也不希望影响其他任务执行的场景 。 提供了更灵活的错误处理机制 。
Android 特定作用域 (如 viewModelScope, lifecycleScope) 生命周期绑定 :与 Android 架构组件(如 ViewModelLifecycle)的生命周期绑定 。 在 Android 开发中,用于执行与 UI 控制器或 ViewModel生命周期相关的异步任务 。 当关联的 ViewModel被清除或 Lifecycle被销毁时,在此作用域内启动的协程会自动取消,有效避免内存泄漏

关键概念与组件

挂起函数(Suspend Function)

使用 suspend关键字标记,代表该函数可以"挂起"协程的执行而不阻塞线程。它只能在协程或其他挂起函数中调用。典型的做法是使用 withContext来指定函数执行的线程,确保主线程安全。

复制代码
   suspend fun fetchUserData(): User {
       // 通过 withContext 确保主线程安全
       return withContext(Dispatchers.IO) {
           // 模拟网络请求等耗时操作
           networkService.getUser()
       }
   }
协程作用域(CoroutineScope)

它是协程运行的上下文,负责管理其生命周期。在Android开发中,强烈推荐使用以下内置作用域:

  • viewModelScope : 在 ViewModel中使用,当 ViewModel清除时自动取消所有子协程。

  • lifecycleScope : 在 ActivityFragment中使用,绑定组件的生命周期。

调度器(Dispatcher)

它决定了协程在哪个线程池中执行。常用的调度器有:

  • Dispatchers.Main: 用于更新UI。

  • Dispatchers.IO: 适用于网络、数据库等I/O密集型操作。

  • Dispatchers.Default: 适用于CPU密集型计算任务。

协程构建器
  • launch : 启动一个不直接返回结果的协程,返回一个 Job对象用于控制协程生命周期。适用于"一劳永逸"的任务。

  • async : 启动一个可以返回结果的协程,返回一个 Deferred对象。通过调用 await()来获取结果。常用于并行任务。

高频面试题与答案

简单说明Kotlin协程是什么,以及它的主要特点。

参考答案 : Kotlin协程是一种轻量级的并发设计模式,通过挂起和恢复机制简化异步编程。它的主要特点包括:极高的并发效率(单线程可运行大量协程)、内置的结构化并发支持(避免内存泄漏)、简洁的同步代码风格、以及灵活安全的线程调度能力。
2.

协程中的 launchasync构建器有什么区别?

参考答案 : 它们的核心区别在于返回值和使用场景。launch用于执行不需要返回结果的异步任务,返回 Job对象。async用于需要返回结果的异步任务,返回 Deferred对象(它是 Job的子类),可以通过 await()方法获取结果。当 async内部发生异常时,异常会被封装在 Deferred对象中,直到调用 await()时才会抛出。
3.

解释 withContext函数的作用,并与线程切换进行对比。

参考答案withContext是一个挂起函数,它用于临时切换协程执行的上下文(通常是线程)。它会挂起当前协程,在指定的新调度器(如 Dispatchers.IO)上执行代码块,完成后自动切回原来的上下文。与传统的线程切换(如 new Thread().run())相比,withContext不会阻塞原始线程,并且与协程的结构化并发生命周期无缝集成,更加安全和高效。
4.

什么是结构化并发?它在Android开发中有何重要性?

参考答案 : 结构化并发是指协程的生命周期严格受其作用域(CoroutineScope)约束的理念。当父协程或作用域被取消时,其内部的所有子协程也会被自动取消。在Android开发中,这至关重要,因为它能有效防止因生命周期组件(如Activity、ViewModel)销毁后,后台协程继续运行而导致的内存泄漏 。例如,使用 viewModelScope可确保ViewModel清除时网络请求自动取消。
5.

在协程中,如何处理异常?

参考答案: 协程的异常处理方式取决于构建器:

  • 对于 launch启动的协程,异常会立即抛出,因此可以使用传统的 try-catch块在内部捕获。

  • 对于 async启动的协程,异常不会立即抛出,而是被延迟到调用 await()方法时。因此,try-catch应该包裹 await()调用。

  • 还可以使用 CoroutineExceptionHandler作为全局的异常捕获备用方案。

如何实现多个网络请求的并行执行?

参考答案 : 可以使用多个 async构建器来同时启动多个协程,然后通过 awaitAll()函数等待所有请求完成。

Kotlin 复制代码
viewModelScope.launch {
    val deferred1 = async { repository.getData1() }
    val deferred2 = async { repository.getData2() }
    // 等待所有请求完成
    val results = awaitAll(deferred1, deferred2)
    // 处理结果
}
相关推荐
froginwe111 小时前
RSS 语法:全面解析与优化指南
开发语言
美摄科技1 小时前
android短视频sdk,灵活集成,快速上线!
android·音视频
佳哥的技术分享1 小时前
图形化android可视化开机观测工具bootchart
android
杨筱毅1 小时前
【底层机制】 Android ION内存分配器深度解析
android·底层机制
abner.Li2 小时前
基于AOSP11创建一个能用emulator启动的android tv产品
android
小飞大王6662 小时前
JavaScript基础知识总结(六)模块化规范
开发语言·javascript·ecmascript
qk学算法2 小时前
Collections工具类
java·开发语言
缺点内向2 小时前
Java: 为PDF批量添加图片水印实用指南
java·开发语言·pdf