跳到主要内容 Kotlin 语言核心特性与常见陷阱测试题解析 | 极客日志
Kotlin 大前端 java
Kotlin 语言核心特性与常见陷阱测试题解析 本文通过一系列 Kotlin 编程谜题,深入探讨了语言的核心特性与常见陷阱。内容涵盖 Lambda 表达式、字符串处理、控制流、面向对象设计、集合操作、扩展函数及类型系统等关键知识点。每个题目均提供了详细的代码示例、选项分析及正确答案解释,重点剖析了编译器行为、静态绑定机制及惰性求值等底层原理。文章旨在帮助开发者摆脱对 IDE 的过度依赖,提升对 Kotlin 语言本质的理解,从而编写出更严谨高效的代码。
w795471 发布于 2025/2/6 更新于 2026/4/21 2 浏览在不借助 IDE 的情况下,测试你的代码理解能力。以下是一系列 Kotlin 编程谜题,旨在考察对编译器行为、类型系统和语言特性的深入理解。
Scala-like functions fun hello () = {
println("Hello, World" )
}
hello()
选项:
a) 编译失败
b) 打印 "Hello, World"
c) 无输出
d) 其他
分析:
在 Kotlin 中,函数体如果是 lambda 表达式,返回的是该 lambda 本身,而不是执行结果。因此 hello() 返回的是一个 () -> Unit 类型的对象,而不是 Unit。要执行这个 lambda,需要使用 hello()() 或者 hello().invoke()。IDE 的 Lint 通常会提示 "Unused return value of a function with lambda expression body"。
这让人联想到 Flutter 中的立即执行函数表达式 (IIFE),即 (() => {})()。
Indent trimming val world = "multiline world"
println(
"""
Hello
$world
""" .trimIndent()
)
选项:
a) 保留原始缩进
b) 去除首尾空行但保留内部缩进
c) 根据最短行的缩进来修剪所有行
d) 编译失败
分析:
在 Kotlin 中,多行字符串(Raw String)由三引号定义,不包含转义字符,但可以包含换行符。trimIndent() 方法会根据字符串中最短行的缩进量来修剪每一行的前导空白。如果字符串中包含反斜杠,在 Raw String 中它们就是普通字符,不需要转义。如果需要展示字面量的反斜杠,可以直接输入两个反斜杠 \\。
If-else chaining fun printNumberSign (num: Int ) {
if (num < 0 ) {
"negative"
} else if (num > 0 ) {
"positive"
} else {
"zero"
}.let { Log.d("xys" , it) }
}
printNumberSign(-2 )
Log.d("xys" , "," )
printNumberSign(0 )
Log.d("xys" , "," )
printNumberSign(2 )
选项:
a) negative,zero,positive
b) negative,zero,
c) negative,positive
d) ,zero,positive
分析:
在 Java 编译器处理之后,else if 结构实际上被转换为嵌套的 if-else。但在 Kotlin 中,if 是一个表达式,其值取决于分支的结果。上述代码中,.let { ... } 仅作用于最后一个 else if 分支的结果。第一个 if 分支返回 "negative",但该值未被使用,且没有显式返回或打印。只有当进入最后的 else 分支时,才会触发 .let 并打印 "zero"。同理,第二个 else if 分支返回 "positive" 也不会被 .let 捕获。为了避免这种情况,可以将整个 if-else 块包裹在小括号中,然后在其上调用 .let。
Lambda runnables fun run () {
val run: () -> Unit = {
println("Run run run!" )
}
Runnable { run() }.run()
}
run()
选项:
a) 打印 "Run run run!"
b) 编译失败
c) StackOverflowError
d) 以上都不是
分析:
这道题考察的是 Kotlin 局部函数的作用域和闭包。上面的代码中,run 变量是一个 lambda。在 Runnable 构造函数中传入的 lambda { run() } 会捕获外部的 run 变量。虽然函数名也是 run,但这里使用的是变量 run。逻辑等价于将 lambda 提取为独立变量后调用。使用局部函数可以将逻辑隐藏在函数内部,避免命名冲突。
Making open abstract open class A {
open fun a () {}
}
abstract class B : A () {
abstract override fun a ()
}
open class C : B ()
选项:
a) 编译通过
b) 错误:类 'C' 不是抽象类且未实现抽象基类成员
c) 错误:'a' 未重写任何内容
d) 错误:函数 'a' 必须有主体
分析:
我们可以用抽象函数来覆写一个 open 的函数,但这会使子类必须实现该方法。在 B 类中,a() 被声明为 abstract override,这意味着 C 类作为 B 的非抽象子类,必须提供 a() 的具体实现。否则编译器会报错。正确的做法是在 C 类中覆写 a()。
List minus list val list = listOf(1 , 2 , 3 )
println(list - 1 )
println(list - listOf(1 ))
val ones = listOf(1 , 1 , 1 )
println(ones - 1 )
println(ones - listOf(1 ))
选项:
a) [2, 3][2, 3][1, 1][1, 1]
b) [2, 3][2, 3][1, 1][]
c) [1, 3][2, 3][][1, 1]
d) [2, 3][2, 3][][]
分析:
这道题考察 minus 函数的实现细节。在 Kotlin 集合中:
List minus T:移除第一个匹配的元素。
List minus List:从第一个 List 中,移除第二个 List 中存在的所有元素(按数量对应)。
因此,listOf(1, 1, 1) - listOf(1) 只会移除一个 1,剩下 [1, 1]。而 listOf(1, 1, 1) - 1 同样只移除第一个 1。
Composition operator fun (() -> Unit ).plus(f: () -> Unit ): () -> Unit = {
this ()
f()
}
({ print("Hello, " ) } + { print("World" ) })()
选项:
a) 打印 "Hello, World"
b) 错误:期望顶层声明
c) 错误:表达式 f 不能作为函数调用
d) 错误:未解析引用(操作符 + 未定义)
e) 运行正常但不打印
分析:
操作符重载函数 plus 的定义是正确的。它返回一个新的函数(使用 lambda 表达式创建),该函数依次执行两个参数函数。当我们添加两个函数时,我们得到了另一个可调用的函数。调用它时,lambda 表达式会按顺序执行。
What am I? val whatAmI = {}()
println(whatAmI)
选项:
a) "null"
b) "kotlin.Unit"
c) 不打印任何东西
d) 编译失败
分析:
这道题考察 lambda 表达式的基本知识。空 lambda {} 的类型是 () -> Unit。调用 {}() 会执行该 lambda,其返回类型为 Unit。因此 println 会打印 "kotlin.Unit"。
Return return fun f1 () : Int {
return return 42
}
fun f2 () {
throw throw Exception()
}
选项:
a) 返回 42; 抛出异常
b) 返回 42; 编译失败
c) 编译失败; 抛出异常
d) 编译失败; 编译失败
分析:
在 Kotlin 中,return 表达式有返回类型,可以作为表达式使用。在 f1 中,内部的 return 42 返回 Int,外部的 return 接收该值并结束函数。虽然 IDE 可能会提示冗余,但语法上是合法的。同样地,throw 声明类型为 Nothing,也是一个返回类型。throw throw Exception() 中,内层 throw 返回 Nothing,外层 throw 接收 Nothing 并抛出异常。因此两个函数都能编译,f1 返回 42,f2 抛出异常。
Extensions are resolved statically open class C
class D : C ()
fun C.foo () = "c"
fun D.foo () = "d"
fun printFoo (c: C ) {
println(c.foo())
}
printFoo(D())
选项:
a) 编译失败
b) 运行时错误
c) c
d) d
分析:
这个例子考察扩展函数的具体实现原理。因为被调用的扩展函数只依赖于参数 c 的声明类型(即 C 类),所以它只会调用 C 类的扩展函数 foo。扩展函数不会修改它们所扩展的类,只是让新的函数可以在这个类型的变量上用点号来调用,相当于一层 Wrapper。即使传入的是 D 的实例,由于静态绑定,仍调用 C 的扩展函数。
Expression or not fun f1 () {
var i = 0
val j = i = 42
println(j)
}
fun f2 () {
val f = fun () = 42
println(f)
}
fun f3 () {
val c = class C
println(c)
}
选项:
a) 42 () -> kotlin.Int class C
b) 42 () -> kotlin.Int 编译失败
c) 编译失败 () -> kotlin.Int 编译失败
d) 编译失败 编译失败 编译失败
分析:
变量初始化和类声明都是 Kotlin 中的语句,它们没有声明任何返回类型,所以我们不能将这种声明分配给变量,因此不能编译。在 f1 中,i = 42 是赋值语句,返回 Unit,但 val j = ... 试图将其赋值为 Int 类型(推断),导致类型不匹配或编译错误(取决于具体上下文,通常赋值表达式返回 Unit)。在 f2 中,fun() = 42 是匿名函数,可以赋值给变量。在 f3 中,class C 是类声明语句,不能作为表达式使用,无法赋值给 val c。
Eager or lazy? val x = listOf(1 , 2 , 3 ).filter {
print("$it " )
it >= 2
}
print("before sum " )
println(x.sum())
选项:
a) 1 2 3 before sum 5
b) 2 3 before sum 5
c) before sum 1 2 3 5
d) 顺序不确定
分析:
与 Java 8 的 Stream API 不同,Kotlin 中的集合扩展函数(如 filter)是 Eager(急切)的。这意味着它们在调用时立即执行过滤逻辑。如果需要使用 Lazy(惰性)方式,可以使用 sequenceOf 或 asSequence,序列都是使用的惰性初始化。
Map default val map = mapOf<Any, Any>().withDefault { "default" }
println(map["1" ])
选项:
a) default
b) nothing
c) null
d) 编译失败
分析:
不要被 withDefault 的字面意义骗了。mapOf 返回的是不可变映射,且 withDefault 只能用于委托属性的场景(如 mutableMapOf 配合属性代理)。对于普通的 mapOf 创建的不可变 Map,访问不存在的键会返回 null,而不是默认值。withDefault 的正确用法通常配合 mutableMapOf 和属性委托:
val map = mutableMapOf<String, Set<String>>().withDefault { mutableSetOf() }
var property: Set<String> by map
Null empty val s: String? = null
if (s?.isEmpty()) println("is empty" )
if (s.isNullOrEmpty()) println("is null or empty" )
选项:
a) is empty is null or empty
b) is null or empty
c) 不打印任何东西
d) 编译失败
分析:
当 s == null 时,s?.isEmpty() 会返回 null。因此,这个表达式的返回类型应该是 Boolean?。在 if 条件中,Kotlin 要求布尔类型为 Boolean,不能是 Boolean?,所以不能编译通过。可以通过下面的方式进行修改:
val s: String? = null
if (s?.isEmpty() == true ) {
println("is empty" )
}
if (s.isNullOrEmpty()) {
println("is null or empty" )
}
List or not val x = listOf(1 , 2 , 3 )
println(x is List<*>)
println(x is MutableList<*>)
println(x is java.util.List<*>)
选项:
a) true false true
b) false false true
c) true true true
d) true false false
分析:
在 Kotlin 中,listOf、MutableList、Java ArrayList,返回的都是 java.util.List 的实现。Kotlin 的 List 接口继承自 Java 的 java.util.List。因此它们的类型检查是一致的。
Everything is mutable val readonly = listOf(1 , 2 , 3 )
if (readonly is MutableList) {
readonly.add(4 )
}
println(readonly)
选项:
a) [1, 2, 3]
b) [1, 2, 3, 4]
c) UnsupportedOperationException
d) 编译失败
分析:
类似 listOf、Array.asList() 这样的 Helper functions 它们返回的是 java.util.Arrays$ArrayList 的包装器,或者是不可变的视图,而不是真正的 java.util.ArrayList。虽然类型检查可能通过(取决于具体实现),但在尝试修改时,底层实现会抛出 UnsupportedOperationException。在标准库中,listOf 返回的是不可变列表,直接调用 add 会抛错。
Fun with composition val increment = { i: Int -> i + 1 }
val bicrement = { i: Int -> i + 2 }
val double = { i: Int -> i * 2 }
val one = { 1 }
private infix fun <T, R> (() -> T).then(another: (T) -> R): () -> R = { another(this ()) }
operator fun <T, R1, R2> ((T) -> R1).plus(another: (T) -> R2) = { x: T -> this (x) to another(x) }
val equilibrum = one then double then (increment + bicrement)
println(equilibrum())
选项:
a) Nothing, it doesn't compile
b) 5
c) (1, 2)
d) (3, 4)
分析:
这是一个经典的复合函数问题。one 返回 1。通过 then 中缀运算符 double 后,变成了 2。在 plus 中,increment 和 bicrement 分别作用于 2,得到 3 和 4。plus 返回一个 Pair (3, 4)。注意 private 修饰符在顶层文件中无效,应移除以保证代码可运行。
总结 很多人问,这些玩意儿到底有啥用?很多代码放 IDE 里面就能知道到底是对是错,运行结果是什么,为什么还要这样去做呢?
实际上,理解这些东西,对你的编程思维和对语言的理解能力会有很大帮助。在 IDE 里面,它帮助我们做了太多的事,以至于我们很多时候都不能真正发现问题的本质是什么。借助这些题目的训练,我们可以理解编译器是如何处理代码的,可以理解代码是如何执行的,这才是我们训练这些题目的目的。掌握这些底层机制,能帮助你在面对复杂业务逻辑时写出更健壮、性能更优的代码。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online