面试题 - Android - Kotlin相关内容

831 阅读3分钟

1. Kotlin 简介及特性

Kotlin 是一门由 JetBrains 开发的现代编程语言,可以在 JVM 上运行。主要特性包括:

  • 空安全(Null Safety)
  • 简洁的语法
  • 与 Java 100% 互操作
  • 支持函数式编程
  • 智能类型推断
  • 协程支持

2. @JvmOverloads 注解

@JvmOverloads 的作用是在有默认参数值的函数中自动生成重载函数。

class Example {
    @JvmOverloads
    fun greet(name: String = "World", greeting: String = "Hello") {
        println("$greeting, $name!")
    }
}

这会生成以下 Java 代码:

class Example {
    public void greet(String name, String greeting) { ... }
    public void greet(String name) { ... }
    public void greet() { ... }
}

3. List 与 MutableList 区别

  • List: 不可变集合,只能读取
  • MutableList: 可变集合,可以进行增删改操作
val readOnlyList: List<String> = listOf("a", "b", "c")
val mutableList: MutableList<String> = mutableListOf("a", "b", "c")

// 只能读取
readOnlyList[0]  // 正确
// readOnlyList.add("d")  // 错误!

// 可以修改
mutableList.add("d")  // 正确

4. Kotlin 实现单例的方式

  1. object 关键字(最简单):
object Singleton {
    fun doSomething() {}
}
  1. 伴生对象:
class Singleton private constructor() {
    companion object {
        @Volatile private var instance: Singleton? = null
        
        fun getInstance(): Singleton =
            instance ?: synchronized(this) {
                instance ?: Singleton().also { instance = it }
            }
    }
}

5. data 关键字

data 类自动生成:

  • equals()/hashCode()
  • toString()
  • componentN()
  • copy()
data class User(val name: String, val age: Int)

6. 委托属性

委托属性允许将属性的 getter/setter 委托给另一个类。

常见使用场景:

  • 懒加载(lazy)
  • 观察者模式(Observable)
  • 存储属性(如 SharedPreferences)
class Example {
    private val heavyObject by lazy {
        // 复杂初始化
        HeavyObject()
    }
}

7. with 与 apply 区别

// with 返回最后一行结果
val result = with(person) {
    println(name)
    age + 1
}

// apply 返回对象本身
val person = Person().apply {
    name = "张三"
    age = 20
}

8. 协程与线程的区别

协程比线程轻量的原因:

  1. 内存占用少:协程只需要几十字节的内存
  2. 上下文切换成本低:协程是用户态的切换
  3. 可以在单线程上运行多个协程

简单的流程图:

graph TD
    A[线程] --> B[系统级]
    A --> C[重量级]
    A --> D[内存占用大]
    
    E[协程] --> F[用户级]
    E --> G[轻量级]
    E --> H[内存占用小]

9. infix 关键字

infix 允许以中缀表达式的形式调用函数。

class Person {
    infix fun likes(thing: String) {
        println("I like $thing")
    }
}

// 使用方式:
val person = Person()
person likes "coding"  // 等同于 person.likes("coding")

10. Kotlin 可见性修饰符

Kotlin 的可见性修饰符:

  • public(默认):所有地方可见
  • private:当前类内可见
  • protected:当前类及子类可见
  • internal:同一模块内可见

与 Java 的主要区别:

  • Kotlin 默认是 public
  • Kotlin 新增 internal
  • Kotlin 的外部类不能访问内部类的 private 成员

11. Kotlin 与 Java 混合开发注意事项

  1. 空安全处理:
// Java 代码返回的类型在 Kotlin 中会被认为是可空的
fun handleJavaMethod(javaString: String?) {
    javaString?.length ?: 0
}
  1. 静态成员访问:
// Java 静态方法在 Kotlin 中的调用
JavaClass.Companion.staticMethod()
  1. SAM 转换注意事项:
// Java 接口
interface OnClickListener {
    void onClick(View view);
}

// Kotlin 调用
view.setOnClickListener { view -> 
    // 处理点击
}

12. Kotlin 解构

解构允许将一个对象的多个属性同时赋值给多个变量:

data class Person(val name: String, val age: Int)

val person = Person("张三", 25)
val (name, age) = person

// 背后的原理是通过 componentN() 函数实现
val name = person.component1()
val age = person.component2()

Kotlin 的解构声明(Destructuring Declarations)底层是通过 componentN() 函数实现的。让我详细解释:

  1. 基本使用
// 数据类自动支持解构
data class User(val name: String, val age: Int)

fun example() {
    val user = User("Tom", 20)
    val (name, age) = user  // 解构声明
    
    // 底层实际调用
    val name = user.component1()
    val age = user.component2()
}
  1. 自定义解构
