Kotlin 相关知识点

1 什么是 Kotlin,它有哪些特性?

Kotlin 和 Java 一样是 JVM 编程语言,它们的代码最终都会被编译成 .class 文件(JVM 字节码)。

Kotlin 的特性主要有以下几点:

  • Lambda 表达式,函数式编程: Lambda 表达式并不是 Kotlin 特有的,Java 中也有,但是有限制
    • Java 中的 Lambda 表达式主要用于简化单抽象方法的接口。
      • 这是因为 Java 的系统类型需要明确的类型信息,Lambda 表达式要和特定的函数式接口(仅包含一个抽象方法的接口 SAM Single Abstract Method Interface)类型匹配;
    • Kotlin 中的 Lambda 表达式同样支持单抽象方法的接口,但更推荐使用闭包来实现。
      • 闭包是一个可以捕获其所在上下文变量的代码块,在 Kotlin 中,闭包可以独立存在,不依赖特定的接口;
  • 扩展
    • 扩展属性: 允许开发者为现有的类添加新的属性,不过并不是真正的给类添加成员变量,而是提供了自定义的 getter(val)setter(var) 方法。
      • 语法形式为:val ClassName.属性名: 类型
    • 扩展方法: 允许开发者为现有的类添加新的函数。
      • 语法形式为:fun ClassName.函数名(): 类型
  • 默认参数,减少方法重载。
  • 判空语法:
    • 安全调用操作符 ?.
    • Elvis(埃尔维斯,猫王)操作符 ?:
    • 非空断言操作符 !!

Lambda 表达式:

java 复制代码
// Java 示例:匿名内部类实现 OnClickListener
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // 处理点击事件
    }
});

// 使用 Lambda 表达式简化
button.setOnClickListener(v -> {
    // 处理点击事件
});

// Kotlin 的转换 (OnClickListener 是 Java 定义的接口)
button.setOnClickListener {
  	// 处理点击事件
}

扩展:

kotlin 复制代码
// 扩展属性
val MutableList<Int>.sum: Int
    get() = this.sum()

val numbers = mutableListOf(1, 2, 3, 4, 5)
println(numbers.sum)

// 扩展函数
fun String.reverseString(): String {
    return this.reversed()
}

val str = "Hello"
val reversedStr = str.reverseString()
println(reversedStr)

默认参数:

kotlin 复制代码
// Kotlin 默认参数
fun showToast(
		message: String,
  	duration: Int = Toast.LENGTH_SHORT // 默认值
) {
  	
}

showToast("Hello") // 使用默认 duration
showToast("Hi", Toast.LENGTH_LONG) // 显式指定 duration

判空语法:

kotlin 复制代码
val length: Int? = nullableStr?.length // 若 nullableStr 为 null,length 也为 null
val lengthOrDefault: Int = nullableStr?.length ?:0 // 若为 null,返回 0
val forcedLength: Int = nullableStr!!.length // 慎用!可能引发崩溃

2 Kotlin 中注解 @JvmOverloads 的作用

在有参数默认值的的方法上加上 @JvmOverloads 注解,Kotlin 就会暴露出多个重载方法。

这样 Java 代码就能像调用 Java 重载方法一样调用 Kotlin 函数,从而间接利用默认参数的功能。

kotlin 复制代码
@JvmOverloads
fun foo(a: Int = 0, b: Boolean, c: String = "abc") {
  
}

生成重载方法:

java 复制代码
foo()
foo(a: Int)
foo(a: Int, b: Boolean)
foo(a: Int, b: Boolean, c: String)

3 Kotlin 中的 List 和 MutableList 的区别

Kotlin 将集合分为可变集合(MutableList)和只读集合(List)。

MutableList:可变集合接口,允许对集合中的元素进行添加、删除、修改等操作。

List:只读集合接口(线程安全),一旦创建,其元素的数量和内容都不能被修改。

只读集合与可变集合之间的转变:

kotlin 复制代码
// 只读 -> 可变
val list = listOf(1, 2, 3)
val mutableList = list.toMutableList() // 新对象
mutableList.add(4)


// 可变 -> 只读
val mutable = mutableListOf(1, 2, 3)
val readOnly = mutable.toList()

只读集合可变的情况:

kotlin 复制代码
val writeList: MutableList<Int> = mutableListOf(1, 2, 3, 4)
val readList: List<Int> = writeList
println(readList) // [1, 2, 3, 4]
writeList[0] = 0
println(readList) // [0, 2, 3, 4]

4 Kotlin 中实现单例的几种常见方式

  • 懒汉式,线程安全: 通过 私有构造函数 + companion object (伴生对象)和 lazy 实现懒加载,首次访问才进行初始化;
    • lazy 默认使用 LazyThreadSafetyMode.SYNCHRONIZED([ˈsɪŋkrənaɪzd]),这是 lazy() 委托的一种线程安全模式,用于确保在多线程环境下延迟初始化只执行一次
  • 懒汉式,双重检验锁(Double - Checked Locking): 通过 私有构造函数 + companion object (伴生对象)、volatilesynchronized 实现,首次访问时才进行初始化;
    • 通过 @Volatile([ˈvɒlətaɪl]) 保证变量的可见性(对于变量的写操作会立即刷新到主内存中去),同时 @Volatile 禁止指令重排序;
    • 通过同步代码块(synchronized)来确保线程的安全;
  • 静态内部类: 通过 私有构造函数 + 静态内部类 + companion object (伴生对象)+ by lazy 来实现。静态内部类只有在首次访问时才进行初始化,由 JVM 保证线程安全,确保静态变量 INSTANCE 只被初始化一次;
    • Kotlin 的类加载机制保证线程安全;
  • 饿汉式: 通过 Object 关键字来实现,像"饿汉"一样,在加载时就被创建(立即被创建)。是 Kotlin 中最简洁的单例模式;
    • Kotlin 编译器会保证单例的创建和线程安全;
    • 反编译成 Java 代码:静态变量在静态代码块中初始化
