当前位置: 首页 > news >正文

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 示例:匿名内部类实现 OnClickListener
button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// 处理点击事件}
});// 使用 Lambda 表达式简化
button.setOnClickListener(v -> {// 处理点击事件
});// Kotlin 的转换 (OnClickListener 是 Java 定义的接口)
button.setOnClickListener {// 处理点击事件
}

扩展:

// 扩展属性
val MutableList<Int>.sum: Intget() = 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 默认参数
fun showToast(message: String,duration: Int = Toast.LENGTH_SHORT // 默认值
) {}showToast("Hello") // 使用默认 duration
showToast("Hi", Toast.LENGTH_LONG) // 显式指定 duration

判空语法:

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 函数,从而间接利用默认参数的功能。

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

生成重载方法:

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:只读集合接口(线程安全),一旦创建,其元素的数量和内容都不能被修改。

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

// 只读 -> 可变
val list = listOf(1, 2, 3)
val mutableList = list.toMutableList() // 新对象
mutableList.add(4)// 可变 -> 只读
val mutable = mutableListOf(1, 2, 3)
val readOnly = mutable.toList()

只读集合可变的情况:

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 代码:静态变量在静态代码块中初始化
// 懒汉式,线程安全:私有构造函数 + 伴生对象 + by lazy
class Singleton private constructor() {companion object {val singleton: Singleton by lazy { // LazyThreadSafetyMode.SYNCHRONIZEDSingleton()}}
}// 懒汉式,双重检验锁:私有构造函数 + 伴生对象 + @Volatile + synchronized
class Singleton private constructor() {companion object {// 变量可见性 + 防止指令重排@Volatileprivate var instance: Singleton? = nullfun 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 代码:

public final class Singleton {@NotNullpublic 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 解析

类委托:

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 对象,编译器自动生成转换方法,无需手动实现接口。

属性委托:

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

标准库中的委托:

// 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 -> 10value = 20  // 输出: value changed: 10 -> 20
}// Map 委托:将属性存储到 Map 中,适用于动态属性场景(如 JSON 解析)
class User(val map: Map<String, Any?>) {val name: String by mapval age: Int     by map
}// 使用
fun main() {val user = User(mapOf("name" to "Alice","age"  to 25))println(user.name) // Aliceprintln(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 函数来说,“返回类型”的概念没有任何意义,因为它们永远不会成功的结束。
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

示例:

infix fun String.concatWith(another: String) = "$this$another"// 链式中缀调用
val message = "Hello" concatWith "World" concatWith "!"

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

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

标准库中的中缀函数:

  • to 函数:用于创建 Pair 对象;
  • until 函数:用于生成区间;
  • 集合操作,如 stepdownTo;
// 源码
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 的很多特性都是在语法和编译器上的优化)

示例:

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 代码

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 中的集合均为可变集合
// Java 类
public class Person {private String name; // 可能为 nullpublic 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 无 @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 {@JvmStaticfun compute(a: Int, b: Int): Int = a + b}
}// Java 调用(直接静态访问)
int result = Utils.compute(1, 2);

注解使用:

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

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

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

解构声明:

val (变量1, 变量2, ...) = 对象

示例:

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() 等函数

val name = user.component1()
val age = user.component2()

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

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

普通高阶函数:

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

反编译成 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 methodpublic static void main(String[] var0) {main();}
}

内联函数:

inline fun inlineFunc(block: () -> Unit) {block()
}fun main() {// 调用时,会生成一个 Function0 对象inlineFunc { println("Hello") }
}

反编译成 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 methodpublic static void main(String[] var0) {main();}
}
13.2 noinline 禁止内联

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

inline fun inlineMethod(inBlock: () -> Unit) {}

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

// 非内联函数
fun noinlineMethod(noBlock: () -> Unit) {}

报错:

noinline

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

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

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

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

非局部返回&局部返回

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

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

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

// 注意:未使用 inline 修饰符!
fun runNonInline(action: () -> Unit) {action()
}fun main() {runNonInline {println("Before return")return // 编译错误:'return' 不允许在这里}
}

局部返回(带标签的 return)

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 自然就作用在了外层函数

内联函数和普通函数

示例:

// 非内联普通函数
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,它将试图返回到原始调用函数,但此时该函数可能已经执行完毕(因为代码在另一个线程执行)。

解决方案:

// 正确使用 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 关键字修饰)则利用内联函数的特性,在编译期保留泛型的具体类型信息,避免了类型擦除,可以在运行时直接反问泛型的实际类型;

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

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

reified 类型参数的用法示例:

// 内联函数 + reified 类型参数
inline fun <reified T> checkType(value: Any): Boolean {return value is T // 不再报错,可直接判断
}// 调用
fun main() {println(checkType<String>("hello")) // trueprintln(checkType<Int>("hello"))    // false
}

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

14.1 构造方法
  • Kotlin 中的构造方法分为主构造方法(Primary Constructor)和次构造方法(Secondary Constructor);
  • 主构造函数在类名之后声明:
    • 可用注解或可见性修饰符( privatepublic )等修饰;
    • 如果主构造函数没有注解或可见性修饰符修饰,可以省略 constructor 关键字;
  • 次构造函数在类体内声明,如果同时声明了主构造函数,次构造函数需要直接或间接的调用主构造函数;
    • 如果一个类没有显式的声明主构造函数,而是显式声明了次构造函数,这个时候次构造函数无需依次调用主构造函数;
  • Kotlin 中的任何类(除 data/object/companion object 类)都默认有一个无参构造函数(主构造函数);
    • 但是,如果显式的声明了构造函数,默认的无参构造函数就失效了;
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 代码块(初始化块);
    • 类的属性初始化器(声明类属性时直接赋值的表达式);

类的属性

class User(fullName: String) {  // 未用 val/var 修饰的参数// 属性初始化器中使用 fullNameval firstName = fullName.split(" ")[0]// init 代码块中使用 fullNameinit {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 代码块(按声明顺序)
    • 子类的次构造函数

执行顺序:

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

继承关系:

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 可以在满足条件时立即终止遍历,避免不必要的计算;

示例代码:

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 代码:

public final class KTTestKt {@NotNullprivate static final List list = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4, 5});@NotNullprivate static final List result0;@NotNullprivate static final Sequence sequence;@NotNullprivate static final List result1;@NotNullpublic static final List getList() {return list;}@NotNullpublic static final List getResult0() {return result0;}@NotNullpublic static final Sequence getSequence() {return sequence;}@NotNullpublic 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));}
}

