kotlin常见面试题

  1. Kotlin 空安全怎么实现??.、?:、!!、lateinit、let 分别是什么?

Kotlin 在类型层区分可空 / 不可空类型,编译期校验,从源头减少 NPE

T:不可空类型,不能赋值 null

T?:可空类型,允许 null

?. 安全调用:对象为 null 时整个表达式返回 null,不执行后面

?: Elvis 艾尔维斯运算符:左边为 null 时,返回右边兜底值

!! 非空断言:强制认为不为 null,null 时抛出 KotlinNullPointerException,项目尽量少用

lateinit var:延迟初始化,var、非空、基本类型不能用;访问前必须赋值,否则抛异常;适合 View、DI 注入

let:作用域函数,?.let{} 仅在非空时执行 lambda,it 代表对象本身

补充:Kotlin 只有几种情况会出现 NPE:!!、Java 互操作返回 null、外部 JVM 代码传入 null、手动 throw NPE。

  1. 协程是什么?和线程的区别?suspend 挂起函数原理?

协程是轻量级、可挂起的计算体,不是线程,运行在线程池之上;由 kotlinx-coroutines 库 + 编译器状态机实现,不是操作系统内核概念

线程:OS 内核资源,栈默认 MB 级,切换开销大;抢占式调度

协程:用户态,开销极小,几万协程可以跑在少量线程上;协作式调度,suspend 点主动让出线程,不阻塞线程

suspend:编译器标记,把函数编译成带Continuation参数的状态机;挂起时保存局部状态、返回标记COROUTINE_SUSPENDED;恢复时从上次状态继续执行

限制:suspend 函数只能在 suspend 函数 / 协程 builder 内部调用

  1. run、apply、let、with、also 作用域函数区别?

let/also 用 it;run/apply/with 用 this;apply/also 返回自身,其余返回 lambda 结果。这里说的lambda结果意思是lambda 大括号里面最后一行表达式的值。

  1. data class 数据类,和普通 class 区别?

data class 编译器自动生成:

(1)equals()、hashCode():按主构造参数比较

(2)toString():打印所有参数

(3)copy():拷贝对象,支持部分参数修改

(4)componentN():支持解构声明 val(a,b)=user

限制:

主构造至少 1 个参数;不能是 abstract、sealed、inner;自动生成只看主构造参数,类内属性不参与 equals/hashCode

  1. 扩展函数 / 扩展属性是什么?原理?能不能访问私有成员?成员函数和扩展同名谁优先?

扩展函数本质是静态工具方法,第一个参数是接收者类型,语法糖,没有真正修改原类源码

(1)不能访问 private、protected 成员

(2)静态分发:成员函数优先级 > 扩展函数;编译时按声明类型解析,不是运行时多态

(3)扩展属性:没有幕后字段,必须提供 get/set,不能存持久状态。

幕后字段(backing field)就是 field,对应 Java 里的成员变量,一块真实存在的内存空间,可以长期存数据。 扩展属性没有这个 field,所以它没有地方可以持久保存值。

(4)适用:工具方法、给系统类增加能力(Context.toast)

  1. inline、noinline、crossinline 内联函数、高阶函数?

高阶函数:参数 / 返回值包含函数类型;普通写法会生成 Function 对象,有额外对象开销。

inline:编译期把函数体 + lambda 直接拷贝到调用处,消除 lambda 对象分配,提升性能;适合小 lambda 高频调用

noinline:同一个 inline 函数里,不想内联某个 lambda,加 noinline,会生成对象

crossinline:禁止 lambda 内部 return(非局部返回),只能 return@label;适合传给其他异步回调

坑:inline 函数里面直接 return,会直接跳出外层调用函数(非局部返回)

  1. by lazy 委托属性,几种模式?线程安全?

val x by lazy { ... } 懒加载,第一次访问才执行 lambda 求值,之后缓存结果,只能 val

三种模式:

(1)LazyThreadSafetyMode.SYNCHRONIZED【默认】:加锁,线程安全,开销大

(2)PUBLICATION:多线程可同时执行 lambda,先计算完成的结果被缓存,无锁

(3)NONE:无锁、非线程安全,单线程场景最快

和 lateinit 对比:by lazy 是 val、懒加载、线程安全可选;lateinit 是 var、需要手动赋值、不能基础类型。

  1. == 和 === 的区别?

==:结构相等,调用 equals(),Kotlin 会自动做空判断;Java 的 equals

===:引用相等,判断是不是同一个对象,对应 Java ==

注意:数字缓存(-128~127)会影响 === 结果;字符串常量池同理。 即,JVM 对 Integer(装箱 Int) 有缓存池(-128 ~ 127)、字符串有字符串常量池,在这个范围内时,装箱出来会复用同一个对象,所以 === 返回 true;超出范围就新建对象,=== 返回 false。

