Kotlin面试题
目录1.launch启动协程能获取返回值吗如何取消2.object class和伴生类有什么区别3.lateinit定义的变量可以为null吗4.data class在MVVM中的作用是什么5.Java中NonNull注解的方法在kotlin中调用有什么问题吗6.现在kotlin中还用DataBinding吗有哪些优缺点7.MVVM中为什么使用MutableStateFlow而不是用MutableLiveData8.kotlin中如何A类有一个方法b如果给A扩展一个方法也叫b调用A.b时调用的是哪个方法9.写一个登录页面10.fragment写法和activity中ViewBinding的使用有什么区别11.密封类和枚举的区别是什么12.什么是密封类MVVM中为什么使用密封类13.MVVM中是如何做到View只更新UIViewModel只操作数据的14.单例模式的写法有哪些15.委托的用法1. 类委托接口委托2. 属性委托超级重要1.launch启动协程能获取返回值吗如何取消不能获取返回值只能获取Job对象1.拿到 Job 对象调用 cancel()2.作用域取消lifecycleScope/viewModelScope 页面销毁自动取消3.内部可以用 isActive 判断是否已取消主动终止逻辑2.object class和伴生类有什么区别1object是天然单例饿汉式直接 类名.方法() 调用无需实例独立整体适合工具类、全局管理器全局唯一生命周期跟随 APP2companion object依附普通类存在不能单独脱离类相当于 Java 静态成员一个类只能有一个伴生对象可以访问类的私有成员一句话区别object独立单例工具类companion object挂靠在普通类里的 “静态域”3.lateinit定义的变量可以为null吗不可以lateinit var 只能修饰非空类型不能加 ?不能是基本类型Int/Long 不行只能用包装类必须在使用前手动初始化否则抛 UninitializedPropertyAccessException只能用在 var不能用 val4.data class在MVVM中的作用是什么在 MVVM 中data class 主要用来定义网络实体、UI 状态类依靠自动生成的 equals、hashCode、copy、解构 特性实现不可变状态更新、精准 UI 刷新、避免重复渲染、简化状态管理是 MVVM 单向数据流最佳数据载体。二、在 MVVM 里具体 5 大作用1. 作为网络请求响应实体 Bean接口返回数据直接用 data class 接收kotlindata class UserBean(val id: Int,val name: String,val avatar: String)ViewModel 拿到后丢给 View不用自己重写 equals、打印日志也方便。2. 作为UI 状态实体StateMVVM 单向数据流常用UIStatekotlinsealed class UiState {data class Loading(val show: Boolean) : UiState()data class SuccessT(val data: T) : UiState()data class Error(val msg: String) : UiState()}密封类 data class 完美分装加载 / 成功 / 失败状态View 观察 State 自动刷新 UI3. 借助 copy() 实现不可变数据、局部更新状态MVVM 推荐数据不可变不修改原对象用 copy 生成新对象kotlindata class HomeUiState(val loading: Boolean false,val list: ListString emptyList(),val error: String )// 只改一个字段其他复用val newState oldState.copy(loading true)LiveData/StateFlow 只感知对象变化配合 copy 精准触发刷新。4. 自动 equals避免重复刷新 UI普通 class 即使内容一样也是不同对象会重复触发观察回调。data class 自动按字段值比较内容没变 → 不触发 UI 刷新内容变了 → 才刷新大幅减少无效重组、冗余更新尤其 Compose / DataBinding 收益巨大。5. 解构赋值简化 View 取值kotlinval (id, name) userBeanView 层取值更简洁不用反复写 .xxx。为什么 MVVM 不用普通 class普通 class 需要手动重写 equals、hashCode、toString模板代码多没有自带 copy做不可变状态很麻烦引用比较导致频繁无效 UI 刷新没有解构代码冗余5.Java中NonNull注解的方法在kotlin中调用有什么问题吗这个注解不是强制的可以返回nullkotlin会信以为真结果可能就崩溃了6.现在kotlin中还用DataBinding吗有哪些优缺点现在新项目基本不用 DataBinding老项目还在用但在逐步迁移到 ViewBinding 或 Compose一、现状现在还用不用新项目2024–2026几乎不用。官方和大厂主流是ViewBinding ViewModel Flow或直接Compose。老项目大量还在跑尤其 MVVM 老项目但团队普遍在逐步移除 / 局部替换。官方态度不再推荐新项目使用AndroidX 8.0 已标记为 “不再积极开发”只修 bug。二、DataBinding 是什么一句话能在 XML 里写表达式把 ViewModel / 数据直接绑定到 XML 控件支持单向绑定{}和双向绑定{}自动更新 UI。三、优点为什么曾经火彻底干掉 findViewById、大量 setText/setVisibilityActivity/Fragment 很干净。原生支持 MVVM数据驱动 UILiveData/StateFlow 一变XML 自动刷新。双向绑定表单登录 / 注册极方便EditText 输入直接回写数据。空安全、类型安全编译期检查减少空指针。0 反射编译期生成代码性能好于 findViewById。四、缺点为什么现在不火XML 里写逻辑难读难调试表达式写在 XML报错晦涩堆栈不直观。复杂逻辑if/else、字符串处理放 XML维护噩梦。编译慢、构建卡要解析 XML 表达式、生成大量中间类编译速度明显变慢。改个 XML 经常要全量编译大型项目很痛苦。BindingAdapter 隐式、难追踪自定义属性如加载图片靠注解IDE 跳转差新人看不懂。逻辑分散在 XML 和注解方法排查问题困难。包体积 / 方法数增加空项目开启后类 120、方法 9k混淆后约 3k对超小包敏感项目不友好。与现代写法冲突和Compose不兼容和ViewBinding功能重叠但更重。现在用ViewModel Flow ViewBinding也能实现干净 MVVM还没 XML 逻辑。7.MVVM中为什么使用MutableStateFlow而不是用MutableLiveData1. StateFlow 是 Kotlin 原生LiveData 是 Android 专用MutableLiveData属于 Android Jetpack只能在 Android 项目用MutableStateFlow属于 Kotlin 协程跨平台可用Android、iOS、后端都能用 现在 Google 官方推荐协程 Flow 架构一统天下2. StateFlow 支持粘性 / 非粘性LiveData 只能粘性LiveData 天生粘性页面旋转、重新进入都会自动收到最后一次数据有时会造成重复刷新、重复弹窗、重复请求StateFlow 默认粘性但可以轻松改成非粘性用 SharedFlow / 自定义包装 Flow 更灵活LiveData 死板3. StateFlow 支持线程切换、强大操作符LiveData 只有postValuesetValuemapswitchMapFlow 拥有filter、map、flatMapConcat、zip、combine、debounce、distinctUntilChanged...完美处理复杂数据流 ViewModel 处理业务逻辑Flow 吊打 LiveData4. StateFlow 是不可变状态最佳搭档MVVM 标准MVVM 推荐UI 状态不可变 → 用 copy () 生成新状态 → 发射新对象StateFlow 天然适合kotlin_uiState.emit( oldState.copy(loading false) )因为它严格感知新对象。LiveData 也能用但没有 Flow 那么契合不可变架构。5. StateFlow 类型安全强制初始化不能为空StateFlow 必须给初始值不会出现 null 意外崩溃LiveData 默认可空容易产生 NPE StateFlow 更安全、更严谨6. 生命周期安全一样能做到LiveData 有的生命周期安全StateFlow repeatOnLifecycle /lifecycle.repeatOnLifecycle完全一样安全甚至更精准kotlinlifecycleScope.launch {repeatOnLifecycle(STARTED) {viewModel.uiState.collect { }}}一句话总结MutableStateFlow 是 Kotlin 原生、跨平台、类型安全、操作符强大、适合不可变 UI 状态的现代化数据流MutableLiveData 是 Android 专用、功能有限、类型不安全、死板的老方案。现代 MVVM 推荐用 MutableStateFlow 替代 LiveData。ViewModel 中 StateFlow / SharedFlow 标准万能模板直接复制就能用项目、面试都标准遵循私有 Mutable、对外只读、UI 状态用 StateFlow、单次事件用 SharedFlow8.kotlin中如何A类有一个方法b如果给A扩展一个方法也叫b调用A.b时调用的是哪个方法优先调用类本身成员方法 扩展方法9.写一个登录页面MVVM 登录页面·全套标准 Kotlin 写法密封类请求状态 页面聚合UI状态 实体BeanAndroid 项目通用、面试满分、实战规范1. 接口返回实体 DataBeankotlin// 接口原始数据data class LoginBean(val userId: Int,val userName: String,val token: String)2. 请求状态密封类 UiStatekotlin// 请求单次状态加载/成功/失败sealed class LoginUiState {object Loading : LoginUiState()data class Success(val data: LoginBean) : LoginUiState()data class Error(val msg: String) : LoginUiState()}3. 页面整体UI聚合状态 Data类kotlin// 页面所有UI变量打包data class LoginViewState(val loginLoading: Boolean false,val loginErrorMsg: String ,val loginSuccess: Boolean false)4. ViewModel 核心逻辑kotlinclass LoginViewModel : ViewModel() {// 页面UI状态唯一对外Flowprivate val _loginViewState MutableStateFlow(LoginViewState())val loginViewState: StateFlowLoginViewState _loginViewState// 登录请求fun login(account: String, pwd: String) {viewModelScope.launch {// 1. 状态改为加载中_loginViewState.value _loginViewState.value.copy(loginLoading true)// 2. 网络请求 - 返回密封类UiStateval state requestLogin(account, pwd)// 3. 根据密封类结果更新页面UI状态when (state) {LoginUiState.Loading - Unitis LoginUiState.Success - {_loginViewState.value LoginViewState(loginLoading false,loginSuccess true)}is LoginUiState.Error - {_loginViewState.value LoginViewState(loginLoading false,loginErrorMsg state.msg)}}}}// 模拟网络接口private suspend fun requestLogin(account: String, pwd: String): LoginUiState {delay(1000)return if (account.isNotBlank() pwd.isNotBlank()) {LoginUiState.Success(LoginBean(1, account, token_123456))} else {LoginUiState.Error(账号密码不能为空)}}}5. Activity/Fragment 观察UIkotlinclass LoginActivity : AppCompatActivity() {private val vm by viewModelsLoginViewModel()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_login)// 监听页面UI状态lifecycleScope.launch {vm.loginViewState.collect { state -// 更新Loading弹窗if (state.loginLoading) {showLoading()} else {dismissLoading()}// 错误提示if (state.loginErrorMsg.isNotEmpty()) {Toast.makeText(thisLoginActivity, state.loginErrorMsg, Toast.LENGTH_SHORT).show()}// 登录成功跳转if (state.loginSuccess) {startMainActivity()}}}// 点击登录按钮btn_login.setOnClickListener {vm.login(et_account.text.toString(), et_pwd.text.toString())}}}10.fragment写法和activity中ViewBinding的使用有什么区别一、核心根源所有区别的源头Activity生命周期 和 视图生命周期 完全绑定视图创建后不会中途销毁直到页面关闭。FragmentFragment 实例生命周期 和 View 视图生命周期 是分离的Fragment 对象可以被缓存、常驻但它的 View 会频繁销毁、重建切换 Tab、ViewPager、回退栈重建等。二、写法上 4 个关键区别1. 变量定义方式不同Activity可以直接用lateinitkotlinprivate lateinit var binding: ActivityMainBinding一次性赋值全程不用置空安全不泄漏。Fragment必须可空变量 只读代理kotlinprivate var _binding: FragmentHomeBinding? null private val binding get() _binding!!_binding可空视图销毁能手动置空只读binding业务端免判空直接用get() 就是给属性自定义一个「取值方法」每次访问这个属性都会执行后面的代码。2. 初始化时机不同Activity在onCreate里一次性初始化kotlinbinding ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root)Fragment在onCreateView里 inflatekotlin_binding FragmentHomeBinding.inflate(inflater, container, false) return binding.root每次视图重建都会重新 inflate 新 binding。3. 是否需要手动置空最大区别Activity不用手动置空跟着 Activity 一起销毁就行。Fragment必须在onDestroyView手动置空kotlinoverride fun onDestroyView() { super.onDestroyView() _binding null }目的切断 View 引用防止内存泄漏。如果不置空Fragment 实例还活着但 View 已经销毁binding 持有旧控件引用 → 内存泄漏。4. 复用与重建行为不同Activity视图只创建一次binding 全局唯一。FragmentView 会多次销毁重建每次都会生成新的 binding 对象靠get()实时取_binding最新值不会拿到旧引用。11.密封类和枚举的区别是什么1. 最核心区别① 枚举enum每个类型不能带独立数据所有实例都是单例适合性别、状态、类型、简单分类② 密封类sealed class每个子类可以带不同数据每个子类可以是data class / object适合UI 事件、网络结果、页面状态、多形态数据2. 代码对比一看就懂枚举不能带个性化数据kotlinenum class Event { SHOW_TOAST, // 不能带 msg NAVIGATE // 不能带参数 }你没法让SHOW_TOAST携带一个字符串消息。密封类每个事件可以带不同数据kotlinsealed class HomeEvent { data class ShowToast(val msg: String) : HomeEvent() // 带字符串 object NavigateNext : HomeEvent() // 不带数据 data class JumpToDetail(val id: Int) : HomeEvent() // 带Int }✅ 每个事件结构不同、数据不同3. 什么时候用枚举状态少不需要携带数据每个类型是固定常量例kotlinenum class LoadState { LOADING, SUCCESS, ERROR }4. 什么时候用密封类UI 事件ViewModel → View 通知网络请求结果成功 / 失败 / 加载每个类型需要携带不同数据类型结构不一样这就是为什么MVVM 中 UI 事件必须用 sealed class不能用枚举5. 面试满分总结密封类 vs 枚举枚举是单例、不能携带不同数据密封类可以有多个子类每个子类可携带不同数据枚举适合简单分类密封类适合复杂事件 / 状态MVVM 中UI 事件必须用密封类因为每个事件要带不同参数。12.什么是密封类MVVM中为什么使用密封类什么是 Kotlin 密封类sealed class用最简单、大白话 语法 作用 场景一次性讲透。1. 字面定义kotlinsealed class 类名密封类是一种「受限继承」的特殊父类特点Kotlin 1.5 及以前只能写在同一个文件里密封类的所有子类必须和sealed class在同一个 Kotlin 文件中不能跑到别的文件。Kotlin 1.7 版本开始可以不在同一个文件但必须在同一个模块、同一个包下外部不能再随便新建子类继承它子类数量固定、可控、不能无限扩展2. 最通俗理解密封类 只能有固定几个子类的 “封闭家族”家门关死了外面不能再进新成员。普通类谁都可以随便继承密封类只有我文件里写好的这些能继承外人不许插队。3. 基本写法kotlinsealed class Result { data class SuccessT(val data: T) : Result() data class Error(val msg: String) : Result() object Loading : Result() }整个Result只有三种情况成功 / 失败 / 加载中不能再新增别的子类。4. 密封类三大核心特性继承受限子类只能在同一个 Kotlin 文件中定义外部无法再继承。when 表达式自动全覆盖不用写 elsekotlinfun handle(res: Result) { when(res) { is Result.Success - {} is Result.Error - {} Result.Loading - {} // 不用写 else编译器帮你检查全部分支 } }如果你漏写一个分支直接编译报错。子类形态灵活子类可以是data class能带参数object单例无参数普通class这是枚举做不到的。MVVM中为什么使用密封类一、核心原因只有 2 句面试必背限制类型让事件类型固定、有限、可枚举when 表达式不用写 else编译器自动检查全覆盖二、不用密封类会怎样如果不用 sealed用普通 classkotlinclass ShowToast(val msg:String) class NavigateNext那你在 View 里接收事件时kotlinwhen(event) { is ShowToast - ... is NavigateNext - ... // 必须加 else不然报错 else - {} }缺点容易漏写事件新增事件不知道编译器不提醒不安全、不规范用了 sealed class →完美解决kotlinwhen(event) { is HomeEvent.ShowToast - showToast() HomeEvent.NavigateNext - navigate() } // ✅ 不用写 else // ✅ 新增事件编译器直接报错提醒你补上 // ✅ 绝对不会漏处理三、为什么 MVVM 一定要用密封类写事件因为 ViewModel 不能操作 View所以 ViewModel 只能发送事件指令密封类就是最标准、最安全的指令清单ViewModel 只管发kotlin_event.emit(HomeEvent.ShowToast(加载成功))View 只管接收并执行 UIkotlinwhen(event) { is ShowToast - showToast(event.msg) NavigateNext - startActivity(...) }13.MVVM中是如何做到View只更新UIViewModel只操作数据的MVVM 通过发布订阅模式借助 LiveData/StateFlow 作为中间桥梁View 只订阅数据变化、负责 UI 渲染和用户事件转发ViewModel 不持有任何 View 引用只处理业务逻辑、修改数据状态依靠单向数据流和可观察组件实现 View 与 ViewModel 完全解耦做到视图只管 UI、逻辑层只管数据。14.单例模式的写法有哪些一、饿汉式object SingleTon { }二、懒汉式不带参数最佳class SingleTon private constructor(){ private object Holder { val instance SingleTon() } companion object { val instance get() Holder.instance } }三、懒汉式不带参数次佳class SingleTon private constructor(){ companion object { val instance by lazy { SingleTon() } } }四、懒汉式带参数class SingleTon private constructor(){ companion object { Volatile private var instance: SingleTon? null fun getInstance(): SingleTon { if (instance null) { synchronized(SingleTon::class.java) { if (instance null) { instance SingleTon() } } } return instance!! } } }15.委托的用法委托 自己不干活把事情交给别人干。Kotlin 用by关键字实现。一、Kotlin 委托分 2 种1.类委托最常用接口委托2.属性委托最常用by lazy / 自定义委托1. 类委托接口委托类委托就是不用重写接口所有方法直接「借别人的实现」避免代码重复、替代静态代理。一、先回忆传统写法不用类委托有个接口kotlininterface ISpeak { fun hello() fun bye() fun eat() }已有一个完整实现类kotlinclass Person : ISpeak { override fun hello() println(hello) override fun bye() println(bye) override fun eat() println(eat) }现在我要写一个Student 类也要实现ISpeak功能和 Person 一模一样。不用类委托的痛点你必须在 Student 里把三个方法全部重写一遍内部再调用 person 的方法kotlinclass Student(private val p: Person) : ISpeak { override fun hello() p.hello() override fun bye() p.bye() override fun eat() p.eat() }接口方法越多重复模板代码越多纯体力活、毫无意义。二、用类委托一行搞定kotlinclass Student(p: Person) : ISpeak by p完了不用写任何 override 方法编译器自动帮你把所有接口方法全部委托给p去实现。三、类委托核心意义4 点面试直接背1. 消灭模板代码不用手动重写接口一堆方法编译器自动帮你转发调用。2. 复用已有实现已有类已经把接口逻辑写好了新类直接借用不用重复造轮子。3. 完美替代 静态代理模式Java 静态代理要手写大量转发方法Kotlin 类委托一行完成代理底层编译器自动生成代理代码。4. 遵循组合优于继承不用强行继承父类来复用代码用组合 委托更灵活、无继承耦合。四、进阶只想重写个别方法委托默认全部借用你只需要重写想改的那几个就行kotlinclass Student(p: Person) : ISpeak by p { // 只覆盖这一个其余全部自动委托 override fun hello() { println(student hello) } }非常灵活。五、一句话总结类委托的意义类委托就是 Kotlin 帮你自动生成接口方法的转发代码省去手写冗余重写用组合替代继承极简实现静态代理复用现有接口实现。六、适用场景接口方法多不想全部手动重写做包装类、装饰器模式静态代理简化复用已有实现不想用继承2. 属性委托超级重要Kotlin 最强大、最常用的就是属性委托。格式kotlinval/var 变量名 by 委托者① 最常用by lazy懒加载kotlinval name: String by lazy { 我是懒加载第一次调用才初始化 }特点只初始化一次线程安全只读 val② observable 监听属性变化kotlinvar age by Delegates.observable(0) { _, old, new - println(年龄从 $old 变成 $new) } age 18 // 自动触发监听③ vetoable 拦截赋值可拒绝修改kotlinvar num by Delegates.vetoable(0) { _, old, new - new old // 只有新值更大才允许赋值 }④ 自定义委托高级你可以自己写一个委托控制get和set。kotlinclass MyDelegate { operator fun getValue(thisRef: Any?, property: KProperty*): String { return 我是自定义get } operator fun setValue(thisRef: Any?, property: KProperty*, value: String) { println(设置成 $value) } }使用kotlinvar str by MyDelegate()