优化的短路操作:

// 普通集合:先 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 等作用域函数也可以处理可空类型

安全调用操作符:

data class User(val address: Address?)
data class Address(val city: String?)fun getCity(user: User?): String? {// 链式安全调用:任何环节为 null,整个表达式返回 nullreturn user?.address?.city 
}// 调用
val user: User? = null
println(getCity(user)) // 输出 null,无异常

Elvis 运算符:

fun getUserName(user: User?): String {// 若 user 为 null,返回默认值 "Unknown"return user?.name ?: "Unknown"
}// 复杂场景:结合安全调用返回非空类型
val length = user?.name?.length ?: 0 // 若 name 为 null,长度默认为 0

非空断言:

fun printName(user: User?) {// 断言 user 不为 null,否则抛 NPEprintln(user!!.name) 
}

安全类型转换 as

fun safeCast(obj: Any?): String? {// 若转换失败,返回 nullreturn obj as? String 
}val str: Any? = 123
println(safeCast(str)) // 输出 null,无异常

let 函数

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():返回对象的字符串表示;

不同点:

Any 和 Object

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

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()

类型转换

正确的写法:

val a: Int = 1
val b: Long = 2
println(a.toLong() == b) // false

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

  • for-in 循环;
  • forEach 高阶函数:通过 Lambda 表达式遍历,简介且支持函数式编程风格:
  • 迭代器(Iterator):显示使用 iterator() 获取迭代器,手动控制遍历过程;
  • 通过索引遍历(仅适用于有序集合);
  • 范围遍历;
  • 序列(Sequence)遍历;
// 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) }
http://www.dtcms.com/a/339724.html

相关文章:

  • 驱动开发系列66 - glCompileShader实现 - GLSL中添加内置函数
  • 从“为什么”到“怎么做”——Linux Namespace 隔离实战全景地图
  • [激光原理与应用-309]:光学设计 - 什么是光学系统装配图,其用途、主要内容、格式与示例?
  • 线性基 系列
  • Java static关键字
  • OptiTrack光学跟踪系统,提高机器人活动精度
  • 讯飞星火语音大模型
  • CAD图纸如何批量转换成PDF格式?
  • 机器学习概念(面试题库)
  • 部署tomcat应用时注意事项
  • vue3+element-plus 输入框el-input设置背景颜色和字体颜色,样式效果等同于不可编辑的效果
  • t-SNE详解与实践【附代码】
  • 自定义组件可使用的方法
  • 在 Python 中操作 Excel 文件的高效方案 —— Aspose.Cells for Python
  • 《P1550 [USACO08OCT] Watering Hole G》
  • Java开发过程中实用的技术点(一)
  • 【矢量数据】1:250w中国地质图地断层数据/岩性shp数据
  • FlashAttention编译错误
  • Docker 搭建私有镜像仓库
  • 【C++】 C++11 智能指针
  • AI因子模型视角下的本周五鲍威尔演讲:通胀约束与就业压力的政策博弈
  • Spring Cloud系列—Seata分布式事务解决方案AT模式
  • 2025年6月中国电子学会青少年软件编程(图形化)等级考试试卷(一级)答案 + 解析
  • 编译器错误消息: CS0016: 未能写入输出文件“c:\Windows\Microsoft.NET... 拒绝访问
  • Linux管道
  • NVIDIA 优化框架:Jetson 平台 PyTorch 安装指南
  • 初步学习WPF-Prism
  • 图论\dp 两题
  • GIS相关调研
  • Meta首款AR眼镜Hypernova呼之欲出,苹果/微美全息投入显著抢滩市场新增长点!