Kotlin 面试题及答案大全

这里整理了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协程中的结构化并发是什么?

答案
结构化并发确保协程的生命周期被正确管理,不会泄漏。

原则

  1. 父子关系:父协程等待所有子协程完成
  2. 异常传播:子协程异常会向上传播
  3. 取消传播:父协程取消会取消所有子协程
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的选择依据?"

需要针对某个具体问题深入展开吗?我可以提供更详细的源码级分析。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容