这里整理了Kotlin面试中最高频的题目,从基础到进阶,附带详细答案和代码示例。
🟢 一、基础篇
1. Kotlin和Java有什么区别?
答案要点:
- 空安全:Kotlin在编译时就处理了空指针问题
- 扩展函数:Kotlin可以给现有类添加新方法
- 数据类:一行代码生成POJO类,自动生成getter/setter/toString等
- 协程:轻量级线程,简化异步编程
- 函数式编程:支持Lambda、高阶函数
- 类型推断:可以自动推断变量类型
- 智能类型转换:类型检查后自动转换
// Java
if (obj instanceof String) {
String str = (String) obj;
}
// Kotlin - 智能转换
if (obj is String) {
print(obj.length) // 自动转换为String
}
2. Kotlin中val和var的区别?
答案:
- val:不可变引用,值初始化后不能改变(类似Java的final)
- var:可变引用,值可以改变
val name = "张三"
// name = "李四" // 编译错误
var age = 25
age = 26 // 可以修改
注意:val修饰的对象内部状态可以改变
val list = mutableListOf(1, 2, 3)
list.add(4) // 可以,list指向的对象没变
3. Kotlin中的可空类型和非空类型
答案:
Kotlin通过类型系统消除空指针异常,类型后面加?表示可以为null。
var nonNull: String = "不能为null"
// nonNull = null // 编译错误
var nullable: String? = "可以为null"
nullable = null // 允许
// 安全调用
val length = nullable?.length // 如果nullable为null,结果为null
// Elvis操作符
val len = nullable?.length ?: 0 // 如果为null返回0
// 非空断言(谨慎使用)
val mustLen = nullable!!.length // 如果为null抛出NPE
4. Kotlin中的==和===有什么区别?
答案:
- ==:比较结构相等性(equals方法),类似Java的equals
- ===:比较引用相等性,类似Java的==
val a = "Hello"
val b = String(charArrayOf('H', 'e', 'l', 'l', 'o'))
println(a == b) // true,内容相等
println(a === b) // false,不同对象
5. Kotlin中的lateinit和lazy有什么区别?
答案:
| 特性 | lateinit | lazy |
|---|---|---|
| 适用对象 | var属性(可变) | val属性(不可变) |
| 初始化时机 | 手动指定 | 第一次访问时自动初始化 |
| 线程安全 | 不安全 | 默认线程安全 |
| 适用类型 | 非空类型 | 任何类型 |
| 能否多次赋值 | 能 | 不能 |
// lateinit - 用于依赖注入、单元测试等
lateinit var view: TextView
// 后续必须初始化
view = findViewById(R.id.text_view)
// lazy - 用于耗时初始化
val config: String by lazy {
println("第一次访问时初始化")
loadConfig() // 耗时操作
}
🔵 二、中级篇
6. Kotlin中的data class是什么?有什么特点?
答案:
数据类专门用于持有数据的类,用data关键字修饰。
自动生成的方法:
-
equals()/hashCode() -
toString()格式如 "User(name=张三, age=25)" -
copy()函数 - 解构函数
componentN()
data class User(val name: String, val age: Int)
val user1 = User("张三", 25)
val user2 = user1.copy(age = 26) // 复制并修改年龄
println(user1) // 输出: User(name=张三, age=25)
// 解构
val (name, age) = user1
println("$name $age")
要求:
- 主构造函数至少有一个参数
- 主构造函数参数必须用val/var标记
- 不能是抽象、开放、密封或内部类
7. Kotlin中的伴生对象是什么?
答案:
伴生对象(companion object)是Kotlin中实现静态成员的替代方案。
class MyClass {
companion object {
const val TAG = "MyClass"
fun create(): MyClass = MyClass()
}
}
// 调用方式
MyClass.TAG
MyClass.create()
特点:
- 一个类只能有一个伴生对象
- 伴生对象的名字可以省略(默认名为Companion)
- 编译后会变成真正的静态成员(使用@JvmStatic注解)
- 伴生对象可以实现接口
8. Kotlin中的扩展函数是什么?原理是什么?
答案:
扩展函数可以在不修改原类的情况下给类添加新方法。
// 给String类添加扩展函数
fun String.addExclamation(): String {
return this + "!"
}
// 给Context添加扩展
fun Context.showToast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
// 使用
println("Hello".addExclamation()) // "Hello!"
showToast("操作成功")
原理:
- 扩展函数是静态解析的,不是真正修改了原类
- 编译后会生成静态方法,第一个参数是接收者类型
- 不能访问私有成员
- 如果成员函数和扩展函数同名,成员函数优先级高
9. Kotlin中的高阶函数和Lambda
答案:
高阶函数是指参数或返回值是函数的函数。
// 高阶函数示例
fun <T> List<T>.customFilter(predicate: (T) -> Boolean): List<T> {
val result = mutableListOf<T>()
for (item in this) {
if (predicate(item)) {
result.add(item)
}
}
return result
}
// Lambda表达式
val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.customFilter { it % 2 == 0 }
// 常用高阶函数
list.filter { it > 3 }
.map { it * 2 }
.sorted()
.take(3)
常见高阶函数:
-
filter:过滤 -
map:映射转换 -
flatMap:扁平化映射 -
fold/reduce:折叠 -
forEach:遍历 -
groupBy:分组
10. Kotlin中的sealed class有什么用?
答案:
密封类(sealed class)用于表示受限的类层次结构,是一种特殊的抽象类。
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result()
}
fun handleResult(result: Result) = when (result) {
is Result.Success -> println("成功: ${result.data}")
is Result.Error -> println("错误: ${result.message}")
Result.Loading -> println("加载中")
// 不需要else分支,编译器知道所有情况
}
特点:
- 所有子类必须在同一文件中声明
- when表达式处理时不需要else分支
- 常用于表示状态、错误处理等场景
- Kotlin 1.5后支持密封接口(sealed interface)
🔴 三、高级篇
11. Kotlin协程和线程的区别?
答案:
| 特性 | 协程 | 线程 |
|---|---|---|
| 调度 | 用户态调度,由编译器支持 | 内核态调度,由操作系统管理 |
| 资源消耗 | 轻量级,百万级没问题 | 重量级,几千个就吃力 |
| 切换成本 | 极低 | 高(上下文切换) |
| 代码风格 | 顺序式编写异步代码 | 回调或复杂同步 |
| 取消 | 支持结构化取消 | 较难安全取消 |
// 协程示例
lifecycleScope.launch {
val data = withContext(Dispatchers.IO) {
fetchFromNetwork() // 自动切线程
}
textView.text = data // 自动切回主线程
}
核心概念:
- 挂起函数:suspend关键字,不会阻塞线程
- 调度器:Dispatchers.Main/IO/Default
- 作用域:viewModelScope、lifecycleScope
- 结构化并发:取消父协程会自动取消所有子协程
12. Kotlin中的挂起函数是如何工作的?
答案:
挂起函数是Kotlin协程的核心,它在不阻塞线程的情况下暂停执行。
原理:
- 编译器将挂起函数转换为状态机
- 每个挂起点是一个状态
- 通过Continuation传递控制权
// 看似简单的挂起函数
suspend fun fetchData(): String {
val data1 = fetchFromNetwork() // 挂起点1
val data2 = processData(data1) // 挂起点2
return data2
}
// 编译后类似(简化版)
fun fetchData(continuation: Continuation): Any {
// 状态机实现
when (continuation.label) {
0 -> {
// 第一次调用
continuation.label = 1
fetchFromNetwork(continuation)
return COROUTINE_SUSPENDED
}
1 -> {
// 从挂起点1恢复
val data1 = continuation.result
continuation.label = 2
processData(data1, continuation)
return COROUTINE_SUSPENDED
}
2 -> {
// 从挂起点2恢复
val data2 = continuation.result
return data2
}
else -> throw IllegalStateException()
}
}
13. Kotlin Flow和LiveData的区别?
答案:
| 特性 | Flow | LiveData |
|---|---|---|
| 设计目标 | 响应式流处理 | UI状态持有 |
| 线程切换 | 内置flowOn/launchIn | 需手动处理 |
| 操作符 | 丰富(map、filter等) | 较少 |
| 背压 | 支持 | 不支持 |
| 生命周期感知 | 需配合repeatOnLifecycle | 自动感知 |
| 数据发射 | 冷流 | 热 |
// Flow - 适合复杂数据流
fun getUsers(): Flow<List<User>> = flow {
val users = database.getUsers() // 数据库查询
emit(users)
}.flowOn(Dispatchers.IO) // 指定上游线程
// 收集Flow
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.users.collect { users ->
// 更新UI
}
}
}
// LiveData - 适合简单UI状态
val userLiveData: LiveData<User> = liveData {
emit(loadUser())
}
14. Kotlin中的inline、noinline、crossinline有什么区别?
答案:
inline:
- 内联函数,编译时将函数体复制到调用处
- 减少Lambda对象创建,提高性能
- 可以return外层函数
inline fun inlineTest(block: () -> Unit) {
block()
println("内联函数")
}
fun main() {
inlineTest {
println("Lambda")
return // 允许,会退出main函数
}
}
noinline:
- 用于不想内联的参数
- 当有多个Lambda参数,只想内联部分时使用
inline fun test(
inlined: () -> Unit,
noinline notInlined: () -> Unit // 这个参数不内联
) {
// ...
}
crossinline:
- 禁止Lambda中的非局部返回
- 用于Lambda在另一个执行上下文中执行的场景
inline fun crossinlineTest(crossinline block: () -> Unit) {
Runnable {
block() // 在另一个线程执行
// block中的return不允许,否则会破坏调用栈
}.run()
}
15. Kotlin中的委托属性是怎么实现的?
答案:
委托属性通过by关键字将属性的get/set委托给另一个对象。
// 自定义委托
class StringDelegate {
private var value: String = ""
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("获取属性 ${property.name} 的值")
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
println("设置属性 ${property.name} 的值为 $newValue")
value = newValue
}
}
class MyClass {
var name: String by StringDelegate()
}
标准委托:
- lazy:延迟初始化
- observable:观察属性变化
- vetoable:可否决的观察
- notNull:非空委托
- map:从Map中获取属性
// observable示例
var age: Int by Delegates.observable(0) { prop, old, new ->
println("年龄从 $old 变为 $new")
}
// map委托
class User(val map: Map<String, Any>) {
val name: String by map
val age: Int by map
}
16. Kotlin协程中的结构化并发是什么?
答案:
结构化并发确保协程的生命周期被正确管理,不会泄漏。
原则:
- 父子关系:父协程等待所有子协程完成
- 异常传播:子协程异常会向上传播
- 取消传播:父协程取消会取消所有子协程
viewModelScope.launch {
// 父协程
launch {
// 子协程1
delay(1000)
println("子任务1完成")
}
launch {
// 子协程2
delay(2000)
println("子任务2完成")
}
// 父协程会等待两个子协程都完成
println("所有任务完成")
}
// 异常处理
viewModelScope.launch {
try {
coroutineScope { // 作用域构建器
launch {
throw Exception("子协程异常")
}
}
} catch (e: Exception) {
// 能捕获到异常
println("捕获异常: $e")
}
}
17. Kotlin中的reified关键字有什么用?
答案:
reified用于内联函数中,在运行时获取泛型类型信息。
// 普通泛型函数
fun <T> getType() {
// T.class // 编译错误,无法获取类型
}
// 使用reified
inline fun <reified T> getGenericType() {
println(T::class.java) // 可以获取类型
}
// 实用示例
inline fun <reified T> Gson.fromJson(json: String): T {
return fromJson(json, T::class.java)
}
// Activity启动
inline fun <reified T : Activity> Context.startActivity() {
val intent = Intent(this, T::class.java)
startActivity(intent)
}
// 使用
startActivity<DetailActivity>()
18. Kotlin中的协程异常处理策略
答案:
异常传播机制:
- launch:自动抛出异常,立即崩溃
- async:等待await时抛出异常
// launch异常处理
viewModelScope.launch {
try {
doWork()
} catch (e: Exception) {
println("捕获异常: $e")
}
}
// 全局异常处理
val handler = CoroutineExceptionHandler { _, exception ->
println("协程异常: $exception")
}
viewModelScope.launch(handler) {
throw Exception("出错啦")
}
// SupervisorJob - 不向上传播异常
val supervisor = SupervisorJob()
val scope = CoroutineScope(supervisor)
scope.launch {
throw Exception("子协程异常")
}
scope.launch {
delay(1000)
println("这个协程还会执行")
}
// supervisorScope
viewModelScope.launch {
supervisorScope {
launch {
throw Exception() // 不会影响其他子协程
}
launch {
delay(1000)
println("仍然会执行")
}
}
}
19. Kotlin Flow的操作符分类
答案:
// 1. 构建操作符
flow { emit(1) } // flow构建器
flowOf(1, 2, 3) // 从固定值
list.asFlow() // 集合转Flow
// 2. 中间操作符
flow.map { it * 2 } // 映射
flow.filter { it > 5 } // 过滤
flow.transform { emit(it) } // 转换
flow.take(3) // 取前N个
flow.drop(2) // 跳过前N个
flow.distinctUntilChanged() // 去重连续重复值
// 3. 线程操作符
flow.flowOn(Dispatchers.IO) // 指定上游线程
// 4. 缓冲与背压
flow.buffer() // 缓冲
flow.conflate() // 只保留最新值
flow.collectLatest { } // 只处理最新值
// 5. 组合操作符
flow1.combine(flow2) { a, b -> a + b } // 组合
flow1.zip(flow2) { a, b -> a + b } // 配对
flow.flatMapConcat { } // 扁平化
// 6. 末端操作符
flow.collect { } // 收集
flow.toList() // 转List
flow.single() // 确保只有一个值
flow.first() // 取第一个
20. Kotlin中实现单例的几种方式
答案:
// 1. object声明(最简单)
object AppConfig {
const val BASE_URL = "https://api.example.com"
fun log(message: String) {
println(message)
}
}
// 2. 伴生对象
class AppManager private constructor() {
companion object {
@Volatile
private var instance: AppManager? = null
fun getInstance(): AppManager {
return instance ?: synchronized(this) {
instance ?: AppManager().also { instance = it }
}
}
}
}
// 3. 懒加载委托
class DatabaseHelper private constructor() {
companion object {
val instance: DatabaseHelper by lazy {
DatabaseHelper()
}
}
}
// 4. 依赖注入(Dagger/Hilt)
@Singleton
class UserRepository @Inject constructor() {
// 由DI容器管理单例
}
📝 面试答题技巧
1. STAR法则回答项目问题
- Situation:项目背景
- Task:任务目标
- Action:具体行动(用了Kotlin的什么特性)
- Result:成果(性能提升、代码量减少等)
2. Kotlin相关问题加分点
- 提到Kotlin与Java的互操作性
- 强调Kotlin如何解决Java痛点(空安全、代码冗长)
- 结合实际项目经验
- 关注最新版本特性(Kotlin 1.8/1.9新功能)
3. 常见反问问题
- "你们项目Kotlin化程度如何?"
- "遇到Kotlin版本升级兼容性问题吗?"
- "协程和RxJava的选择依据?"
需要针对某个具体问题深入展开吗?我可以提供更详细的源码级分析。