kotlin 复制代码
// 懒汉式,线程安全:私有构造函数 + 伴生对象 + by lazy
class Singleton private constructor() {
    companion object {
        val singleton: Singleton by lazy { // LazyThreadSafetyMode.SYNCHRONIZED
            Singleton()
        }
    }
}

// 懒汉式,双重检验锁:私有构造函数 + 伴生对象 + @Volatile + synchronized
class Singleton private constructor() {
    companion object {
        // 变量可见性 + 防止指令重排
        @Volatile
        private var instance: Singleton? = null

        fun getInstance(): Singleton {
            return instance ?: synchronized(this) {
                return instance ?: Singleton().also { instance = it }
            }
        }
    }
}

// 私有构造函数 + 静态内部类 + 伴生对象 + by lazy:
class Singleton private constructor() {
    companion object {
        // LazyThreadSafetyMode.SYNCHRONIZED 保证线程安全
        val singleton: Singleton by lazy { Holder.INSTANCE }
    }

    // 静态内部类只有在使用的时候才被加载,从而实现延迟初始化
    private object Holder {
      	// 反编译成 Java 代码是静态变量
        val INSTANCE = Singleton()
    }
}

// 饿汉式:Kotlin 编译器会保证线程安全和单例的创建
object Singleton {
    
}

饿汉式反编译成 Java 代码:

java 复制代码
public final class Singleton {
   @NotNull
   public static final Singleton INSTANCE;

   private Singleton() {
   }

   static {
      Singleton var0 = new Singleton();
      INSTANCE = var0;
   }
}

5 谈谈你对 Kotlin 中 data 关键字的理解?相比于普通类有哪些特点?

Kotlin 中 的 data 关键字用于声明数据类(Data Class),专门为简化数据模型而设计的。

特点:

  • 主构造函数必须至少有一个参数,且参数标记为 varval
    • 数据类的核心是属性,而非普通参数。如果不标记为 var/val,参数仅作为构造参数存在,不会成为类的属性,导致:
      • 无法通过 对象.属性名 访问;
      • 编译器无法生成 equals()/hashCode() 等方法;
  • 自动生成标准方法:toString()componentN()equals()hashCode()copy() 等方法;
    • toString():生成格式化的字符串表示,如 User(name=John, age=30)
    • componentN():函数:支持解构声明访问属性;
    • equals()hashCode():比较两个对象的内容是否相等(而非引用相等),仅基于主构造函数中声明的属性;
    • copy() :快速创建对象的副本,并可选择性修改部分属性(适用于不可变独享);
  • 数据类不能是 abstractopensealed (密封类)或 inner
    • 不能是 abstract (抽象类):数据类的主要目的是存储数据,需要能被直接实例化使用;
      • 抽象类不能直接实例化;
      • 数据类自动生成的 copy() 方法依赖具体实现;
      • 自动生成的 componentN() 函数要求属性必须实现;
    • 不能是 open(开放类):数据类的自动生成方法需要不可变性保证;
      • 子类添加新属性会导致 equlas() 行为错误;
      • hashCode() 在不同子类间可能产生冲突;
      • copy() 方法无法正确处理子类属性;
    • 不能是 sealed(密封类):密封类要求有子类,而数据类要求不能有子类
    • 不能是 inner(内部类):内部类隐式持有外部类引用;
      • 自动生成的 equals() 会包含外部类引用比较;
      • hashCode() 会包含不可控的外部信息类;
      • copy() 方法无法正确的处理外部类的绑定;

6 什么是委托属性?请简要说说其使用场景和原理?

在 Kotlin 中,委托是一种强大的设计模式,通过 by 关键字来实现,它允许对象将部分职责交给另一个辅助对象来完成。

Kotlin 支持的两种委托为:类委托(Class Delegation [ˌdelɪˈɡeɪʃn])和属性委托(Property Delegation)。

  • 类委托: 允许一个类将其公共接口的实现委托给另一个对象。

    • 类似于继承,但更灵活,因为它是通过组合来实现的,避免了继承的一些限制(比如单继承问题)。遵循"组合优于继承"的原则;(多重继承:多个父类中有相同的方法或属性,导致冲突)
    • 当我们希望一个类实现某个接口,但不想自己实现所有的方法,而是想重用另一个类已有的实现时;
  • 属性委托: 允许经属性的访问器(getter/setter)逻辑委托给另一对象。这样可以将属性的读取和写入操作交给另一个辅助对象来处理,实现逻辑复用;

    • 只读属性(val):提供 getValue()

    • 可变属性(var):提供 getValue()setValue()

    • 使用场景:

      • 惰性加载(lazy properties):属性第一次访问时才计算初始值;
      • 可观察属性(observable properties):属性变化时触发通知;
      • 属性存储在映射(map)中:适用于动态配置的属性,如 JSON 解析

类委托:

kotlin 复制代码
interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { println(x) }
}

// 委托给 BaseImpl 实例
class Derived(base: Base) : Base by base

// 使用
val base = BaseImpl(10)
val derived = Derived(base)
derived.print()  // 输出 10(实际由 BaseImpl 实现)

