《Java Puzzlers》(中文名《Java 解惑》)里面讲解了许多 Java 语言的大坑,相信各位julao应该都看过。Kotlin 作为「a better Java」,在填补一些坑的同时,不可避免地引入了许多新坑。本来本鶸进行了一段时间的取材,想要写一篇《Kotlin Puzzlers》的,可谁知 已经有人早就把我的饭碗抢走了,而且素材比我还多,可恶!
这篇文章就是本鶸看完录像(油土鳖上面有)以及 Github 上的完整内容后,将一些比较坑的谜题拿出来报复社会,蛐蛐一篇观后感而已。
题型为选择题,本辣鸡博客没有NGA的折叠,没有萌百的黑幕,为了防止一眼瞄到答案而造成剧透,本文对答案(以及解释)的摆放位置做了调整,例如第一题的答案被我放在了第二题的位置(以此类推)。各位看客从上到下开始阅读就好了。
使用的 Kotlin 版本为1.2。
虽然没有被坑,比较简单,但是值得注意的题。一些更加简单的题目就不放上来了。想刷一遍完整题库的同学可以到 GitHub 上面找。
fun hello(): String { val result = return throw return "Hello" println(result.toString()) } println(hello())
这会打印出什么? a) Hello b) 两个Hello c) 这破代码根本没法通过编译 d) 以上答案都不对
本题答案(以及解释)在下一题那里。(以此类推)
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()
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) 以上答案都不对
上一题的答案:d
事实上是 child1
和 parent
。 Kotlin
的这些扩展方法如 apply
、 let
、 also
等等都是适用于所有类型的,包括可空类型。 child2
那行 apply
函数接收的拉姆达表达式的类型其实是 Node?.() -> Unit
,如果 child2
那行代码是写在 Parent
类的外面的话,你就会发现这行代码根本没法通过编译,这里面调用的 lookup
实际上是 parent
的 lookup
。(你可以把 apply
换成 also
试试。)
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 { } { }
这样的调用的话应该是办不到的吧。(如果能做到请赶快告诉我!)
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
了啊!
参见 http://jetbrains.github.io/kotlin-spec/#_singleton_objects
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
,看看那个加号是什么意思吧。解决方法:把加号放在上一行的后面可破。
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
命名参数是静态分配的。
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 不熟悉不清楚。据说 Ceylon 就没有这个问题,具体可以看 Ceylon 官网上的说明(趁机吹一波 Ceylon)。
https://ceylon-lang.org/documentation/1.3/tour/initialization/#definite_assignment_and_definite_initialization
Kotlin 官方人员曾表示修复这个缺陷是一件十分困难的事。
我果然是鶸,错了这么多,进入自卑模式~
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 版本里有不同的表现。(我懒得试旧版本了)
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
再进行的比较。
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
可破。
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”””
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" } }
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)
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
。这个一元操作符的优先级比普通方法调用低。
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
是所有类型的子类型,所以编译器没法判断该调用哪个扩展方法。