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)
    // 处理结果
}
相关推荐
金士顿13 分钟前
Ethercat耦合器添加的IO导出xml 初始化IO参数
android·xml·java
电饭叔20 分钟前
Luhn算法与信用卡识别完善《python语言程序设计》2018版--第8章14题利用字符串输入作为一个信用卡号之三
android·python·算法
漏洞文库-Web安全20 分钟前
CTFHub-RCE漏洞wp
android·安全·web安全·网络安全·ctf·ctfhub
曹牧20 分钟前
C#:Dictionary类型数组
java·开发语言·c#
享哥。27 分钟前
MVI 模式及mvp,mvvm对比
android
不秃头的帅哥27 分钟前
程序地址空间(基于c++和linxu的一些个人笔记
linux·开发语言·c++·操作系统·内存空间
Dxxyyyy33 分钟前
零基础学JAVA--Day41(IO文件流+IO流原理+InputStream+OutputStream)
java·开发语言·python
独自破碎E40 分钟前
力场重叠问题
java·开发语言·算法
jiuweiC42 分钟前
python 虚拟环境-windows
开发语言·windows·python
前端世界44 分钟前
C 语言项目实践:用指针实现一个“班级成绩智能分析器”
c语言·开发语言