class Point(val x: Int, val y: Int) {
    // 手动实现 componentN 函数
    operator fun component1() = x
    operator fun component2() = y
}

// 使用
val point = Point(10, 20)
val (x, y) = point
  1. 常见应用场景
// 1. Map 遍历
val map = mapOf("a" to 1, "b" to 2)
for ((key, value) in map) {
    println("$key = $value")
}

// 2. 函数返回多个值
data class Result(val success: Boolean, val data: String)
fun getData(): Result {
    return Result(true, "数据")
}

val (success, data) = getData()

// 3. Lambda 参数
map.forEach { (key, value) ->
    println("$key = $value")
}
  1. 解构原理示例
// 编译前
data class Point(val x: Int, val y: Int)
val (a, b) = Point(1, 2)

// 编译后等价于
data class Point(val x: Int, val y: Int) {
    operator fun component1() = x
    operator fun component2() = y
}
val point = Point(1, 2)
val a = point.component1()
val b = point.component2()
  1. 实际应用
// 1. 处理网络响应
data class ApiResponse<T>(
    val code: Int,
    val message: String,
    val data: T?
)

fun handleResponse() {
    val response = getApiResponse()
    val (code, message, data) = response
    // 处理响应
}

// 2. 状态管理
data class UiState(
    val isLoading: Boolean,
    val data: List<Item>,
    val error: String?
)

fun render(state: UiState) {
    val (isLoading, items, error) = state
    // 更新 UI
}

总结:

  1. 解构的实现方式
  • 通过 componentN() 函数
  • 数据类自动生成
  • 可手动实现
  1. 常用场景
  • Map 遍历
  • 多返回值
  • 状态管理
  • 参数解构
  1. 注意事项
  • 解构顺序要对应
  • 可以忽略部分值用 _
  • 注意性能开销
  1. 最佳实践
  • 适度使用
  • 保持代码清晰
  • 考虑可读性

13. 内联函数

内联函数可以减少 Lambda 表达式的性能开销:

inline fun measureTime(block: () -> Unit) {
    val start = System.currentTimeMillis()
    block()
    println("耗时:${System.currentTimeMillis() - start}ms")
}

内联函数的工作原理图:

graph LR
    A[调用处代码] --> B[编译时]
    B --> C[内联函数体复制到调用处]
    C --> D[避免函数调用开销]

14. Kotlin 构造方法

Kotlin 有主构造函数和次构造函数:

class Person(val name: String) { // 主构造函数
    var age: Int = 0
    
    constructor(name: String, age: Int) : this(name) { // 次构造函数
        this.age = age
    }
    
    init {
        // 初始化代码块
        println("Creating person: $name")
    }
}

15. Sequence

Sequence 是惰性求值的序列,对比即时求值的 List 更高效:

// List 方式(即时求值)
listOf(1, 2, 3, 4)
    .map { it * 2 }    // 创建新列表 [2, 4, 6, 8]
    .filter { it > 5 } // 创建新列表 [6, 8]
    .first()          // 返回 6

// Sequence 方式(惰性求值)
sequenceOf(1, 2, 3, 4)
    .map { it * 2 }    // 不立即执行
    .filter { it > 5 } // 不立即执行
    .first()          // 只处理需要的元素

性能比较图:

graph TD
    A[List操作] --> B[创建多个中间集合]
    B --> C[处理所有元素]
    
    D[Sequence操作] --> E[无中间集合]
    E --> F[按需处理元素]

16. 空安全处理

Kotlin 提供多种空安全处理机制:

// 安全调用操作符
val length = str?.length

// Elvis 操作符
val length = str?.length ?: 0

// 非空断言
val length = str!!.length

// 智能转换
if (str != null) {
    println(str.length) // 编译器知道 str 非空
}

17. Any 与 Object

主要区别:

  • Any 是所有 Kotlin 类的超类
  • Any 只有 equals()、hashCode() 和 toString() 三个方法
  • Any 可以是空类型(Any?)
  • Object 是所有 Java 类的超类

18. 集合遍历方式

val list = listOf(1, 2, 3)

// 1. for 循环
for (item in list) { }

// 2. forEach
list.forEach { }

// 3. 索引遍历
for (i in list.indices) { }

// 4. withIndex
for ((index, value) in list.withIndex()) { }

19. Kotlin 数据类型隐式转换

Kotlin 不支持数据类型的隐式转换,这是为了避免精度损失和运行时错误。

val intNumber: Int = 100
// val longNumber: Long = intNumber  // 编译错误
val longNumber: Long = intNumber.toLong()  // 正确方式

// 数字类型转换方法
val number = 100
number.toByte()
number.toShort()
number.toInt()
number.toLong()
number.toFloat()
number.toDouble()

