【水】Kotlin Puzzlers 改
《Java Puzzlers》(中文名《Java 解惑》)里面讲解了许多 Java 语言的大坑,相信各位julao应该都看过。Kotlin 作为「a better Java」,在填补一些坑的同时,不可避免地引入了许多新坑。本来本鶸进行了一段时间的取材,想要写一篇《Kotlin Puzzlers》的,可谁知 已经有人早就把我的饭碗抢走了,而且素材比我还多,可恶!
这篇文章就是本鶸看完录像(油土鳖上面有)以及 Github 上的完整内容后,将一些比较坑的谜题拿出来报复社会,蛐蛐一篇观后感而已。
题型为选择题,本辣鸡博客没有NGA的折叠,没有萌百的黑幕,为了防止一眼瞄到答案而造成剧透,本文对答案(以及解释)的摆放位置做了调整,例如第一题的答案被我放在了第二题的位置(以此类推)。各位看客从上到下开始阅读就好了。
使用的 Kotlin 版本为1.2。
没有坑到我的谜题
虽然没有被坑,比较简单,但是值得注意的题。一些更加简单的题目就不放上来了。想刷一遍完整题库的同学可以到 GitHub 上面找。
强力返回 ~ Power Return
fun hello(): String {
val result = return throw return "Hello"
println(result.toString())
}
println(hello())
这会打印出什么?
a) Hello
b) 两个Hello
c) 这破代码根本没法通过编译
d) 以上答案都不对
本题答案(以及解释)在下一题那里。(以此类推)
计划生育 ~ One Chile Policy
open class Node(val name: String) {
fun lookup() = println(name)
}
class Parent : Node("parent") {
fun child(name: String): Node? = Node(name)
val child1 = child("child1")?.apply { lookup() }
val child2 = child("child2").apply { lookup() }
}
Parent()
这会打印出什么?
a) child1 和 child2
b) child1 和 parent
c) parent 和 child2
d) 以上答案都不对
上一题的答案:a
要记住,return ***
和 throw ***
都是表达式,其结果的类型为 Nothing
,Nothing
类型是任意类型的子类型,所以 Nothing
可以被抛出,可以被返回,可以赋值给任意类型的变量。事实上 hello()
在 return "Hello"
的时候已经结束了,剩下的 throw
、val result
、println()
什么的都是不可到达代码(unreachable code),不会被运行。
PS:你甚至可以写出这样的代码:throw throw throw Exception()
两只拉姆达跑得快 ~ Two Lambda
typealias L = (String) -> Unit
fun foo(one: L = {}, two: L = {}) {
one("one")
two("two")
}
foo { print(it) }
foo({ print(it) })
这会打印出什么?
a) oneone
b) twotwo
c) onetwo
d) 以上答案都不对
上一题的答案:b
事实上是 child1
和 parent
。Kotlin
的这些扩展方法如 apply
、let
、also
等等都是适用于所有类型的,包括可空类型。child2
那行 apply
函数接收的拉姆达表达式的类型其实是 Node?.() -> Unit
,如果 child2
那行代码是写在 Parent
类的外面的话,你就会发现这行代码根本没法通过编译,这里面调用的 lookup
实际上是 parent
的 lookup
。(你可以把 apply
换成 also
试试。)
衔尾蛇 ~ Cyclic Object Constructions
open class A(val x: Any?)
object B : A(C)
object C : A(B)
print(B.x)
print(C.x)
这会打印出什么?
a) nullnull
b) C@********null
c) ExceptionInInitializerError
d) 这破代码根本没法通过编译
上一题的答案:d
实际上是 twoone
。第一句的语法只有在拉姆达表达式是最后一个参数的时候才能写的,所以是 two
。第二句是普通的方法调用,先填上第一个参数,第二个参数使用默认值。
PS:想要朴素地实现 foo { } { }
这样的调用的话应该是办不到的吧。(如果能做到请赶快告诉我!)
哇,好长 ~ Breaking Lines
val anExtremelyLongAndBoringStatementThatBarelyFitsOnALine = 2
val anotherExtremelyLongStatementThatBarelyFitsOnALine = 2
val someList = listOf(1)
val result = someList.map {
anExtremelyLongAndBoringStatementThatBarelyFitsOnALine
+ anotherExtremelyLongStatementThatBarelyFitsOnALine
}
print(result)
这会打印出什么?
a) [1]
b) [2]
c) [4]
d) [1, 4]
上一题的答案:b
B
初始化需要 C
,C
初始化需要 B
。咦,B
还没初始化完成呢,那么哪来的 B
呢,只能是 null
了啊!
更详细的解释可以看这里:https://github.com/Kotlin/kotlin-spec/blob/spec-old/kotlin-spec.asc#singleton-objects
换个名字你就不认识我了 ~ Good Child Has Many Names
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(d.sum(x = 0))
println()
这会打印出什么?
a) 22
b) 11
c) 21
d) 这破代码根本没法通过编译
上一题的答案:b
之前裙里有julao问过类似的问题所以我没被坑到。你可以把代码丢到IDEA里面,光标定位到加号前面,按下 Ctrl+B
或者 Ctrl+Q
,看看那个加号是什么意思吧。解决方法:把加号放在上一行的后面可破。
排序 ~ Sorting
val list = arrayListOf(1, 5, 3, 2, 4)
val sortedList = list.sort()
print(sortedList)
这会打印出什么?
a) [1, 5, 3, 2, 4]
b) [1, 2, 3, 4, 5]
c) kotlin.Unit
d) 这破代码根本没法通过编译
上一题的答案:c
命名参数是静态分配的。
致命的顺序 ~ The Order
class Order {
private val c: String
init {
the()
c = ""
}
private fun the() {
println(c.length)
}
}
Order()
这会打印出什么?
a) 0
b) null
c) 这破代码根本没法通过编译
d) 以上答案都不对
上一题的答案:c
参见 https://zhuanlan.zhihu.com/p/27234651
本题的答案:d
JVM 不想理你并向你抛出了一只 NPE。Java 也有这个问题,Scala 不熟悉不清楚。同样是 JVM 平台上的语言,Ceylon 就没有这个问题,Ceylon 官网的文档有这个问题的说明,可以参考一下(趁机吹一波 Ceylon)。
Kotlin 官方人员曾表示过修复这个缺陷是一件十分困难的事。
那些坑了我的迷题
我果然是鶸,错了这么多,进入自卑模式~
区间测试 ~ Inclusive Range
val i = 10.5
when (i) {
in 1..10 -> println("in")
!in 1..10 -> println("!in")
else -> println("else")
}
这会打印出什么?
a) in
b) !in
c) else
d) 这破代码根本没法通过编译
据说这道题在 Kotlin 1.0 版本和 1.2 版本里有不同的表现。(我懒得试旧版本了)
极性不定 ~ Weird Chaining
fun printNumberSign(num: Int) {
if (num < 0) {
"negative"
} else if (num > 0) {
"positive"
} else {
"zero"
}.let { println(it) }
}
printNumberSign(-2)
printNumberSign(0)
printNumberSign(2)
这会打印出什么?
a) negative; zero; positive
b) negative; zero
c) negative; positive
d) zero; positive
上一题的答案:a
实际上是把 i
转成 Int
再进行的比较。
$_$ ~ Dollar In Multiline Literals
val multiline = """
To win \$999.999 execute "rm -fr \$HOME/kotlin-puzzlers/*"
""".trimIndent()
println(multiline)
这会打印出什么?
a) To win \$999.999 execute "rm -fr \$HOME/kotlin-puzzlers/*"
b) To win 999.999 execute "rm -fr \/home/user/kotlin-puzzlers/*"
c) To win $999.999 execute "rm -fr $HOME/kotlin-puzzlers/*"
d) 这破代码根本没法通过编译
上一题的答案:d
相当于:
if (num < 0) {
"negative"
} else {
if (num > 0) { "positive" }
else { "zero" }.let { println(it) }
}
秒懂!
解决方法:用小括号将那串 if else
括起来再接 let
可破。
你的名字是 ~ Property Override
open class Named {
open var name: String? = null
get() = field ?: "<unnamed>"
}
class Person: Named() {
override var name: String? = null
get() = super.name
set(value) { field = "Mr $value" }
}
val person = Person()
person.name = "Anton"
println(person.name)
这会打印出什么?
a) Anton
b) Mr Anton
c) <unnamed>
d) null
上一题的答案:d
这种 raw string 里面美元符号 $
一直都是表示模板表达式,而且不能被转义,所以 $HOME
这里糟了。(你问为什么 $999.999
没糟?因为 999.999
不是合法的变量名啊,你在 999.999
两边加上反引号试试。)
解决方法:”””${‘$’}HOME”””
冰雪聪明 ~ Custom Getter Smartcast
class SmartCastable {
val list: List<Int> = mutableListOf(1, 2, 3)
val set: Set<Int> = mutableSetOf(1, 2, 3)
get() = field
}
val sc = SmartCastable()
if(sc.list is MutableList)
sc.list.add(4)
if(sc.set is MutableSet)
sc.set.add(4)
println("${sc.list}, ${sc.set}")
这会打印出什么?
a) [1, 2, 3], [1, 2, 3]
b) [1, 2, 3, 4], [1, 2, 3, 4]
c) UnsupportedOperationException
d) 这破代码根本没法通过编译
上一题的答案:c
这里有两个 backing field
,Named
类的那个 get
方法操纵了父类的 backing field
,set
方法操纵的是自己的 backing field
。
解决方法:
class Person: Named() {
override var name: String?
get() = super.name
set(value) { super.name = "Mr $value" }
}
最小值 ~ MinInt
fun printInt(n: Int) {
println(n)
}
printInt(-2_147_483_648.inc())
这会打印出什么?
a) -2147483647
b) -2147483649
c) 2147483647
d) 以上答案都不对
上一题的答案:d
sc.set
有一个自定义 getter
,编译器没法判断这个 getter
返回的是否是同一个对象,所以无法进行智能转换(smart cast)。
解决方法:这时候别声明只有 getter
的属性,声明有 backing field
的属性就好。或者像这样:
val set = sc.set
if(set is MutableSet)
set.add(4)
人类衰退之后 ~ Population To Mars
class Population(var cities: Map<String, Int>) {
val 帝都 by cities
val 魔都 by cities
val 妖都 by cities
}
val population = Population(mapOf(
"帝都" to 864_816,
"魔都" to 413_782,
"妖都" to 43_005
))
// 许多年过去了,地球毁灭了,只有少数幸存者抵达了火星(大吉大利今晚吃鸡)!
population.cities = emptyMap()
with(population) {
println("$帝都; $魔都; $妖都")
}
这会打印出什么?
a) 0; 0; 0
b) 864816; 413782; 43005
c) NullPointerException
d) NoSuchElementException
上一题的答案:d
破代码没法通过编译。实际的求值顺序是:-(2_147_483_648.inc())
,这TM是个 Long
。这个一元操作符的优先级比普通方法调用低。
反物质 ~ AntiMatter
operator fun Nothing?.not() = Unit
operator fun Unit.not() = null
val foo = null
println(!!!!!foo!!!!)
这会打印出什么?
a) null
b) kotlin.Unit
c) KotlinNullPointerException
d) 这破代码根本没法通过编译
上一题的答案:b
用于委托代理的那个 Map
被保存在了一个 private final
的 field
里面,正常手段没法赋新值。
本题的答案:d
null
的类型是 Nothing?
(而且是这个类型的唯一值)。***!!
这个非空断言比 not()
的优先级要高,所以 foo!!!!
的类型是 Nothing
,Nothing
是所有类型的子类型,所以编译器没法判断该调用哪个扩展方法。
总结
这些辣鸡代码,别学。

本作品采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,转载请注明出处。
本文链接:https://aisia.moe/2017/12/28/kotlin-puzzlers/