Derived 类将 Base 接口的实现委托给 base 对象,编译器自动生成转换方法,无需手动实现接口。

属性委托:

kotlin 复制代码
class Example {
    // 委托给 LazyImpl 实例
    val lazyValue: String by LazyImpl()
}

// 委托类需实现 ReadOnlyProperty 接口(val 属性)
class LazyImpl : ReadOnlyProperty<Example, String> {
    override fun getValue(thisRef: Example, property: KProperty<*>): String {
        return "计算结果"
    }
}

标准库中的委托:

kotlin 复制代码
// Lazy:延迟初始化
val heavyResource: Resource by lazy {
    println("Initializing...")
    Resource.load() // 首次访问时执行
}

// 使用
fun main() {
    println("Before access")
    heavyResource.use() // 此时初始化
    heavyResource.use() // 直接使用缓存
}

// observable:属性变更监听
var value: Int by Delegates.observable(0) { 
    prop, old, new ->
    println("${prop.name} changed: $old -> $new")
}

// 使用
fun main() {
    value = 10  // 输出: value changed: 0 -> 10
    value = 20  // 输出: value changed: 10 -> 20
}

// Map 委托:将属性存储到 Map 中,适用于动态属性场景(如 JSON 解析)
class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

// 使用
fun main() {
    val user = User(mapOf(
        "name" to "Alice",
        "age"  to 25
    ))
    println(user.name) // Alice
    println(user.age)  // 25
}

7 请举例说明 Kotlin 中的 with 函数和 apply 函数的应用场景和区别?

8 Kotlin 中 Unit 类型的作用以及与 Java 中 void 的区别

  • Kotlin 中的 Unit:和 Int 一样,Unit 是一种数据类型,表示无返回值类型;
    • Kotlin 中引入 Unit 类型的很大原因是函数式编程;
    • 在 Kotlin 中,对象或函数都有类型,如果方法的返回类型是 Unit,可以省略;
  • Java 中的 void:是关键字,表示什么都不返回,void 不能省略;
    • 这是 Java 中不能说函数调用都是表达式的原因,因为有些方法不具有返回值或类型信息,所以不能算作是表达式;
  • Kotlin 中的 Nothing:表示这个函数永不返回;
    • 对于某些 Kotlin 函数来说,"返回类型"的概念没有任何意义,因为它们永远不会成功的结束。
kotlin 复制代码
fun main() {
    fail("Error occurred")
}

fun fail(message: String): Nothing {
    throw java.lang.IllegalStateException(message)
}

// Exception in thread "main" java.lang.IllegalStateException: Error occurred
// 	at com.ixuea.test.TestKt.fail(Test.kt:158)
// 	at com.ixuea.test.TestKt.main(Test.kt:152)
// 	at com.ixuea.test.TestKt.main(Test.kt)

9 Kotlin 中 infix 关键字的原理和使用场景

在 Kotlin 中,用 infix 关键字修饰的函数称为中缀函数。使用时可以省略 .()。让代码看起来更自然(类似自然语言)。

普通函数与中缀函数的语法:

  • 普通函数:a.function(b)
  • 中缀函数:a function b

示例:

kotlin 复制代码
infix fun String.concatWith(another: String) = "$this$another"

// 链式中缀调用
val message = "Hello" concatWith "World" concatWith "!"

定义一个中缀函数,必须满足以下条件:

  • 该中缀函数必须是某个类的扩展函数或成员方法;
  • 该中缀函数只能有一个参数;
  • 该中缀函数的参数不能有默认值(否则,以上形式的 b 会缺失,从而对中缀表达式的语义造成破坏);

标准库中的中缀函数:

  • to 函数:用于创建 Pair 对象;
  • until 函数:用于生成区间;
  • 集合操作,如 stepdownTo;
kotlin 复制代码
// 源码
infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

val map = mapOf(
    "name" to "Eileen",
    "age" to 30
)

中缀函数的底层实现(原理): Kotlin 编译器在语法层面给了支持,反编译成 Java 代码后就可以看到是普通函数。(Kotlin 的很多特性都是在语法和编译器上的优化)

示例:

kotlin 复制代码
class Person(private val name: String) {
    // 成员中缀函数
    infix fun say(message: String) {
        println("$name says $message")
    }
}

// 扩展中缀函数
infix fun Int.multiply(factor: Int): Int = this * factor

反编译中 Java 代码

java 复制代码
public final class Person {
   private final String name;

   public final void say(@NotNull String message) {
      Intrinsics.checkNotNullParameter(message, "message");
      String var2 = this.name + " say " + message;
      System.out.println(var2);
   }

   public Person(@NotNull String name) {
      Intrinsics.checkNotNullParameter(name, "name");
      super();
      this.name = name;
   }
}

10 Kotlin 中的可见修饰符有哪些?相比于 Java 有什么区别?

