# 答疑(一)| Java和Kotlin到底谁好谁坏? 你好,我是朱涛。 由于咱们课程的设计理念是简单易懂、贴近实际工作,所以我在课程内容的讲述上也会有一些侧重点,进而也会忽略一些细枝末节的知识点。不过,我看到很多同学都在留言区分享了自己的见解,算是对课程内容进行了很好的补充,这里给同学们点个赞,感谢你的仔细思考和认真学习。 另外,我看到不少同学提出的很多问题也都非常有价值,有些问题非常有深度,有些问题非常有实用性,有些问题则非常有代表性,这些问题也值得我们再一起探讨下。因此,这一次,我们来一次集中答疑。 ## Java和Kotlin到底谁好谁坏? 很多同学看完[开篇词](https://time.geekbang.org/column/article/472129)以后,可能会留下一种印象,就是貌似Java就是坏的,Kotlin就是好的。但其实在我看来,语言之间是不存在明确的优劣之分的。“XX是世界上最好的编程语言”这种说法,也是没有任何意义的。 不过,虽然语言之间没有优劣之分,但在特定场景下,还是会有更优选择的。比如说,站在Android开发的角度上看,Kotlin就的确要比Java强很多;但如果换一个角度,服务端开发,Kotlin的优势则并不明显,因为Spring Boot之类的框架对Java的支持已经足够好了;甚至,如果我们再换一个角度,站在性能、编译期耗时的视角上看,Kotlin在某些情况下其实是略逊于Java的。 如果用发展的眼光来看待这个问题的话,其实这个问题根本不重要。Kotlin是一门基于JVM的语言,它更像是站在了巨人的肩膀上。**Kotlin的设计思路就是“扬长避短”。**Java的优点,Kotlin都可以拿过来;Java的缺点,Kotlin尽量都把它扔掉!这就是为什么很多人会说:Kotlin是一门更好的Java语言(Better Java)。 在开篇词里,我曾经提到过Java的一些问题:语法表现力差、可读性差,难维护、易出错、并发难。而这并不是说Java有多么不好,我想表达的其实是这两点: * **Java太老了**。Java为了自身的兼容性,它的语法很难发展和演进,这才导致它在几十年后的今天看起来“语法表现力差”。 * **不是Java变差了,而是Kotlin做得更好了**。因为Kotlin的理念就是扬长避短,因此,在Java特别容易出错的领域,Kotlin做了足够多的优化,比如内部类默认静态,比如不允许隐式的类型转换,比如挂起函数优化异步逻辑,等等。 所以,Kotlin一定就比Java好吗?结论是并不一定。但在大部分场景下,我会愿意选Kotlin。 ## Double类型字面量 在Java当中,我们会习惯性使用“1F”代表Float类型,“1D”代表Double类型。但是这一行为在Kotlin当中其实会略有不同,而我发现,很多同学都会下意识地把Java当中的经验带入到Kotlin(当然也包括我)。 ```plain // 代码段1 val i = 1F // Float 类型 val j = 1.0 // Double 类型 val k = 1D // 报错!! ``` 实际上,在Kotlin当中,要代表Double类型的字面量,我们只需要**在数字末尾加上小数位**即可。“1D”这种写法,在Kotlin当中是不被支持的,我们需要特别注意一下。 ## 逆序区间 在[第1讲](https://time.geekbang.org/column/article/472154)里,我曾提到过:如果我们想要逆序迭代一个区间,不能使用“6…0”这种写法,因为这种写法的区间要求是:右边的数字大于等于左边的数字。 ```plain // 代码段2 fun main() { for (i in 6..0) { println(i) // 无法执行 } } ``` 在我们实际工作中,我们也许不会直接写出类似代码段2这样的逻辑,但是,当我们的区间范围变成变量以后,这个问题就没那么容易被发现了。比如我们可以看看下面这个例子: ```plain // 代码段3 fun main() { val start = calculateStart() // 6 val end = calculateEnd() // 0 for (i in start..end) { println(i) } } ``` 在这段代码中,如果end小于start,我们就很难通过读代码发现问题了。所以在实际的开发工作中,我们其实应该慎重使用“start…end”的写法。如果我们不管是正序还是逆序都需要迭代的话,这时候,我们可以考虑封装一个全局的顶层函数: ```plain // 代码段4 fun main() { fun calculateStart(): Int = 6 fun calculateEnd(): Int = 0 val start = calculateStart() val end = calculateEnd() for (i in fromTo(start, end)) { println(i) // end 小于start,无法执行 } } fun fromTo(start: Int, end: Int) = if (start <= end) start..end else start downTo end ``` 在上面的fromTo()当中,我们对区间的边界进行了简单的判断,如果左边界小于右边界,我们就使用逆序的方式迭代。 ## 密封类优势 在[第2讲](https://time.geekbang.org/column/article/473349)中,有不少同学觉得密封类不是特别好理解。在课程里,我们是拿密封类与枚举类进行对比来说明讲解的。我们知道,**所谓枚举,就是一组有限数量的值**。枚举的使用场景往往是某种事物的某些状态,比如,电视机有开关的状态,人类有女性和男性,等等。在Kotlin当中,同一个枚举,在内存当中是同一份引用。 ```plain enum class Human { MAN, WOMAN } fun main() { println(Human.MAN == Human.MAN) println(Human.MAN === Human.MAN) } 输出 true true ``` 那么**密封类,其实是对枚举的一种补充**。枚举类能做的事情,密封类也能做到: ```plain sealed class Human { object MAN: Human() object WOMAN: Human() } fun main() { println(Human.MAN == Human.MAN) println(Human.WOMAN === Human.WOMAN) } 输出 true true ``` 所以,密封类,也算是用了枚举的思想。但它跟枚举不一样的地方是:**同一个父类的所有子类**。举个例子,我们在IM消息当中,就可以定义一个BaseMsg,然后剩下的就是具体的消息子类型,比如文字消息TextMsg、图片消息ImageMsg、视频消息VideoMsg,这些子类消息的种类肯定是有限的。 而密封类的好处就在于,对于每一种消息类型,它们都可以携带各自的数据。 ```plain // 代码段5 sealed class BaseMsg { // 密封类可以携带数据 // ↓ data class TextMsg(val text: String) : BaseMsg() data class ImageMsg(val url: String) : BaseMsg() data class VideoMsg(val url: String) : BaseMsg() } ``` 所以我们可以说:**密封类,就是一组有限数量的子类**。针对这里的子类,我们可以让它们创建不同的对象,这一点是枚举类无法做到的。 那么,**使用密封类的第一个优势,**就是如果我们哪天扩充了密封类的子类数量,所有密封类的使用处都会智能检测到,并且给出报错: ```plain // 代码段6 sealed class BaseMsg { data class TextMsg(val text: String) : BaseMsg() data class ImageMsg(val url: String) : BaseMsg() data class VideoMsg(val url: String) : BaseMsg() // 增加了一个Gif消息 data class GisMsg(val url: String): BaseMsg() } // 报错!! fun display(data: BaseMsg): Unit = when(data) { is BaseMsg.TextMsg -> TODO() is BaseMsg.ImageMsg -> TODO() is BaseMsg.VideoMsg -> TODO() } ``` 上面的代码会报错,因为BaseMsg已经有4种子类型了,而when表达式当中只枚举了3种情况,所以它会报错。 **使用密封类的第二个优势**在于,当我们扩充了子类型以后,IDE可以帮我们快速补充分支类型: ![图片](https://static001.geekbang.org/resource/image/24/e6/24c3b78cd2e208f669f2804e7e9362e6.gif?wh=2088x1268) 不过,还有一点需要特别注意,那就是else分支。一旦我们在枚举密封类的时候使用了else分支,那我们前面提到的两个密封类的优势就会不复存在! ```plain sealed class BaseMsg { data class TextMsg(val text: String) : BaseMsg() data class ImageMsg(val url: String) : BaseMsg() data class VideoMsg(val url: String) : BaseMsg() // 增加了一个Gif消息 data class GisMsg(val url: String): BaseMsg() } // 不会报错 fun display(data: BaseMsg): Unit = when(data) { is BaseMsg.TextMsg -> TODO() is BaseMsg.ImageMsg -> TODO() // 注意这里 else -> TODO() } ``` 请留意这里的display()方法,当我们只有三种消息类型的时候,我们可以在枚举了TextMsg、ImageMsg以后,使得else就代表VideoMsg。不过,一旦后续增加了GifMsg消息类型,这里的逻辑就会出错。而且,在这种情况下,我们的编译器还不会提示报错! 因此,**在我们使用枚举或者密封类的时候,一定要慎重使用else分支。** ## 枚举类的valueOf() 另外,在使用Kotlin枚举类的时候,还有一个坑需要我们特别注意。在[第4讲](https://time.geekbang.org/column/article/473656)实现的第一个版本的计算器里,我们使用了valueOf()尝试解析了操作符枚举类。而这只是理想状态下的代码,实际上,正确的方式应该使用2.0版本当中的方式。 ```plain val help = """ -------------------------------------- 使用说明: 1. 输入 1 + 1,按回车,即可使用计算器; 2. 注意:数字与符号之间要有空格; 3. 想要退出程序,请输入:exit --------------------------------------""".trimIndent() fun main() { while (true) { println(help) val input = readLine() ?: continue if (input == "exit") exitProcess(0) val inputList = input.split(" ") val result = calculate(inputList) if (result == null) { println("输入格式不对") continue } else { println("$input = $result") } } } private fun calculate(inputList: List): Int? { if (inputList.size != 3) return null val left = inputList[0].toInt() // 注意这里 // ↓ val operation = Operation.valueOf(inputList[1])?: return null val right = inputList[2].toInt() return when (operation) { Operation.ADD -> left + right Operation.MINUS -> left - right Operation.MULTI -> left * right Operation.DIVI -> left / right } } enum class Operation(val value: String) { ADD("+"), MINUS("-"), MULTI("*"), DIVI("/") } ``` 请留意上面的代码注释,这个valueOf()是无法正常工作的。Kotlin为我们提供的这个方法,并不能为我们解析枚举类的value。 ```plain fun main() { // 报错 val wrong = Operation.valueOf("+") // 正确 val right = Operation.valueOf("ADD") } ``` 出现这个问题的原因就在于,**Kotlin提供的valueOf()就是用于解析“枚举变量名称”的**。 这是一个非常常见的使用误区,不得不说,Kotlin在这个方法的命名上并不是很好,导致开发者十分容易用错。Kotlin提供的valueOf()还不如说是nameOf()。 而如果我们希望可以根据value解析出枚举的状态,我们就需要自己动手。最简单的办法,就是使用**伴生对象**。在这里,我们只需要将2.0版本当中的逻辑挪进去即可: ```plain enum class Operation(val value: String) { ADD("+"), MINUS("-"), MULTI("*"), DIVI("/"); companion object { fun realValueOf(value: String): Operation? { values().forEach { if (value == it.value) { return it } } return null } } } ``` 对应的,在我们尝试解析操作符的时候,我们就不再使用Kotlin提供的valueOf(),而是使用自定义的realValueOf()了: ```plain val help = """ -------------------------------------- 使用说明: 1. 输入 1 + 1,按回车,即可使用计算器; 2. 注意:数字与符号之间要有空格; 3. 想要退出程序,请输入:exit --------------------------------------""".trimIndent() fun main() { while (true) { println(help) val input = readLine() ?: continue if (input == "exit") exitProcess(0) val inputList = input.split(" ") val result = calculate(inputList) if (result == null) { println("输入格式不对") continue } else { println("$input = $result") } } } private fun calculate(inputList: List): Int? { if (inputList.size != 3) return null val left = inputList[0].toInt() // 变化在这里 // ↓ val operation = Operation.realValueOf(inputList[1])?: return null val right = inputList[2].toInt() return when (operation) { Operation.ADD -> left + right Operation.MINUS -> left - right Operation.MULTI -> left * right Operation.DIVI -> left / right } } ``` 因此,对于枚举,我们在使用valueOf()的时候一定要足够小心!因为它解析的根本就不是value,而是name。 ## 小结 在我看来,专栏是“作者说,读者听”的过程,而留言区则是“读者说,作者听”的过程。这两者结合在一起之后,我们才能形成一个更好的沟通闭环。今天的这节答疑课,就是我在倾听了你的声音后,给到你的回应。 所以,如果你在学习的过程中遇到了什么问题,请一定要提出来,我们一起交流和探讨,共同进步。 ## 思考题 请问你在使用Kotlin的过程中,还遇到过哪些问题?请在留言区提出来,我们一起交流。