9、Kotlin 可见性修饰符?和 Java 区别?

public(默认):全部可见

private:本类内

protected:本类 + 子类

internal:同模块(module)内可见,Java 没有这个修饰符;Java 默认是包访问权限(package-private),Kotlin 没有包私有

另外,Kotlin 类、方法默认是 final(不可继承 / 不可重写),需要加 open 才可以继承 / 重写,Java 默认可以继承。

10、sealed class 密封类,什么时候用?和 enum 区别?

密封类:子类受限,只能在同一个文件内定义;when 分支可以穷尽校验,不需要 else,适合状态、结果、事件封装(UI 状态、网络结果)

sealed:子类可以有不同属性、多实例;支持继承

enum:每个枚举是单例,属性固定

11、泛型 in /out/ * 星投影(声明处变型)

Kotlin 是声明处变型,Java 是使用处通配符

out T:协变,生产者,只读,只能输出 T,对应 Java ? extends T,如 List<out T>

in T:逆变,消费者,只写入 T,对应 Java ? super T,如 Comparator<in T>

*:星投影,List<*> 相当于 List<out Any?>,只读,不知道具体类型

12、结构化并发 Structured Concurrency

核心原则:协程生命周期由 Scope 管理,父子 Job 绑定在一起

(1)父不结束,等所有子协程完成

(2)父取消,所有子递归取消(防泄漏,ViewModelScope、lifecycleScope 就是这个目的)

(3)默认 Job:子抛异常,向上传播,父取消其他所有子;

(4)SupervisorJob /supervisorScope:子异常不传播给父,不取消兄弟协程(适合多个独立任务,比如多个并行埋点)

13、launch 和 async 区别?

launch:返回 Job,只执行、无返回值,"发后不管";异常直接向上抛

async:返回 Deferred<T>,用来获取返回结果;异常先保存在 Deferred,调用 await () 时才抛出

14、Flow、StateFlow、SharedFlow 区别?

(1)Flow:冷流,只有 collect 收集才开始生产数据;每次 collect 独立重新执行;适合单次数据流(网络请求、本地查询)

(2)StateFlow:热流,有初始值,保存最新状态;新订阅者立刻拿到最新值;自动去重相同值;适合 UI 状态(页面数据、加载状态),等价于可观察的 LiveData

(3)SharedFlow:热流,无初始值;可配置 replay、缓冲、溢出策略;适合一次性事件(toast、弹窗、导航事件)

冷流:生产者跟着消费者生命周期;热流:生产者独立运行,和 collect 无关。

15、协程取消机制?为什么是协作式取消?

协程取消是协作式,不是强制杀死:

(1)调用 cancel () 后,Job 标记为取消;只有在 suspend 函数(delay、withContext)内部会检查取消状态,抛出 CancellationException

(2)纯 CPU 死循环没有 suspend 点,不会自动停止;需要手动 ensureActive() / yield() 检测

(3)坑:不要 catch(e:Exception) 吞掉 CancellationException,会导致协程无法正常取消、内存泄漏

16、集合 List / MutableList 和 Sequence 区别?

(1)List 链式操作(map、filter):每一步都生成新集合,中间对象开销大,及早求值

(2)Sequence:懒求值,流式,只有最后终端操作(toList、forEach)才真正遍历;中间不生成临时集合;大数据量更省内存;底层是迭代器

17、companion object 伴生对象,和 Java static 区别?@JvmStatic/@JvmField

Kotlin 没有 static 关键字,companion object 是类内单例对象

(1)直接写的方法:Kotlin 里可以 Class.xxx(),Java 调用需要 Class.Companion.xxx()

(2)@JvmStatic:在伴生对象函数上加,Java 侧可以直接 Class.xxx()

(3)@JvmField:去掉 getter/setter,Java 直接访问字段

18、解构声明 componentN ()

val (name,age) = user 就是解构;编译器调用 component1()、component2();data class 自动生成 componentN,普通类自己手动实现 operator componentN。

19、@JvmOverloads 作用

给带默认参数的函数生成多个重载方法,兼容 Java 调用;Java 不支持默认参数,不加 @JvmOverloads 时 Java 只能传全参。

20、Kotlin 与 Java 核心差异

(1)空安全类型系统

(2)扩展函数、data class、密封类

(3)协程原生支持异步

(4)类默认 final,需要 open

(5)没有 checked exception

(6)区分只读集合 / 可变集合

(7)类型推断、if 是表达式、字符串模板

(8)内部类默认不是静态内部类(Java 内部类默认持有外部);Kotlin inner 关键字才是带外部引用的内部类

21、Channel 是什么?和 Flow 区别?背压

Channel