11 你觉得 Kotlin 和 Java 混合开发时需要注意哪些问题?

  • 空安全: Kotlin 有严格的空安全机制,如非空类型 String 和 可空类型 String?,但 Java 没有
    • Kotlin 代码调用 Java 代码时:Java 中的所有引用类型都认为是可空的,Kotlin 需要进行空判断,如使用安全调用操作符(?.)、Elvis 操作符(?:)、非空断言(!!)。
    • Java 在调用 Kotlin 代码时:可能会传入 null 到非空参数,因此,需要为调用的 Kotlin 方法参数添加 @Nullable@NotNull 注解
  • 默认参数和方法重载: Kotlin 支持默认参数,但 Java 是不支持的,Java 调用带默认参数的 Kotlin 方法时必须传递所有的参数;
    • 解决方案:使用 @JvmOverloads 注解生成多个重载方法;
  • 注解: @JvmStatic@JvmFiled
    • @JvmStatic 注解可以让 Kotlin 中伴生对象中的属性、方法在 Java 中成为真正的静态属性、方法;
    • @JvmFiled 注解可以让 Kotlin 中的 private 字段(通常为 val)直接暴露为 Java 中的 public final 字段,禁止自动生成 getter/setter 方法;
  • 数据类: Kotlin 中的数据类可以自动生成 equals()hashCode()toStringComponentN() 等方法,Java 需要手动实现;
  • 集合操作: Kotlin 中的集合分为只读集合(List )和可变集合(MutableList),而 Java 中的集合均为可变集合
kotlin 复制代码
// Java 类
public class Person {
    private String name; // 可能为 null

    public String getName() {
        return name;
    }
}

// Kotlin 调用 Java 方法
val person = Person()
val name: String = person.name // 可能为空
val safeName: String = person.name ?: "" // 空安全处理


fun processString(@NotNull text: String) {
    
}

@JvmOverloads
fun greet(name: String, prefix: String = "Hello") {
    
}

@JvmStatic 的使用:

kotlin 复制代码
// Kotlin 无 @JvmStatic
class Utils {
    companion object {
        fun compute(a: Int, b: Int): Int = a + b
    }
}

// Java 调用(需通过 Companion 对象)
int result = Utils.Companion.compute(1, 2);

// Kotlin
class Utils {
    companion object {
        @JvmStatic
        fun compute(a: Int, b: Int): Int = a + b
    }
}

// Java 调用(直接静态访问)
int result = Utils.compute(1, 2);

注解使用:

kotlin 复制代码
class KotlinService {
    companion object {
        @JvmStatic fun create(): KotlinService = KotlinService()
    }
    
    @JvmField val VERSION = "1.0"
    
    @JvmOverloads
    fun process(data: String, timeout: Int = 1000) { ... }
    
    @Throws(IOException::class)
    fun loadResource() { ... }
}

12 在 Kotlin 中,何为解构,该如何使用?

在 Kotlin 中,解构是一种语法糖,允许将一个对象分解成多个独立的变量。

解构声明:

kotlin 复制代码
val (变量1, 变量2, ...) = 对象

示例:

kotlin 复制代码
data class User(val name: String, val age: Int)

// 创建对象
val user = User("Eileen", 34)
// 解构为多个变量
val (name, age) = user
println("name = $name, age = $age") // name = Eileen, age = 34

原理: 解构实际上是调用对象 component1()component2() 等函数

kotlin 复制代码
val name = user.component1()
val age = user.component2()

13 在 Kotlin 中,什么是内联函数?有什么作用?

13.1 内联函数
  • 定义: 在 Kotlin 中,内联函数是一种通过 inline 关键字声明的函数;
  • 目的: 优化 Lambda 表达式所带来的开销;
  • 原理: 内联函数会在编译时直接将函数中的代码"嵌入"到调用处,从而避免函数调用所带来的开销;
    • 当调用一个普通函数时,程序会跳转到函数体内去执行;
    • Java 方法的执行是基于 Java 虚拟机栈的,每一个方法从被调用到执行完成,都对应着一个栈帧的入栈和出栈过程,有一定的性能开销;

普通高阶函数:

kotlin 复制代码
fun nonInlineFun(block: () -> Unit) {
    block()
}


fun main() {
    // 调用时,会生成一个 Function0 对象
    nonInlineFun { println("Hello") }
}

反编译成 Java 代码:

java 复制代码
public final class UserKt {
   public static final void nonInlineFun(@NotNull Function0 block) {
      Intrinsics.checkNotNullParameter(block, "block");
      block.invoke();
   }

