Android面试 - Kotlin相关-未完待续

张开发
2026/4/11 20:13:25 15 分钟阅读
Android面试 - Kotlin相关-未完待续
kotlin和java区别特性JavaKotlin优势空安全❌ 需手动检查✅ 编译期检查避免 NullPointerException数据类需写大量样板代码data class一行搞定减少 80% 代码扩展函数❌ 不支持✅ 可为现有类添加方法增强可读性协程回调/Thread/RxJava原生协程支持简化异步编程类型推断部分支持var全面支持代码更简洁函数式编程有限支持Stream API原生支持高阶函数更声明式的代码智能转换需显式类型转换自动智能转换is减少 cast 代码语法简洁性、空安全机制、数据类和对象表达式/声明、扩展函数和委托属性、Lambda表达式和集合操作、函数式编程支持和互操作性上‌Kotlin作为现代语言更注重开发效率和代码安全性而Java凭借成熟生态仍是企业级开发的主力。‌① ‌代码简洁性‌Kotlin通过类型推断、数据类data class、扩展函数、默认参数与方法重载等特性减少模板代码。例如数据类可自动生成equals()、hashCode()等方法而Java需手动编写。Java需要显式类型声明和分号类定义更冗长如POJO类需编写构造函数和getter/setter方法‌② 空安全机制‌声明可空类型安全调用操作符 ?.Elvis 操作符 ?:非空断言 !!慎用安全转换 as?Java默认允许空值需依赖Optional类或手动检查空指针易引发崩溃③ 数据类和对象表达式/声明Java在Java中创建一个简单的数据载体类通常需要编写大量的样板代码如getter、setter、equals()、hashCode()和toString()方法等。KotlinKotlin引入了数据类data class的概念只需一行代码即可自动生成这些常用的方法。此外Kotlin还支持对象表达式和对象声明允许开发者以更简单的方式创建单例对象或匿名对象。④ 扩展函数和委托属性JavaJava不直接支持扩展函数即无法为现有类添加新方法而不修改其源代码。虽然可以通过装饰器模式等方式实现类似功能但通常较为复杂。KotlinKotlin允许开发者为任何类定义扩展函数这使得在不修改原有类的情况下增加新功能变得非常简单。同时Kotlin还支持委托属性允许一个类的属性通过另一个对象来实现其功能。⑤ Lambda表达式和集合操作Java从Java 8开始Java引入了Lambda表达式和流StreamAPI以简化集合操作和并行计算。然而由于Java的历史包袱和向后兼容性要求这些特性的使用有时仍然显得不够直观。KotlinKotlin从一开始就全面支持Lambda表达式和高阶函数。它的集合框架也进行了相应的优化使得使用Lambda表达式进行集合操作变得更加简单和自然。‌⑥ 函数式编程支持‌Kotlin原生支持Lambda表达式、高阶函数和协程简化异步编程如协程替代回调嵌套Java需通过Stream APIJava 8实现类似功能但语法复杂度较高kotlin的扩展函数是什么为现有类定义扩展函数 , 可以在不修改原有类的情况下增加类的功能遵循“开放-封闭原则”原理编译后是静态方法第一个参数是接收者实例本质是语法糖不修改原类优先级低于成员函数。// Kotlin fun String.addExclamation(): String { return $this! } // 使用 Hello.addExclamation() // Hello! // 扩展函数编译为静态方法 public static final String addExclamation(String $this$addExclamation) { return $this$addExclamation !; } // 调用处 addExclamation(Hello);Kotlin 中如果类没有被 open 关键字修饰 , 则该类不能被继承 , 如果想要扩展该类 , 可以使用 扩展函数 ;扩展函数可以作用于自定义的类 , 也可以作用于系统自带的类 , 如 String , List 等标准库 API 类。注意扩展属性不能有初始化值扩展属性不能有 fieldval String.lastChar: Char this[length - 1] // ❌var StringBuilder.lastChar: Charget() this[length - 1]set(value) {this[length - 1] value // ✅// 但不能有field value ❌}注意扩展函数不能访问类的私有成员变量注意扩展函数不能继承/重写open class Animal class Dog : Animal() fun Animal.speak() Animal sound fun Dog.speak() Bark fun main() { val animal: Animal Dog() println(animal.speak()) // Animal sound ❌ 不是 Bark }扩展函数 vs 成员函数特性成员函数扩展函数定义位置类内部类外部访问权限可访问 private只能访问 public优先级高低可继承✅❌可重写✅❌给现有类定义扩展属性val 现有类类名.扩展属性名: 扩展属性类型get() {}var 现有类类名.扩展属性名: 扩展属性类型get() {}set() {}扩展属性不能进行初始化 , 必须在 getter 函数中进行初始化 ;val 只读类型变量 扩展属性 必须提供 getter 函数 ;var 可变类型变量 扩展属性 必须提供 getter 和 setter 函数常见的扩展函数有let、also、applyrunwithhttps://blog.csdn.net/2303_79132118/article/details/145539493latetinit vs by lazy的用途和区别https://juejin.cn/post/7385356774445498422对Kotlin来说类的属性需要在或构造函数、或声明时、或init代码块中进行初始化。如果初始化时属性值尚为空那可能就需要将其声明为可空类型?并在后续使用时进行非空判断lateinit和lazy是Kotlin中用于延迟初始化的两种机制主要区别在于初始化时机和适用场景为了免除在这样的初始化延迟的场景中对属性进行大量非空判断Kotlin提出了lateinit这一关键字① lateinit延迟初始化适用场景‌用于类属性初始化适用于必须在构造函数之后才能初始化的非空变量如Android组件初始化、依赖注入等特性‌仅用于var声明的可变变量只能用来修饰类属性不能用来修饰局部变量不能修饰原生类型Int、Double等不支持(因为基本类型的属性在类加载后的准备阶段都会被初始化为默认值)要声明一个Int类型的延迟初始化属性那直接使用Integer类型即可需手动在适当时机如生命周期回调完成初始化访问未初始化变量会抛出ml-search-more[UninitializedPropertyAccessException]{textUninitializedPropertyAccessException}异常② by lazy惰性初始化委托单例lazy()便是延迟属性的工厂方法适用场景‌用于类属性或局部变量适用于高开销初始化如数据库连接、文件读取或条件性初始化场景特性‌仅用于val声明的不可变变量首次访问时执行初始化代码后续直接使用缓存结果支持基本类型如Int默认线程安全LazyThreadSafetyMode.SYNCHRONIZED示例对比‌lateinit适用于可变、生命周期明确的对象需手动控制初始化。by lazy适用于只读、高开销或按需初始化的资源自动处理线程安全属性委托如何一行代码解决by关键字可进行① 类委托(即接口委托)② 属性委托(自定义属性委托)③ 资源复用与解耦https://juejin.cn/post/7476284371760578587https://cloud.tencent.com/developer/article/2387971如何避免未初始化崩溃① 初始化所有变量② 使用Optional或非空判断③ 使用Kotlin的空安全特性④ 使用Lazily Initialized Properties延迟初始化lateinit、by lazy⑤ 使用Android的StrictMode进行调试和开发阶段检测未初始化对象的使用设置可空类型会有大量问号必须进行可空检查有办法避免吗① 判空后使用② item?.let/apply { }③ Elvis ?:④ 强制非空断言‌通过 !! 操作符强制解除可空限制需谨慎使用可能引发空指针异常空安全速查表componentN 是什么能举个用它的例子吗componentN解构声明val (id, name) User(1, Tom)或者val user1 User(1, Tom, 20)val (id, name, age) user1高阶函数接收函数作为参数或返回函数的函数内联函数的作用和原理减少函数调用开销支持非局部返回允许 reified 类型参数。追问inline 一定提高性能吗✅ 不一定内联会增加代码体积。作用减少函数调用开销支持非局部返回允许 reified 类型参数减少 lambda 对象创建缺点增加代码体积密封类用密封类写个网络请求结果sealed class Resultout T { data class SuccessT(val data: T) : ResultT() data class Error(val msg: String) : ResultNothing() object Loading : ResultNothing() }密封类 vs 枚举特性枚举 (enum)密封类 (sealed class)实例数固定每个枚举值是单例不固定可创建多个实例状态无状态或固定状态可携带状态属性继承不能继承可被子类继承使用场景固定选项状态机、表达式✅ 代码示例// 1. 枚举 - 固定选项 enum class Direction { NORTH, SOUTH, EAST, WEST } // 2. 密封类 - 网络请求结果 sealed class Resultout T { data class SuccessT(val data: T) : ResultT() data class Error(val message: String, val code: Int) : ResultNothing() object Loading : ResultNothing() } // 使用密封类 fun handleResult(result: ResultString) { when (result) { is Result.Success - println(成功: ${result.data}) is Result.Error - println(错误: ${result.message}) Result.Loading - println(加载中...) // 不需要 else编译器知道已穷尽 } } // 3. 枚举带状态 enum class Color(val rgb: Int) { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF) }协程协程是轻量级的任务调度单元跑在线程上的轻量级任务可以在单线程中并发执行多个任务支持挂起和恢复。用同步的方式写异步代码彻底解决了回调地狱问题。协程的核心是挂起和恢复机制通过状态机和** Continuation 传递**在不阻塞线程的情况下实现异步执行底层是回调的语法糖Kotlin 协程通过 CPS 转换将挂起函数编译为状态机每个挂起点对应一个状态。Continuation 保存执行状态调度器决定执行线程。挂起时不阻塞线程恢复时从挂起点继续执行实现同步写法的异步性能。关键点协程不是轻量级线程而是挂起函数的执行框架挂起函数会被编译器改写添加状态机逻辑Continuation 是回调的包装用于恢复执行调度器决定协程在哪个线程运行协程的切换成本极低可以在用户态完成协程实现的三层架构┌─────────────────────┐│ 用户层 (Suspend Function) ││ suspend fun fetchUser() │└────────────────┬────┘↓┌────────────────────┐│ 编译器层 (CPS 变换) ││ Continuation 传递 状态机 │└──────┬─────────────┘↓┌────────────────────┐│ 运行时层 (Dispatcher) ││ 线程调度 挂起/恢复逻辑 │└────────────────────┘1. 调用 suspend 函数2. 创建 Continuation状态机实例3. 执行到挂起点delay/withContext4. 保存状态返回 COROUTINE_SUSPENDED5. 线程执行其他任务6. 挂起条件满足时间到/I/O完成7. 调用 continuation.resume()8. 恢复执行跳转到对应 label 核心三要素1️⃣挂起函数 (Suspend Function)使用suspend关键字标记的函数可以在不阻塞线程的情况下挂起执行。挂起函数的特点可以在不阻塞线程的情况下挂起执行只能在协程或其他挂起函数中调用挂起时保存执行状态恢复时继续执行协程 suspend 原理通过 CPSContinuation Passing Style转换将挂起函数转换为状态机CPS 转换编译器将挂起函数转换为状态机Continuation保存挂起点和恢复点保存和恢复执行状态状态机每个挂起点是一个状态管理挂起点和恢复点什么是 CPS将函数的返回结果改为通过回调参数传递Continuation 的作用保存挂起点位置label保存局部变量提供恢复方法2️⃣协程作用域 (CoroutineScope)管理协程的生命周期所有协程必须在作用域内启动。// 1. GlobalScope - 全局不推荐 GlobalScope.launch { } // 2. 自定义作用域 val scope CoroutineScope(Job() Dispatchers.Main) // 3. Android 内置 viewModelScope.launch { } // ViewModel lifecycleScope.launch { } // LifecycleOwner3️⃣调度器 (Dispatcher 4 种调度器)决定协程在哪个线程上执行。launch(Dispatchers.Default) { // CPU 密集型计算、任务 // 线程池大小 CPU 核心数 } launch(Dispatchers.IO) { // IO 密集型适合网络、数据库操作 // 线程池更大64个 } launch(Dispatchers.Main) { // 主线程Android // 更新 UI } launch(Dispatchers.Unconfined) { // 不指定线程继承上下文 // 调试用生产不用不推荐用 }3️⃣ 协程构建器创建协程的三种方式// 1. launch - 不关心结果 val job launch { delay(1000) println(Done) } // 2. async - 需要结果 val deferred async { delay(1000) 42 } val result deferred.await() // 3. runBlocking - 阻塞当前线程仅用于测试或 main 函数 runBlocking { delay(1000) // 阻塞线程 }协程是线程么协程不是线程而是跑在线程上的轻量级任务。线程由操作系统调度协程由程序控制。一个线程可以运行成千上万个协程协程挂起时不阻塞线程launch 和 async 区别特性launchasync返回值JobDeferredT异常处理立即传播不传播直到 await()使用场景不需要结果的后台任务需要结果的计算任务取消可取消可取消结构化并发Structured Concurrency✅ 核心原则父协程取消 → 所有子协程取消✅ SupervisorJob一个失败其他继续 协程的异常处理1️⃣try-catch 捕获异常2️⃣使用 CoroutineExceptionHandler3️⃣supervisorScope- 子协程失败不影响其他协程协程性能优化✅ 选择合适的调度器✅ 并发执行 vs 顺序执行// 顺序慢 val time1 measureTimeMillis { val one async { doSomethingUsefulOne() }.await() // 立即 await val two async { doSomethingUsefulTwo() }.await() // 立即 await one two } // 并发快 val time2 measureTimeMillis { val one async { doSomethingUsefulOne() } val two async { doSomethingUsefulTwo() } one.await() two.await() } 高级模式1️⃣使用 Mutex 保护共享状态2️⃣Channel 用于协程间通信追问withContext 和 async 区别// 顺序执行 val a withContext(Dispatchers.IO) { getA() } val b withContext(Dispatchers.IO) { getB() } // 并发 val aDeferred async { getA() } val bDeferred async { getB() } val a aDeferred.await() val b bDeferred.await()如何避免协程内存泄漏viewModelScope.launch { ... } lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { flow.collect { ... } } }delay() 和 Thread.sleep() 区别✅ delay() 挂起协程释放线程sleep() 阻塞线程。flowjetpack相关库使用lifecycle viewmodel roomstate follow viewmodel的回调

更多文章