是协程之间的热流、队列,用来协程之间通信(生产者 - 消费者),类似阻塞队列,挂起式的。

  • 热的:创建之后就开始发数据,有没有人接收都可以发
  • 有缓冲区,支持背压:发得快、收得慢时,数据存在 buffer;满了生产者挂起
  • 一对一 / 多对多:SendChannel、ReceiveChannel,可以多个发、多个收
  • 基础 API:send () /receive (),都是挂起函数

Flow

冷流:只有调用 collect 收集的时候,才开始执行、发射数据;不收集就不跑。

  • 每一次 collect,都会重新走一遍上游代码
  • 背压:Flow 默认有背压策略(buffer、conflate、flowOn 等),是面向数据流转换(map/filter/transform),适合 UI 数据流
  • 一般是单订阅(除非 SharedFlow/StateFlow)

一句话总结:协程之间互相发消息用 Channel;UI 订阅数据流、链式转换用 Flow

**22、**flowOn、withContext 线程切换区别

withContext(Dispatcher):切换当前整个协程上下文 ,上下游一起切,是挂起函数。 作用:在协程内部,切换当前这段代码的线程;上下游都受影响

flowOn(Dispatcher):只改变它上游所有 Flow 的分发器,下游不受影响! flowOn 是 Flow 的操作符,不是挂起函数。上游单独跑在指定调度器,下游继续沿用原来的上下文。 可以多次 flowOn,就近生效,只管自己上游。

23、CoroutineDispatcher 四个:Main、IO、Default、Unconfined

  1. Dispatchers.Main:主线程(UI 线程),Android 用来更新 UI;底层是 Handler
  2. Dispatchers.IO:IO 线程池,专门文件读写、网络、数据库、阻塞 IO;线程数量可扩容
  3. Dispatchers.Default:CPU 密集计算(排序、解析、大量循环),线程数≈CPU 核心数
  4. Dispatchers.Unconfined :无约束调度器。
    • 一开始在调用它的线程执行;遇到挂起恢复后,在恢复的那个线程继续跑
    • 不绑定固定线程池,极少用,一般业务不推荐;容易线程乱飞

24、CoroutineExceptionHandler 生效条件

CoroutineExceptionHandler 协程异常处理器

生效规则:

  1. 只对根协程(顶级 launch)生效
  2. 子协程异常会向上传播,父协程的 handler 捕获;子协程自己设置的 handler 无效
  3. async 不会触发 CoroutineExceptionHandler! async 的异常不会自动抛出,异常藏在 Deferred 里面,只有调用 await () 的时候才抛出异常,需要自己 try-catch await
  4. 协程取消(CancellationException)不算异常,不会进这个 handler,是正常取消

补充:SupervisorJob 可以让子协程失败不搞崩兄弟协程

25、suspendCancellableCoroutine 封装回调

作用:把 Java 回调风格的异步 API,转成挂起函数 suspend,是桥接老回调代码和协程的核心工具。 核心要点:

  1. 包在 suspend 函数内部,阻塞(挂起)当前协程,不阻塞线程
  2. cont.resume(value) → 成功返回
  3. cont.resumeWithException(e) → 抛出异常
  4. 必须处理取消! cont.isCancelled,并且注册 cont.invokeOnCancellation 协程取消的时候,要取消底层回调 / 请求,防止回调回来 resume 已经取消的协程(崩溃)

26、SupervisorJob 和 Job 区别?

Job 一个子协程异常,全部子协程取消;SupervisorJob 子协程失败,不影响其他兄弟协程。

27、cancel 是怎么工作的?

协程取消是协作式,不是强制杀线程;需要在挂起点检查 isActive,或者用 yield、delay 等挂起函数才会响应取消。

ok. 整理了这些。有的知识点不熟,还需要研究下。

相关推荐
mmsx25 分钟前
耳切法切带洞多边形:论文能跑通,真实数据为什么崩
android·kotlin
草莓熊Lotso2 小时前
【C++ 基于微服务的即时通讯系统】文件存储子服务实战:UUID生成、文件IO封装与全接口单元测试
开发语言·网络·数据库·c++·微服务·架构·单元测试
Kapaseker2 小时前
Kotlin 又要加语法糖了:这次轮到 if-else
android·kotlin
huaqianzkh3 小时前
WinForm C#:循环修改控件UI闪烁卡顿?SuspendLayout / ResumeLayout 与 BeginUpdate 优化
开发语言·ui·c#
沐晓时光13 小时前
C语言入门,内存函数知识讲解
c语言·开发语言
David猪大卫19 小时前
【C++修炼】map 和 set 的使用
c语言·开发语言·数据结构·c++·经验分享·笔记·学习
fengci.20 小时前
CTF2MISC中等部分
开发语言·前端·学习
xys_6781 天前
JavaEE通关手册:初识Java
java·开发语言