转换关系图:

graph LR
    A[源类型] -->|显式转换| B[目标类型]
    C[隐式转换] -->|不支持| D[编译错误]

20. 对象表达式与 Lambda 表达式的区别

// 1. 对象表达式
val clickListener = object : View.OnClickListener {
    override fun onClick(v: View?) {
        // 处理点击
    }
}

// 2. Lambda 表达式
val clickListener = { v: View? -> 
    // 处理点击
}

主要区别:

  1. 内存分配:

    • 对象表达式会创建匿名类实例
    • Lambda 表达式通常会被编译器优化,不会创建额外对象
  2. 功能性:

    • 对象表达式可以实现多个接口
    • Lambda 表达式只能实现单个抽象方法

21. 协程的深入理解

协程的基本组件:

// 1. 启动协程
GlobalScope.launch {
    // 协程代码
}

// 2. 使用 suspend 函数
suspend fun fetchData(): Data {
    delay(1000) // 非阻塞延迟
    return Data()
}

// 3. 协程作用域
class MyViewModel : ViewModel() {
    private val viewModelScope = CoroutineScope(Dispatchers.Main)
    
    fun loadData() {
        viewModelScope.launch {
            val data = fetchData()
            // 处理数据
        }
    }
}

协程的生命周期图:

graph TD
    A[协程创建] --> B[协程启动]
    B --> C[运行中]
    C --> D[挂起]
    D --> C
    C --> E[完成]
    C --> F[取消]

22. 协程的上下文和调度器

// 不同调度器的使用
coroutineScope {
    // UI 线程
    launch(Dispatchers.Main) {
        updateUI()
    }
    
    // IO 操作
    launch(Dispatchers.IO) {
        fetchData()
    }
    
    // CPU 密集型操作
    launch(Dispatchers.Default) {
        processData()
    }
}

调度器选择流程:

graph TD
    A[操作类型] --> B{是否UI操作?}
    B -->|是| C[Dispatchers.Main]
    B -->|否| D{是否IO操作?}
    D -->|是| E[Dispatchers.IO]
    D -->|否| F[Dispatchers.Default]

23. 协程的异常处理

// 1. try-catch 方式
launch {
    try {
        // 可能抛出异常的代码
    } catch (e: Exception) {
        // 处理异常
    }
}

// 2. 协程作用域的异常处理
val handler = CoroutineExceptionHandler { _, exception ->
    println("Caught $exception")
}

GlobalScope.launch(handler) {
    throw Exception("Error")
}

// 3. SupervisorJob 方式
val scope = CoroutineScope(SupervisorJob() + handler)

24. 协程与线程的详细对比

特性协程线程
内存占用~几十字节~1MB
切换成本用户态切换,非常轻量系统态切换,较重
并发量可同时运行数十万个受系统资源限制
调度控制完全由程序控制依赖操作系统调度
取消操作支持结构化取消难以安全取消

25. 协程的最佳实践

class Repository {
    // 1. 使用适当的作用域
    private val coroutineScope = CoroutineScope(
        Dispatchers.IO + SupervisorJob()
    )
    
    // 2. 结构化并发
    suspend fun fetchData() = coroutineScope {
        val part1 = async { fetchPart1() }
        val part2 = async { fetchPart2() }
        combineResults(part1.await(), part2.await())
    }
    
    // 3. 优雅的异常处理
    private val handler = CoroutineExceptionHandler { _, e ->
        // 处理异常
    }
}

26.Unit 与 Void 的区别

  1. 类型系统

    • Unit 是一个实际的类,只有一个单例实例
    • Void 是一个不能实例化的类型
  2. 返回值

// Kotlin中的Unit
fun doSomething(): Unit {
    // 不需要显式返回任何值
}

// Java中的void
public void doSomething() {
    // 不能返回值
}
  1. 泛型使用
// Kotlin中可以使用Unit作为泛型参数
interface Callback<T> {
    fun onSuccess(value: T)
    fun onFailure(error: Throwable)
}

// Unit用作泛型参数
val callback = object : Callback<Unit> {
    override fun onSuccess(value: Unit) {
        // 处理成功
    }
    override fun onFailure(error: Throwable) {
        // 处理失败
    }
}
  1. 函数类型
// Unit在函数类型中的使用
val action: () -> Unit = { println("Hello") }

// 等价的Java代码需要使用Void
Runnable runnable = () -> System.out.println("Hello");

主要区别总结:

  • Unit 是一个具体的类型,有一个单例实例
  • Void 不能实例化
  • Unit 可以用在泛型中
  • Unit 在函数式编程中更加灵活
  • Kotlin 中的 Unit 相当于其他函数式编程语言中的 unit 或 void 类型