   public static final void main() {
      nonInlineFun((Function0)null.INSTANCE);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

内联函数:

kotlin 复制代码
inline fun inlineFunc(block: () -> Unit) {
    block()
}


fun main() {
    // 调用时,会生成一个 Function0 对象
    inlineFunc { println("Hello") }
}

反编译成 Java 代码:

java 复制代码
public final class UserKt {
   public static final void inlineFunc(@NotNull Function0 block) {
      int $i$f$inlineFunc = 0;
      Intrinsics.checkNotNullParameter(block, "block");
      block.invoke();
   }

   public static final void main() {
      int $i$f$inlineFunc = false;
      int var1 = false;
      String var2 = "Hello";
      System.out.println(var2);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}
13.2 noinline 禁止内联

如果一个内联函数的参数里也包含 Lambda 表达式,也就是函数参数,那么该形参也是 inline 的,例如:

kotlin 复制代码
inline fun inlineMethod(inBlock: () -> Unit) {
   
}

需要注意的是:在这个内联函数的内部,函数参数被其他非内联函数调用,是会报错的:

kotlin 复制代码
// 非内联函数
fun noinlineMethod(noBlock: () -> Unit) {

}

报错:

想要解决这个问题,必须为内联函数的参数加上 noinline 修饰符,表示禁止内联,保留原有函数的特性。以上代码的正确写法是:

kotlin 复制代码
inline fun inlineMethod(noinline inBlock: () -> Unit) {
    noinlineMethod(inBlock)
}
13.3 局部返回与非局部返回

内联函数支持非局部返回(Non-local return): 是指从内联函数(inline function)的 lambda 表达式中直接退出退出外层函数(而非 Lambda 本身)的行为;

  • 外层函数是内联的(使用 inline 修饰);
  • Lambda 直接使用 return(无标签修饰);

内联函数支持非局部返回:

kotlin 复制代码
inline fun runCustom(action: () -> Unit) {
    action()
}

fun main() {
    runCustom {
        println("Before return")
        return // 非局部返回:直接退出 main 函数
    }
    println("This won't be printed") // 不会执行
}

// Before return

普通函数不支持非局部返回:

kotlin 复制代码
// 注意:未使用 inline 修饰符!
fun runNonInline(action: () -> Unit) {
    action()
}

fun main() {
    runNonInline {
        println("Before return")
        return // 编译错误:'return' 不允许在这里
    }
}

局部返回(带标签的 return)

kotlin 复制代码
fun main() {
    listOf(1, 2, 3).forEach {
        if (it == 2) return@forEach // 局部返回到 lambda,继续下一次迭代
        println(it)
    }
    println("Done") // 正常执行
}

// 1
// 3
// Done

在 Kotlin 中,当一个普通函数接收 Lambda 表达式作为参数时,该 Lambda 表达式只支持局部返回(不支持非局部返回)。 这是因为:

  • Lambda 表达式会被编译成一个独立的函数对象,有自己的作用域,与它的调用者处于不同的上下文;
  • 从编译器的角度来看,Lambda 表达式是一个独立的代码块,它并不知道外部函数的调用栈情况;
  • 在普通函数中,为了保证 Lambda 表达式能安全的返回,可以使用带标签的返回(如 return@label),明确指定返回的目标;

内联函数支持非局部返回的原因:

  • 内联函数是通过将函数体"嵌入"到调用函数中(包括传递给它的 Lambda 表达式)来消除作用域的边界;
  • 这样,return 自然就作用在了外层函数

示例:

kotlin 复制代码
// 非内联普通函数
fun runNormal(block: () -> Unit) {
    block()
}

// 反编译成 Java 代码
public final class KTTestKt {
   public static final void runNormal(@NotNull Function0 block) {
      Intrinsics.checkNotNullParameter(block, "block");
      block.invoke();
   }
}

在反编译的 Java 代码中:

  • Lambda 会被编译成一个独立的 Function 对象(如 Function0
  • 相当于创建了一个匿名类实例;
  • 关键问题:return 语句子这个独立对象内部无法感知外层函数的上下文环境;

作用域隔离:

Lambda 表达式有自己的作用域边界:

  • 普通函数的 lambda 相当于一个嵌套函数;
  • return 只能作用于当前最内层的作用域
13.4 crossinline 禁止使用非局部返回

在内联函数中,Lambda 表达式参数是默认允许非局部返回。但有时,Lambda 表达式并不会在当前函数中执行,而是会传递给其他的函数。在这种情况下,非局部返回会导致逻辑错误,因为在执行 return 语句时,目标函数可能已经不存在了。

crossinline 是一个用于修饰内联函数中 Lambda 表达式参数的修饰符:

  • 用于标记这个 Lambda 表达式参数,不允许使用非局部返回(return);
  • 允许局部返回(带标签的返回,return@label);
  • 代码仍然会被内联;
  • 编译器会在需要时强制开发者添加 crossinline 修饰符;

非局部返回的潜在风险:

这里如果 action 中包含 return,它将试图返回到原始调用函数,但此时该函数可能已经执行完毕(因为代码在另一个线程执行)。

解决方案:

kotlin 复制代码
// 正确使用 crossinline
inline fun runCustom(crossinline action: () -> Unit) {
    val runnable = Runnable {
        action()
    }
    Executors.newSingleThreadExecutor().submit(runnable)
}

// 正确使用
fun main() {
    runCustom {
        println("Running in background")
    }

    println("Main continues")
}

非局部返回的潜在风险:

13.5 reified

"reified + 内联函数"的组合解决了泛型"类型擦除"带来的问题,允许在运行时访问类型信息。

Java 和 Kotlin 的泛型默认存在"类型擦除",泛型信息(如 List<String> 中的 String)在运行时会被擦除,仅剩原始类型(如 List)。这导致无法在运行时直接获取泛型的具体类型信息。

原理:内联函数让代码"嵌入"到调用处,reified 让"嵌入"的代码保存泛型的类型信息。

  • 内联函数的特性是在编译时将函数体"嵌入"到调用处,而非普通函数那样通过调用栈执行;
  • reified 类型参数(通过 reified 关键字修饰)则利用内联函数的特性,在编译期保留泛型的具体类型信息,避免了类型擦除,可以在运行时直接反问泛型的实际类型;

例如,传统的泛型函数无法直接判断一个对象是否为泛型参数指定的类型:

kotlin 复制代码
// 传统泛型函数:无法在运行时判断 T 的具体类型
fun <T> isType(value: Any): Boolean {
    // 编译错误:Cannot check for instance of erased type T
    return value is T 
}

reified 类型参数的用法示例:

kotlin 复制代码
// 内联函数 + reified 类型参数
inline fun <reified T> checkType(value: Any): Boolean {
    return value is T // 不再报错,可直接判断
}

// 调用
fun main() {
    println(checkType<String>("hello")) // true
    println(checkType<Int>("hello"))    // false
}

14 谈谈 Kotlin 中的构造方法?有哪些注意事项?

14.1 构造方法
  • Kotlin 中的构造方法分为主构造方法(Primary Constructor)和次构造方法(Secondary Constructor);
  • 主构造函数在类名之后声明:
    • 可用注解或可见性修饰符( privatepublic )等修饰;
    • 如果主构造函数没有注解或可见性修饰符修饰,可以省略 constructor 关键字;
  • 次构造函数在类体内声明,如果同时声明了主构造函数,次构造函数需要直接或间接的调用主构造函数;
    • 如果一个类没有显式的声明主构造函数,而是显式声明了次构造函数,这个时候次构造函数无需依次调用主构造函数;
  • Kotlin 中的任何类(除 data/object/companion object 类)都默认有一个无参构造函数(主构造函数);
    • 但是,如果显式的声明了构造函数,默认的无参构造函数就失效了;
kotlin 复制代码
class Person constructor(val name: String, val age: Int) {
    // 类体
}

// 没有注解或可见性修饰符修饰 constructor 可省略
class Person(val name: String, val age: Int) {
  	// 类体
}

// 有主构造函数的情况
class MyView(context: Context) : View(context) {
    // 所有次构造必须调用主构造
    constructor(context: Context, attrs: AttributeSet?) 
        : this(context) // 必须先调用主构造
    
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) 
        : this(context, attrs) // 链式调用
}

// 无主构造函数的情况
class MyView : View {
    // 每个构造直接调用父类对应构造
    constructor(context: Context) : super(context)
    
    constructor(context: Context, attrs: AttributeSet?) 
        : super(context, attrs) // 直接调用父类
    
    // 无链式调用要求
}
14.2 属性声明
  • 主构造函数的参数可以同 val/var 直接声明为类的属性;
    • 可以在类的任何地方(包括 init 代码块、方法等)访问;
  • 如果主构造函数的参数没有用 val/var 修饰,那么它不是类的属性,仅在以下场景下可见:
    • 类体中的 init 代码块(初始化块);
    • 类的属性初始化器(声明类属性时直接赋值的表达式);
kotlin 复制代码
class User(fullName: String) {  // 未用 val/var 修饰的参数
    // 属性初始化器中使用 fullName
    val firstName = fullName.split(" ")[0]
    
    // init 代码块中使用 fullName
    init {
        println("完整名称:$fullName")
    }
    
    fun printFirstName() {
        println("名:$firstName")
        // println(fullName)  // 编译错误:fullName 在此处不可见
    }
}

fun main() {
    val user = User("Alice Smith")
    user.printFirstName()  // 输出:名:Alice
}
14.3 init 初始化代码块
  • 在 Kotlin 中,主构造函数只能有参数声明,不能有可执行代码,init 代码块的核心作用就是弥补这一限制,它允许在主构造函数执行时(即类的初始化阶段)运行初始化逻辑,相当于主构造函数的"代码体";
  • init 代码块的特性:
    • 执行时机:与主构造函数同步执行,在类实例创建时(即调用主构造函数时)自动执行;
    • 执行次数:每个类实例创建时执行一次;
    • 执行顺序:主构造函数参数初始化 ---> 属性初始化器(按声明顺序)---> init 代码块(按声明顺序)
  • **init 代码块和次构造函数: **init 代码块的执行顺序优先于次构造函数中的代码执行
    • 这是因为,次构造函数必须直接或间接调用主构造函数,而 init 代码块是主构造函数初始化逻辑的一部分,会在主构造函数参数解析后立即执行;
  • 在继承关系中: init 代码块的执行顺序遵循"先父后子、先 init 后构造"的规则:
    • 父类的主构造函数参数解析;
    • 父类的 init 代码块(按声明顺序);
    • 父类的次构造函数
    • 子类的主构造函数参数解析
    • 子类的 init 代码块(按声明顺序)
    • 子类的次构造函数

执行顺序:

kotlin 复制代码
class User(val name: String) { // 主构造函数
  	// 属性初始化器
    val greeting = "Hello, $name!".also { println("属性初始化器执行") }

  	// init 代码块
    init {
        println("init 代码块执行")
    }

  	// 次构造函数
    constructor(name: String, age: Int) : this(name) {
        println("次构造函数:name = $name, age = $age")
    }
}

fun main() {
    val user = User("Eileen", 33)
}

// 属性初始化器执行
// init 代码块执行
// 次构造函数:name = Eileen, age = 33

继承关系:

kotlin 复制代码
open class User(name: String) { // 主构造函数
    // 属性初始化器
    val greeting = "Hello, $name!".also { println("(父类)属性初始化器执行") }

    // init 代码块
    init {
        println("(父类)init 代码块执行")
    }

    // 次构造函数
    constructor(name: String, age: Int) : this(name) {
        println("(父类)次构造函数:name = $name, age = $age")
    }
}

class Student(name: String) : User(name) {
    // 属性初始化器
    val sGreeting = "Hello, $name!".also { println("(子类)属性初始化器执行") }

    // init 代码块
    init {
        println("(子类)init 代码块执行")
    }

    // 次构造函数
    constructor(name: String, age: Int) : this(name) {
        println("(子类)次构造函数:name = $name, age = $age")
    }

}

fun main() {
    val student = Student("Eileen", 33)
}

//(父类)属性初始化器执行
//(父类)init 代码块执行
//(子类)属性初始化器执行
//(子类)init 代码块执行
//(子类)次构造函数:name = Eileen, age = 33
14.4 特殊类
  • object/companion object 是对象示例,作为单例类或伴生对象,没有构造函数

  • data class 必须有一个含有至少一个成员属性的主构造函数;

  • 密封类(sealed class):是一种特殊的抽象类, 其子类必须密封类的内部或同一文件中声明(限制继承);

    • 密封类的构造函数默认为 protected,也可以显式声明为 private

    • 不允许声明为 publicinternal

15 谈谈 Kotlin 中的 Sequence,为什么它处理集合操作更加高效?

Sequence(序列)和普通集合的核心区别:

  • 普通集合(如 List)的急切求值: 每调用一个中间操作(如 mapfilter)都会立即执行并生成一个新的集合,最终导致多次遍历和中间集合的内存开销;
  • Sequence 采用惰性求值: 中间操作(如 mapfilter)不会立即执行生成新的集合,只会记录操作逻辑;直到调用终端操作(如 toListcountforEach)时,才会生成最终结果,且仅遍历一次;

Sequence 更高效的原因:

  • 惰性求值: Sequence 避免中间集合的创建,仅仅记录操作逻辑,节省内存;
    • 普通集合的链式操作会产生多个中间集合,占用额外内存;
  • 单次遍历: Sequence 仅需要单次遍历即可完成所有操作;
    • 普通集合的链式操作需要多次遍历(每个操作一次);
    • 普通集合:map 遍历 100 万次 ---> 生成中间集合 ---> filter 再遍历 100 万次 ---> 总共遍历 200 万次;
    • Sequence:终端操作时,遍历 100 万次,每次遍历时同时执行 mapfilter ---> 总共遍历 100 万次;
  • 优化短路操作: 对于包含短路逻辑的操作(如 findanytake),Sequence 可以在满足条件时立即终止遍历,避免不必要的计算;

示例代码:

kotlin 复制代码
val list = listOf(1, 2, 3, 4, 5)
// 普通集合:生成 2 个中间集合(map 结果、filter 结果)
val result = list
    .map { it * 2 }   // 立即执行,生成 [2,4,6,8,10]
    .filter { it > 5 } // 立即执行,生成 [6,8,10]


val sequence = list.asSequence()
// Sequence:不生成中间集合,仅记录 map 和 filter 的逻辑
val result = sequence
    .map { it * 2 }   // 不执行,仅记录
    .filter { it > 5 } // 不执行,仅记录
    .toList()         // 终端操作:触发执行,一次性计算结果

反编译成 Java 代码:

kotlin 复制代码
public final class KTTestKt {
   @NotNull
   private static final List list = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4, 5});
   @NotNull
   private static final List result0;
   @NotNull
   private static final Sequence sequence;
   @NotNull
   private static final List result1;

   @NotNull
   public static final List getList() {
      return list;
   }

   @NotNull
   public static final List getResult0() {
      return result0;
   }

   @NotNull
   public static final Sequence getSequence() {
      return sequence;
   }

   @NotNull
   public static final List getResult1() {
      return result1;
   }

   static {
      Iterable $this$filter$iv = (Iterable)list;
      int $i$f$filter = false;
      Collection destination$iv$iv = (Collection)(new ArrayList(CollectionsKt.collectionSizeOrDefault($this$filter$iv, 10)));
      int $i$f$filterTo = false;
      Iterator var5 = $this$filter$iv.iterator();

      Object element$iv$iv;
      int it;
      boolean var8;
      while(var5.hasNext()) { // 普通集合的第1次遍历
         element$iv$iv = var5.next();
         it = ((Number)element$iv$iv).intValue();
         var8 = false;
         Integer var10 = it * 2;
         destination$iv$iv.add(var10);
      }

      $this$filter$iv = (Iterable)((List)destination$iv$iv);
      $i$f$filter = false;
      destination$iv$iv = (Collection)(new ArrayList());
      $i$f$filterTo = false;
      var5 = $this$filter$iv.iterator();

      while(var5.hasNext()) {// 普通集合的第2次遍历
         element$iv$iv = var5.next();
         it = ((Number)element$iv$iv).intValue();
         var8 = false;
         if (it > 5) {
            destination$iv$iv.add(element$iv$iv);
         }
      }

      result0 = (List)destination$iv$iv;
      sequence = CollectionsKt.asSequence((Iterable)list);
      result1 = SequencesKt.toList(SequencesKt.filter(SequencesKt.map(sequence, (Function1)null.INSTANCE), (Function1)null.INSTANCE));
   }
}

优化的短路操作:

kotlin 复制代码
// 普通集合:先 map 所有元素(生成中间集合),再 filter 查找 → 处理所有元素
list.map { it * 2 }.find { it > 5 } 

// Sequence:遍历到第 3 个元素(1*2=2→不满足,2*2=4→不满足,3*2=6→满足)时,立即终止
list.asSequence().map { it * 2 }.find { it > 5 } 

16 谈谈 Kotlin 中的 Coroutines,它与线程有什么区别?有哪些优点?

在 Kotlin 中,协程(Coroutines /kəʊrəʊˈtiːnz/)是一种轻量级的并发编程模型,用于处理异步、并发和非阻塞代码。

结构化并发:通过作用域自动管理协程的生命周期,减少泄漏的风险。

协程常见作用域:

