1 什么是 Kotlin,它有哪些特性?
Kotlin 和 Java 一样是 JVM 编程语言,它们的代码最终都会被编译成 .class 文件(JVM 字节码)。
Kotlin 的特性主要有以下几点:
- Lambda 表达式,函数式编程: Lambda 表达式并不是 Kotlin 特有的,Java 中也有,但是有限制
- Java 中的 Lambda 表达式主要用于简化单抽象方法的接口。
- 这是因为 Java 的系统类型需要明确的类型信息,Lambda 表达式要和特定的函数式接口(仅包含一个抽象方法的接口 SAM Single Abstract Method Interface)类型匹配;
- Kotlin 中的 Lambda 表达式同样支持单抽象方法的接口,但更推荐使用闭包来实现。
- 闭包是一个可以捕获其所在上下文变量的代码块,在 Kotlin 中,闭包可以独立存在,不依赖特定的接口;
- Java 中的 Lambda 表达式主要用于简化单抽象方法的接口。
- 扩展
- 扩展属性: 允许开发者为现有的类添加新的属性,不过并不是真正的给类添加成员变量,而是提供了自定义的
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
(伴生对象)、volatile
、synchronized
实现,首次访问时才进行初始化;- 通过
@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),专门为简化数据模型而设计的。
特点:
- 主构造函数必须至少有一个参数,且参数标记为
var
或val
;- 数据类的核心是属性,而非普通参数。如果不标记为
var
/val
,参数仅作为构造参数存在,不会成为类的属性,导致:- 无法通过
对象.属性名
访问; - 编译器无法生成
equals()
/hashCode()
等方法;
- 无法通过
- 数据类的核心是属性,而非普通参数。如果不标记为
- 自动生成标准方法:
toString()
、componentN()
、equals()
和hashCode()
、copy()
等方法;toString()
:生成格式化的字符串表示,如User(name=John, age=30)
;componentN()
:函数:支持解构声明访问属性;equals()
和hashCode()
:比较两个对象的内容是否相等(而非引用相等),仅基于主构造函数中声明的属性;copy()
:快速创建对象的副本,并可选择性修改部分属性(适用于不可变独享);
- 数据类不能是
abstract
、open
、sealed
(密封类)或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
,可以省略;
- Kotlin 中引入
- 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
函数:用于生成区间;- 集合操作,如
step
和downTo
;
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 需要进行空判断,如使用安全调用操作符(
- 默认参数和方法重载: Kotlin 支持默认参数,但 Java 是不支持的,Java 调用带默认参数的 Kotlin 方法时必须传递所有的参数;
- 解决方案:使用
@JvmOverloads
注解生成多个重载方法;
- 解决方案:使用
- 注解:
@JvmStatic
和@JvmFiled
@JvmStatic
注解可以让 Kotlin 中伴生对象中的属性、方法在 Java 中成为真正的静态属性、方法;@JvmFiled
注解可以让 Kotlin 中的private
字段(通常为val
)直接暴露为 Java 中的public final
字段,禁止自动生成getter/setter
方法;
- 数据类: Kotlin 中的数据类可以自动生成
equals()
、hashCode()
、toString
、ComponentN()
等方法,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);
- 主构造函数在类名之后声明:
- 可用注解或可见性修饰符(
private
、public
)等修饰; - 如果主构造函数没有注解或可见性修饰符修饰,可以省略
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
; -
不允许声明为
public
或internal
;
-
15 谈谈 Kotlin 中的 Sequence,为什么它处理集合操作更加高效?
Sequence(序列)和普通集合的核心区别:
- 普通集合(如
List
)的急切求值: 每调用一个中间操作(如map
、filter
)都会立即执行并生成一个新的集合,最终导致多次遍历和中间集合的内存开销; - Sequence 采用惰性求值: 中间操作(如
map
、filter
)不会立即执行生成新的集合,只会记录操作逻辑;直到调用终端操作(如toList
、count
、forEach
)时,才会生成最终结果,且仅遍历一次;
Sequence 更高效的原因:
- 惰性求值: Sequence 避免中间集合的创建,仅仅记录操作逻辑,节省内存;
- 普通集合的链式操作会产生多个中间集合,占用额外内存;
- 单次遍历: Sequence 仅需要单次遍历即可完成所有操作;
- 普通集合的链式操作需要多次遍历(每个操作一次);
- 普通集合:
map
遍历 100 万次 ---> 生成中间集合 --->filter
再遍历 100 万次 ---> 总共遍历 200 万次; - Sequence:终端操作时,遍历 100 万次,每次遍历时同时执行
map
和filter
---> 总共遍历 100 万次;
- 优化短路操作: 对于包含短路逻辑的操作(如
find
、any
、take
),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) { ... }
- 使用
run
、apply
、also
等作用域函数也可以处理可空类型
- 常见用途:集中处理非空逻辑,替代
安全调用操作符:
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
是所有非空类型的根类(包括基本类型如Int
、Double
等);- 注意:Kotlin 中的可空类型(如
String?
)的根类是Any?
- 注意:Kotlin 中的可空类型(如
- 在 Java 中,
- 基础方法: 都提供了面向对象的核心方法
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 中,基本数据类型(如 Int
、Long
、Double
等)之间没有隐式转换,必须通过显式转换函数(如 toLong
、toDouble
)进行类型转换:
- 避免精度损失风险:如
Long
转Int
时的数据截断; - 消除类型混淆错误,保持类型一致性;
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) }