Kotlin 语言特性与常见陷阱测试题解析
Kotlin 语言特性与常见陷阱测试题解析。本文通过一系列精选的 Kotlin 编程题目,深入探讨了集合排序、引用相等性、函数重载、属性覆写、协变、扩展函数、Lambda 表达式及委托等核心概念。每道题目均包含代码示例、选项分析及详细解答,旨在帮助开发者理解编译器行为、避免常见错误并提升对 Kotlin 语言机制的掌握程度。适合中高级 Kotlin 开发者进行自我检测与知识巩固。

Kotlin 语言特性与常见陷阱测试题解析。本文通过一系列精选的 Kotlin 编程题目,深入探讨了集合排序、引用相等性、函数重载、属性覆写、协变、扩展函数、Lambda 表达式及委托等核心概念。每道题目均包含代码示例、选项分析及详细解答,旨在帮助开发者理解编译器行为、避免常见错误并提升对 Kotlin 语言机制的掌握程度。适合中高级 Kotlin 开发者进行自我检测与知识巩固。

在 Kotlin 开发中,理解编译器的行为、JVM 的底层机制以及语言特性的细节对于编写健壮代码至关重要。许多开发者依赖 IDE 的智能提示和自动补全,这虽然提高了效率,但也可能掩盖了潜在的逻辑错误。本文通过一系列精选的 Kotlin 编程题目,深入探讨了集合操作、函数重载、属性覆写、协变、扩展函数及 Lambda 表达式等核心概念。每道题目均包含代码示例、选项分析及详细解答,旨在帮助开发者理解编译器如何处理代码,避免常见错误并提升对 Kotlin 语言机制的掌握程度。
题目:
val list = arrayListOf(1, 5, 3, 2, 4)
val sortedList = list.sort()
println(sortedList)
选项: a) [1, 5, 3, 2, 4] b) [1, 2, 3, 4, 5] c) kotlin.Unit d) Will not compile
答案: c
解析:
Kotlin 中的 sort() 函数作用于可变集合(如 ArrayList),它会就地修改原集合的顺序,返回类型为 Unit。而 sorted() 函数会返回一个新的已排序集合,不修改原集合。
最佳实践:
list.sort() 以节省内存。list.sorted()。题目:
println(listOf(1, 2, 3) == listOf(1, 2, 3))
println(listOf(1, 2, 3).asSequence() == listOf(1, 2, 3).asSequence())
println(sequenceOf(1, 2, 3) == sequenceOf(1, 2, 3))
选项: a) true; true; true b) true; true; false c) true; false; true d) true; false; false
答案: d (修正:实际应为 true; false; false)
解析:
在 Kotlin 中,== 运算符会调用对象的 equals() 方法。
List 实现了 equals() 方法,基于内容进行比较,因此两个内容相同的 List 返回 true。Sequence 通常没有重写 equals(),默认继承自 Any,基于引用地址比较。因此不同的 Sequence 实例即使内容相同也返回 false。注意: 原文本中关于 List 相等的描述存在误区,标准 Kotlin 行为是结构相等。
题目:
open class C {
open fun sum(x: Int = 1, y: Int = 2): Int = x + y
}
class D : C() {
override fun sum(y: Int, x: Int): Int = super.sum(x, y)
}
val d: D = D()
val c: C = d
print(c.sum(x = 0))
print(", ")
print(d.sum(x = 0))
选项: a) 2,2 b) 1,1 c) 2,1 d) Will not compile
答案: c
解析:
open 函数的调用在运行时根据实际对象类型决定。c 指向的是 D 实例,所以调用 D 的 sum。c.sum(x=0) 中,编译器根据 C 的签名查找参数 x,值为 0,y 默认为 2,结果 2。d.sum(x=0) 调用 D 的方法,参数名交换了,传入 x=0 对应 D 的 y 参数,super.sum(x, y) 传入 0 和默认值 2,结果为 2?这里需要仔细推导。
D.sum(y, x) 定义中,第一个参数是 y,第二个是 x。d.sum(x=0):将 0 赋值给参数 x。参数 y 无默认值,必须提供。此题代码在严格模式下可能无法编译,因为 y 未传值且无默认值。但假设上下文允许或通过具名参数传递。c.sum(x=0) 走父类逻辑,x=0, y=2 -> 2。d.sum(x=0) 走子类逻辑,x=0,y 缺失。若假设 y 有默认值或忽略此错误,重点在于 super.sum(x, y) 的参数顺序。题目:
open class Parent(open val a: String) {
init { println(a) }
}
class Children(override val a: String): Parent(a)
Children("abc")
选项: a) abc b) Unresolved reference: a c) Nothing, it won't compile d) null
答案: d
解析:
这是一个经典的 Kotlin 初始化陷阱。当子类覆写父类的属性时,父类的 init 块执行时,子类的属性尚未初始化。此时访问该属性,由于 JVM 层面的字段覆盖,可能会读取到子类的未初始化字段(null)。
最佳实践:
lateinit var 或在初始化完成后才访问属性。题目:
open class Node(val name: String) {
fun lookup() = "lookup in: $name"
}
class Example : Node("container") {
fun createChild(name: String): Node? = Node(name)
val child1 = createChild("child1")?.apply {
println("child1 ${lookup()}")
}
val child2 = createChild("child2").apply {
println("child2 ${lookup()}")
}
}
Example()
选项: A) child1 lookup in: child1; child2 lookup in: child2 B) child1 lookup in: child1; child2 lookup in: container C) child1 lookup in: container; child2 lookup in: child2 D) none of the above
答案: B
解析:
createChild 返回 Node?。child1 使用了安全调用符 ?.apply,其接收者类型为 Node?。但在 apply 内部,this 是 Node?。lookup() 在 Node? 上不可直接调用,除非解包。实际上 apply 的 lambda 接收者是 Node (非空)。但 child2 是直接 .apply,接收者是 Node。Example 类本身继承自 Node("container")。child2 的 apply 是在 Example 实例的上下文中吗?不,createChild 返回新对象。child2 的 apply 接收者是 Node("child2")。lookup() 应该输出 child2。但原题答案为 B,暗示 child2 输出了 container。这说明 apply 的接收者可能被捕获了外部 Example 实例的上下文(即 this 指向 Example 而非 Node)。这是因为 createChild 返回的对象在 Example 的作用域内被处理,或者 apply 的闭包捕获了外部 this。Example 类中,createChild 返回 Node。apply 的 lambda 中,如果没有明确指定接收者,this 指向 apply 的调用对象。但如果 lookup 被定义为 Example 的方法(通过继承),则可能混淆。本题考察的是 apply 的 this 指向问题。在 child2 中,apply 的接收者是 Node 实例,应调用 Node.lookup()。若答案为 B,说明 lookup 被解析为了 Example 的 lookup(继承自 Node("container"))。这通常发生在 apply 未正确隔离上下文时。题目:
print(-1.inc())
print(", ")
print(1 + -(1))
选项: a) 0, 0 b) Won't compile in line 4 c) 0, 2 d) -2, 0
答案: a (修正:原题意可能有误,实际为 0, 0)
解析:
-1.inc():先计算 -1,再调用 inc()。-1 + 1 = 0。1 + -(1):-(1) 是一元减,结果为 -1。1 + (-1) = 0。-2 是错误的理解,inc() 优先级高于一元减号的情况不存在于 -1.inc() 这种写法中。题目:
data class Container(val list: MutableList<String>)
val list = mutableListOf("one", "two")
val c1 = Container(list)
val c2 = c1.copy()
list += "oops"
println(c2.list.joinToString())
选项: a) one, two b) one, two, oops c) UnsupportedOperationException d) will not compile
答案: b
解析:
copy() 方法执行的是浅拷贝。它复制了 Container 对象,但 list 字段的引用仍然指向同一个 MutableList 对象。因此,修改原始 list 会影响 c2.list。
最佳实践:
List)配合 copy() 来避免此类副作用。题目:
class Wrapper<out T>
val instanceVariableOne : Wrapper<Nothing> = Wrapper<Any>()//Line A
val instanceVariableTwo : Wrapper<Any> = Wrapper<Nothing>()//Line B
选项: a) Both lines A and B compile b) Lines A and B do not compile c) Line A compiles; Line B does not d) Line B compiles; Line A does not
答案: d
解析:
Wrapper 声明为 out T(协变)。Nothing 是所有类型的子类型,Any 是所有类型的超类型。Wrapper<Nothing> 是 Wrapper<Any> 的子类型。将 Wrapper<Any> 赋值给 Wrapper<Nothing> 变量违反了协变规则(不能将超类型赋给子类型变量)。编译失败。Wrapper<Any> 是 Wrapper<Nothing> 的超类型。将 Wrapper<Nothing> 赋值给 Wrapper<Any> 变量符合协变规则。编译成功。题目: 涉及顶层函数、成员函数、扩展函数、扩展成员函数的调用优先级。
答案: Extension receiver rule (扩展接收者规则)
解析: Kotlin 调用解析优先级从高到低通常为:
但在特定上下文中,显式的扩展接收者(Receiver)优先级较高。本题考察的是当存在多个同名函数时,编译器如何选择。通常,更具体的接收者优先级更高。
题目:
var i = 0
println(i.inc())
println(i.inc())
var j = 0
println(j++)
println(++j)
选项: a) 0, 1, 0, 1 b) 0, 1, 0, 2 c) 1, 1, 0, 2 d) 1, 2, 0, 1
答案: c
解析:
i.inc() 返回新值但不修改 i。第一次打印 1,第二次打印 1(因为 i 仍为 0)。j++ 后缀自增,返回旧值 0,然后 j 变为 1。++j 前缀自增,j 变为 2,返回新值 2。题目:
考察 return 在匿名函数与 Lambda 中的不同行为。
答案: 1134 (对应选项 B)
解析:
f1: forEach 是内联函数,return 直接跳出外层函数 f1。遇到 2 时返回,后续 3, 4 不执行。输出 1。f2: 使用命名参数 fun(it) 实际上是匿名函数,return 仅跳出匿名函数,继续执行 forEach。输出 1, 3, 4。题目:
val j = wtf@ { n: Int -> wtf@ (wtf@ n + wtf@ 2) }(10)
println(j)
答案: 12
解析:
标签主要用于控制 break 或 continue,或者在嵌套 Lambda 中指定 return 的目标。在此处,标签并未改变表达式的求值逻辑,只是语法上的标记。(10 + 2) = 12。
题目:
val x: Int? = 2
val y: Int = 3
val sum = x?:0 + y
println(sum)
答案: 2
解析:
Elvis 运算符 ?: 的优先级低于加法 +。表达式等价于 x ?: (0 + y)。即 x ?: 3。因为 x 为 2,所以结果为 2。
题目: 考察枚举类扩展函数的定义与调用。
答案: Green (对应选项 B)
解析:
扩展函数可以直接定义在枚举类上,无需 Companion Object。Color.from(...) 是合法的扩展函数调用。返回 Color.Green。
题目:
fun hello(block: String.() -> Unit) {
"Hello1".block()
block("Hello2")
}
hello { println(this) }
答案: Hello1Hello2
解析:
String.() -> Unit 表示带接收者的 Lambda。"Hello1".block() 中 this 为 "Hello1"。block("Hello2") 中,block 被调用,参数 "Hello2" 作为接收者传入,this 为 "Hello2"。题目:
考察 apply 中 this 的指向。
答案: bar
解析:
在 foo.apply { return this } 中,this 指的是 apply 的接收者对象,即 IAm 实例。但 foo 是 String 属性。return this 返回的是 IAm 对象本身,还是字符串?代码中 foo.apply 的接收者是 foo (String)。return this 返回 String "bar"。
题目:
operator fun String.invoke(x: () -> String) = this + x()
fun String.z() = "!$this"
fun String.toString() = "$this!"
println("x"{"y"}.z())
答案: !xy
解析:
"x"{"y"} 调用 invoke 函数,this 为 "x",x() 返回 "y"。结果为 "xy"。.z() 调用扩展函数,结果为 "!xy"。题目:
考察 lazy 的线程安全与异常处理。
答案: 1
解析:
lazy 委托在首次访问时计算值。如果抛出异常,下次访问会重新计算。try-catch 捕获异常后修改 x,再次访问 y 时重新计算得到 1。
题目:
考察 forEach 中 return 的行为。
答案: 12
解析:
在 forEach 的 Lambda 中使用 return 会直接退出整个 numbers 函数,而不是仅仅退出 Lambda。因此 "ok" 不会打印。
题目: 考察 Lambda 作为最后一个参数时的省略括号写法。
答案: twotwo
解析:
foo {}:Lambda 作为最后一个参数,省略括号,对应 two 参数(如果有默认值)或第一个参数?foo {} 会将 {} 传给最后一个参数。若 foo(one={}, two={}),则 {} 传给 two。foo({}):括号内的内容传给第一个参数 one。foo { println(it) } 传给 two,输出 "two"。foo({ println(it) }) 传给 one,输出 "one"。通过上述题目的演练,我们深入理解了 Kotlin 编译器在处理集合、函数、属性及 Lambda 时的行为模式。这些知识点不仅有助于通过技术面试,更重要的是能帮助我们在日常开发中规避潜在的类型安全问题和逻辑错误。建议开发者在阅读官方文档的同时,结合此类实战题目进行巩固,从而真正掌握语言的精髓。
希望你在接下来的 Kotlin 进阶之路上,能够避开这些陷阱,写出更加优雅、健壮的代码。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online