  • viewModelScope:与 ViewModel 生命周期绑定;
  • lifecycleScope:与 Activity/Fragment 生命周期绑定;

阻塞、挂起、睡眠 在主动/被动,内存/外存、cpu、锁、线程等 方面有什么不同?

阻塞、挂起、睡眠:

17 Kotlin 中该如何安全地处理可空类型?

  • 安全调用操作符 ?. 当我们不确定某个可空变量是否为 null 时,可以使用安全调用操作符;
    • 如果该变量不为 null,则执行操作;否则,不执行并返回 null
  • Elvis (埃尔维斯)运算符 ?: 用于提供当表达式的结果为 null 时的默认值;
  • 非空断言 !! 当我们确定某个可空变量不为 null 时,可以使用非空断言操作符;
    • 但是,如果变量为 null,则会抛出 NullPointerException(NPE),需谨慎使用;
  • 安全类型转换 as 当我们尝试将对象转换为目标类型时,如果转换失败,通常会导致 ClassCastException。使用安全类型转换 as,即使转换失败,也会返回 null;
  • **let 函数: ** 允许我们对可空变量执行一个代码块,如果变量不为 null,则执行代码块(避免额外的 if 判断);
    • 常见用途:集中处理非空逻辑,替代 if (user != null) { ... }
    • 使用 runapplyalso 等作用域函数也可以处理可空类型

安全调用操作符:

kotlin 复制代码
data class User(val address: Address?)
data class Address(val city: String?)

fun getCity(user: User?): String? {
    // 链式安全调用:任何环节为 null,整个表达式返回 null
    return user?.address?.city 
}

// 调用
val user: User? = null
println(getCity(user)) // 输出 null,无异常

Elvis 运算符:

kotlin 复制代码
fun getUserName(user: User?): String {
    // 若 user 为 null,返回默认值 "Unknown"
    return user?.name ?: "Unknown"
}

// 复杂场景:结合安全调用返回非空类型
val length = user?.name?.length ?: 0 // 若 name 为 null,长度默认为 0

非空断言:

kotlin 复制代码
fun printName(user: User?) {
    // 断言 user 不为 null,否则抛 NPE
    println(user!!.name) 
}

安全类型转换 as

kotlin 复制代码
fun safeCast(obj: Any?): String? {
    // 若转换失败,返回 null
    return obj as? String 
}

val str: Any? = 123
println(safeCast(str)) // 输出 null,无异常

let 函数

kotlin 复制代码
fun processUser(user: User?) {
    // 仅当 user 不为 null 时,才执行 lambda 中的逻辑
    user?.let { 
        println("用户名:${it.name}")
        saveUser(it) // 安全调用非空对象的方法
    }
}

18 说说 Kotlin 中的 Any 和 Java 中的 Object 有何异同?

相同点:

  • 根类:
    • 在 Java 中,Object 类是所有类(基本类型除外)的根类
      • 但基本类型也有对应的包装类,这些包装类都继承自 Object
    • 在 Kotlin 中,Any 是所有非空类型的根类(包括基本类型如 IntDouble 等);
      • 注意:Kotlin 中的可空类型(如 String?)的根类是 Any?
  • 基础方法: 都提供了面向对象的核心方法
    • equals():判断对象的相等性;
    • hashCode():获取对象的哈希值;
    • toString():返回对象的字符串表示;

不同点:

Kotlin 中的默认继承规则:任何没有显式声明父类的类,都会默认继承 Any。这一规则适用于所有的类。

kotlin 复制代码
println(Int::class.supertypes) // [kotlin.Number, kotlin.Comparable<kotlin.Int>, java.io.Serializable]
println(Double::class.supertypes) // [kotlin.Number, kotlin.Comparable<kotlin.Double>, java.io.Serializable]
println(Boolean::class.supertypes) // [kotlin.Comparable<kotlin.Boolean>, java.io.Serializable, kotlin.Any]
println(Number::class.supertypes) // [kotlin.Any, java.io.Serializable]

根类、顶级父类、超类:

19 Kotlin 中的数据类型有隐式转换吗?为什么?

在 Kotlin 中,基本数据类型(如 IntLongDouble 等)之间没有隐式转换,必须通过显式转换函数(如 toLongtoDouble)进行类型转换:

