知识图库
Java知识库
JDK线程池实现原理
Java中的强、软、弱、虚引用
深入拆解Java虚拟机
01 开篇词 | 为什么我们要学习Java虚拟机?
02 Java代码是怎么运行的?
03 Java的基本类型
04 Java虚拟机是如何加载Java类的?
05 JVM是如何执行方法调用的?(上)
06 JVM是如何执行方法调用的?(下)
7 JVM是如何处理异常的?
Java面试常见问题整理
Java面试常见问题-Java 基础篇
Java面试常见问题-Jvm篇
Java面试常见问题-并发篇
Android知识库
Kotlin编程第一课
1 开篇词 | 入门Kotlin有多容易,精通Kotlin就有多难
2 Kotlin基础语法:正式开启学习之旅
3 面向对象:理解Kotlin设计者的良苦用心
4 Kotlin原理:编译器在幕后干了哪些“好事”?
5 实战:构建一个Kotlin版本的四则运算计算器
6 object关键字:你到底有多少种用法?
7 扩展:你的能力边界到底在哪里?
8 高阶函数:为什么说函数是Kotlin的“一等公民”?
9 实战:用Kotlin写一个英语词频统计程序
10 加餐一 | 初识Kotlin函数式编程
11 委托:你为何总是被低估?
12 泛型:逆变or协变,傻傻分不清?
13 注解与反射:进阶必备技能
14 实战:用Kotlin实现一个网络请求框架KtHttp
15 加餐二 | 什么是“表达式思维”?
16 加餐三 | 什么是“不变性思维”?
17 加餐四 | 什么是“空安全思维”?
18 春节刷题计划(一)| 当Kotlin遇上LeetCode
19 春节刷题计划(二)| 一题三解,搞定版本号判断
20 春节刷题计划(三)| 一题双解,搞定求解方程
21 春节刷题计划(四)| 一题三解,搞定分式加减法
22 什么是“协程思维模型”?
23 如何启动协程?
24 挂起函数:Kotlin协程的核心
25 Job:协程也有生命周期吗?
26 Context:万物皆为Context?
27 实战:让KtHttp支持挂起函数
28 期中考试 | 用Kotlin实现图片处理程序
29 题目解答 | 期中考试版本参考实现
30 Channel:为什么说Channel是“热”的?
31 Flow:为什么说Flow是“冷”的?
32 select:到底是在选择什么?
33 并发:协程不需要处理同步吗?
34 异常:try-catch居然会不起作用?坑!
35 实战:让KtHttp支持Flow
36 答疑(一)| Java和Kotlin到底谁好谁坏?
37 集合操作符:你也会“看完就忘”吗?
38 协程源码的地图:如何读源码才不会迷失?
39 图解挂起函数:原来你就是个状态机?
40 加餐五 | 深入理解协程基础元素
41 launch的背后到底发生了什么?
42 Dispatchers是如何工作的?
43 CoroutineScope是如何管理协程的?
44 图解Channel:如何理解它的CSP通信模型?
45 图解Flow:原来你是只纸老虎?
46 Java Android开发者还会有未来吗?
47 Kotlin与Jetpack简直是天生一对!
48 用Kotlin写一个GitHub Trending App
49 结课测试 | “Kotlin编程第一课”100分试卷等你来挑战!
50 结束语 | 不忘初心
Android Framework 教程—基础篇
01 Ubuntu 使用快速入门
02 Make 构建工具入门
03 理解 Unicode UTF-8 UTF-16 UTF-32
04 Linux Shell 脚本编程入门1——核心基础语法
05 SeAndroid 使用极速上手
06 理解 C++ 的 Memory Order
07 AOSP 极速上手
08 系统开发工具推荐
09 添加 Product
运动相关知识
爱上跑步
01 开篇词 | 跑步,不那么简单的事儿
02 跑两步就喘了,是不是我不适合跑步?
03 正确的跑步姿势是什么样的?
04 为什么跑步要先热身?
05 怎样制定你的第一个10公里跑步计划?
06 快跑和慢跑,哪个更燃脂?
07 普通跑步者应该如何选择跑鞋?
08 买跑步装备,不要踩这些坑儿
09 跑步前到底应不应该吃东西?
10 跑步到底伤不伤膝盖?
11 参加了20场马拉松,我是如何准备的?
12 除了马拉松,还能参加哪些跑步赛事?
13 热点问题答疑 :跑完第二天浑身疼,还要不要继续跑?
健身房计划
[DeepSeek]减脂塑形计划
【DeepSeek】训练周期安排
每日餐饮热量控制
减脂期间食物推荐避坑指南
HarmonyOS知识库
其他知识类目
心理学相关
如何学点心理学——关于非专业人士学心理学的一点建议
投射性认同
-
+
首页
36 答疑(一)| Java和Kotlin到底谁好谁坏?
由于咱们课程的设计理念是简单易懂、贴近实际工作,所以我在课程内容的讲述上也会有一些侧重点,进而也会忽略一些细枝末节的知识点。不过,我看到很多同学都在留言区分享了自己的见解,算是对课程内容进行了很好的补充,这里给同学们点个赞,感谢你的仔细思考和认真学习。 另外,我看到不少同学提出的很多问题也都非常有价值,有些问题非常有深度,有些问题非常有实用性,有些问题则非常有代表性,这些问题也值得我们再一起探讨下。因此,这一次,我们来一次集中答疑。 ### Java和Kotlin到底谁好谁坏? 很多同学看完开篇词以后,可能会留下一种印象,就是貌似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(当然也包括我)。 ```kotlin // 代码段1 val i = 1F // Float 类型 val j = 1.0 // Double 类型 val k = 1D // 报错!! ``` 实际上,在Kotlin当中,要代表Double类型的字面量,我们只需要在数字末尾加上小数位即可。“1D”这种写法,在Kotlin当中是不被支持的,我们需要特别注意一下。 ### 逆序区间 在第1讲里,我曾提到过:如果我们想要逆序迭代一个区间,不能使用“6…0”这种写法,因为这种写法的区间要求是:右边的数字大于等于左边的数字。 ```kotlin // 代码段2 fun main() { for (i in 6..0) { println(i) // 无法执行 } } ``` 在我们实际工作中,我们也许不会直接写出类似代码段2这样的逻辑,但是,当我们的区间范围变成变量以后,这个问题就没那么容易被发现了。比如我们可以看看下面这个例子: ```kotlin // 代码段3 fun main() { val start = calculateStart() // 6 val end = calculateEnd() // 0 for (i in start..end) { println(i) } } ``` 在这段代码中,如果end小于start,我们就很难通过读代码发现问题了。所以在实际的开发工作中,我们其实应该慎重使用“start…end”的写法。如果我们不管是正序还是逆序都需要迭代的话,这时候,我们可以考虑封装一个全局的顶层函数: ```kotlin // 代码段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讲中,有不少同学觉得密封类不是特别好理解。在课程里,我们是拿密封类与枚举类进行对比来说明讲解的。我们知道,所谓枚举,就是一组有限数量的值。枚举的使用场景往往是某种事物的某些状态,比如,电视机有开关的状态,人类有女性和男性,等等。在Kotlin当中,同一个枚举,在内存当中是同一份引用。 ```kotlin enum class Human { MAN, WOMAN } fun main() { println(Human.MAN == Human.MAN) println(Human.MAN === Human.MAN) } 输出 true true ``` 那么密封类,其实是对枚举的一种补充。枚举类能做的事情,密封类也能做到: ```kotlin 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,这些子类消息的种类肯定是有限的。 而密封类的好处就在于,对于每一种消息类型,它们都可以携带各自的数据。 ```kotlin // 代码段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() } ``` 所以我们可以说:密封类,就是一组有限数量的子类。针对这里的子类,我们可以让它们创建不同的对象,这一点是枚举类无法做到的。 那么,使用密封类的第一个优势,就是如果我们哪天扩充了密封类的子类数量,所有密封类的使用处都会智能检测到,并且给出报错: ```kotlin // 代码段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可以帮我们快速补充分支类型:  不过,还有一点需要特别注意,那就是else分支。一旦我们在枚举密封类的时候使用了else分支,那我们前面提到的两个密封类的优势就会不复存在! ```kotlin 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讲实现的第一个版本的计算器里,我们使用了valueOf()尝试解析了操作符枚举类。而这只是理想状态下的代码,实际上,正确的方式应该使用2.0版本当中的方式。 ```kotlin 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。 ```kotlin fun main() { // 报错 val wrong = Operation.valueOf("+") // 正确 val right = Operation.valueOf("ADD") } ``` 出现这个问题的原因就在于,Kotlin提供的valueOf()就是用于解析“枚举变量名称”的。 这是一个非常常见的使用误区,不得不说,Kotlin在这个方法的命名上并不是很好,导致开发者十分容易用错。Kotlin提供的valueOf()还不如说是nameOf()。 而如果我们希望可以根据value解析出枚举的状态,我们就需要自己动手。最简单的办法,就是使用伴生对象。在这里,我们只需要将2.0版本当中的逻辑挪进去即可: ```kotlin 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()了: ```kotlin 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的过程中,还遇到过哪些问题?请在留言区提出来,我们一起交流。
嘿手大叔
2024年10月30日 17:40
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码