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 中,闭包可以独立存在,不依赖特定的接口;
- Java 中的 Lambda 表达式主要用于简化单抽象方法的接口。
- 扩展
- 扩展属性: 允许开发者为现有的类添加新的属性,不过并不是真正的给类添加成员变量,而是提供了自定义的
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
(伴生对象)、volatile
、synchronized
实现,首次访问时才进行初始化;- 通过
@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),专门为简化数据模型而设计的。
特点:
- 主构造函数必须至少有一个参数,且参数标记为
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 解析
-
类委托:
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
,可以省略;
- Kotlin 中引入
- 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
函数:用于生成区间;- 集合操作,如
step
和downTo
;
// 源码
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 需要进行空判断,如使用安全调用操作符(
- 默认参数和方法重载: 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 中的集合均为可变集合
// 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
修饰符,表示禁止内联,保留原有函数的特性。以上代码的正确写法是:
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);
- 主构造函数在类名之后声明:
- 可用注解或可见性修饰符(
private
、public
)等修饰; - 如果主构造函数没有注解或可见性修饰符修饰,可以省略
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
; -
不允许声明为
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 可以在满足条件时立即终止遍历,避免不必要的计算;
示例代码:
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) { ... }
- 使用
run
、apply
、also
等作用域函数也可以处理可空类型
- 常见用途:集中处理非空逻辑,替代
安全调用操作符:
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
是所有非空类型的根类(包括基本类型如Int
、Double
等);- 注意:Kotlin 中的可空类型(如
String?
)的根类是Any?
- 注意:Kotlin 中的可空类型(如
- 在 Java 中,
- 基础方法: 都提供了面向对象的核心方法
equals()
:判断对象的相等性;hashCode()
:获取对象的哈希值;toString()
:返回对象的字符串表示;
不同点:
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 中,基本数据类型(如 Int
、Long
、Double
等)之间没有隐式转换,必须通过显式转换函数(如 toLong
、toDouble
)进行类型转换:
- 避免精度损失风险:如
Long
转Int
时的数据截断; - 消除类型混淆错误,保持类型一致性;
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) }