  • 避免精度损失风险:如 LongInt 时的数据截断;
  • 消除类型混淆错误,保持类型一致性;

Kotlin 为所有的基本类型提供了完整的显式转换函数:

  • toByte()toShort()toInt()toLong()
  • toFloat()toDouble
  • toChar()

正确的写法:

kotlin 复制代码
val a: Int = 1
val b: Long = 2
println(a.toLong() == b) // false

20 Kotlin 中遍历集合有哪几种方式?

  • for-in 循环;
  • forEach 高阶函数:通过 Lambda 表达式遍历,简介且支持函数式编程风格:
  • 迭代器(Iterator):显示使用 iterator() 获取迭代器,手动控制遍历过程;
  • 通过索引遍历(仅适用于有序集合);
  • 范围遍历;
  • 序列(Sequence)遍历;
kotlin 复制代码
// for-in 循环
val fruits = listOf("Apple", "Banana", "Cherry")

for (fruit in fruits) {
    println("$fruit  ")
}

for ((index, fruit) in fruits.withIndex()) {
    println("$index: $fruit  ")
}

// forEach 高阶函数
val numbers = listOf(1, 2, 3, 4, 5)

numbers.forEach { num ->
    println("${num * 2} ")
}

numbers.forEachIndexed { index, num ->
    println("$index: $num  ")
}

// 迭代器
val set = setOf("Red", "Green", "Blue")
val iterator = set.iterator()

while (iterator.hasNext()) {
    val color = iterator.next()
    if (color == "Green") {
        break // 中途终止遍历
    }
    println(color)
}


// 通过索引遍历
val animals = listOf("Dog", "Cat", "Bird")

for (i in animals.indices) {
    println("动物 ${i + 1}: ${animals[i]}")
}

for (i in 0 until animals.size) {
    println(animals[i])
}

// 序列
val largeList = (1..1000000).toList().asSequence()
largeList.filter { it % 2 == 0 }
    .map { it * 2 }
    .take(5)
    .forEach { println(it) }