Kotlin vs Java:你需要知道的一些不同点
目的
对于 Java 开发者来说,我认为最好了解下 Kotlin,它确实提升了开发效率,有非常多的优点。这篇文章将介绍 Kotlin 的现状和一些优秀特性,并和 Java 做对比。希望大家能对这个语言感兴趣,平常有一些小玩意的话,也可以用 Kotlin 尝试一下。
什么是 Kotlin?
Kotlin 是一种开源静态类型编程语言,面向 JVM、Android、JavaScript 和 Native。它由 JetBrains 开发。该项目始于 2010 年,第一个官方 1.0 版本于 2016 年 2 月发布。
它能做什么?
- Android 开发,2017 年谷歌宣布 Kotlin 为开发 Android 的首选语言。
- 服务端开发,与 JVM 100% 兼容,可以使用现有的框架。Spring 也在加快对 Kotlin 的支持,https://start.spring.io/ 上 Language 可选 Kotlin,且部分源代码已使用 Kotlin 改写,博客中还有非常多相关文章。
- Web开发,在针对 JavaScript 时,Kotlin 会转译为 ES5.1 并生成与包括 AMD 和 CommonJS 在内的模块系统兼容的代码。
- 原生开发,Kotlin/Native 是 Kotlin 项目的一部分,它将 Kotlin 编译为无需 VM 即可运行的特定于平台的代码。(目前处于测试阶段)
发展
- JetBrains 于 2011 年推出 Kotlin
- 2016 年发布第一个稳定版本 Kotlin 1.0
- 2017 年 Google 宣布 Kotlin 正式成为Android 的开发首选语言
兼容性
Kotlin 与 Java 100% 兼容,可以使用所有的现有框架,例如 Spring、Vert.x。两种语言也可互操作,可以轻松地从 Java 调用 Kotlin 代码或者从 Kotlin 调用 Java 代码。IDE 中还内置了一个自动化的 Java 到 Kotlin 的转换器,可简化现有代码的迁移。但生产中,还是不太建议这样操作,一是转换的代码可读性可能不符合你的期望,二是如果类特备复杂,则影响范围较大有一定风险。可根据类的复杂度,自行抉择是否转换。
(有一句俗话,Java 最好的第三方库是 Kotlin。
优势
Kotlin 更简洁,根据官方的数据,**代码行数约减少了 40%**。
其次,空安全,程序不再容易 NPE 的影响。
其他高级特性,如智能转换、高阶函数、扩展函数等等,提供了编写富有表现力的代码的能力。
难学吗?
Kotlin 的灵感来自 Java、C#、JavaScript、Scala 和 Groovy 等语言,在下面的特性介绍中可以看到很多其他语言的影子。它的基础语法非常易于学习,可以在几天内轻松上手、阅读和编写 Kotlin。 但如果想要学习更多高级功能可能需要更长的时间,但总体而言,它不是一门复杂的语言。
由于 Kotlin 也基于 JVM,可以反编译字节码,再将其转为 Java,这是学习 Kotlin 一种较好的方式,能快速理解。
特性
空安全
在平常的开发和自测过程中,遇见最多的异常可能就是 NPE 了。Kotlin 吸取了 C# 的做法,从源头上抓起,区分了可空引用和不可空引用。空引用:十亿美元的错误
例如,常规的 String 类型的变量就不能容纳 null,在变量类型后面加上一个问号,才表明这个变量可以为 null。如果尝试将一个可空类型的变量直接赋值给一个不可空类型变量,或者访问一个可空类型变量的属性,在编译编译阶段,都会报错。
在语法层面强制 null,这点从实际工程角度来说是非常有利的。
1 | // Kotlin |
如果一个变量 b 可能是空,那该如何访问它的属性?
判断 null
这和 Java 几乎差不多。先判断是否为 null,不为 null 再操作
1 | // Kotlin |
安全的调用 ?.
或者可以使用安全调用操作符 ?. :
1 | // Kotlin |
如果 b 非空,就返回 b.length, 否则返回 null, b?.length 表达式的类型是 Int?
在这个例子中,安全调用看起来很普通,但在链式调用中能简化非常多代码。例如,在业务中经常会获取某个嵌套很深的属性,在 Java 中,就是要做逐层判断,或者使用 Java8 新增的 Optional 类:
1 | // Java 逐层判断 |
Elvis 操作符 ?:
当我们有一个可空的引用 b 时,我们可以说“如果 b 非空,我使用它;否则使用某个非空的值”:
1 | // Java |
如果左侧的 b?.length 不为空,那就返回,否则就返回右侧的表达式。仅当左侧为空时,才会对右侧进行运算。
由于 throw 和 return 在 Kotlin 中都是表达式,所以非常方便用于检测一些入参或者快速返回:
1 | // Kotlin |
!! 操作符
这是不优雅的操作,这是非空断言运算符,可以将任何值转换非空类型,如果为空,则会抛出异常,所以使用该运算符可能会得到一个 NPE
1 | val l = b!!.length |
协程
和 Java 相比,协程是 Kotlin 一个新颖的概念,不过协程不是 Kotlin 提出的概念。在网上搜索协程,我们会看到:
- Kotlin 官方文档上说「本质上,协程是轻量级的线程」。
- 很多博客提到「不需要从用户态切换到内核态」、「是协作式的」等等。
协程早在 1958 年就被 Melvin Conway 提出并用用于构建汇编程序,协程是一种编程思想,并不局限于特定的语言。
从 Java 开发者的角度去理解 Coroutines 和 Threads 的关系:
- 我们所有的代码都是跑在线程中,而线程是跑在进程中的。
- 协程没有直接和操作系统关联,操作系统并不知道协程的存在。但它不是空中阁楼,它也可以跑在线程中,可以是单线程,也可以是多线程。
- 单线程中的协程总的执行时间并不会比不用协程少。
而 Kotlin 的协程,它本质上只是线程池的包装,封装成一套好用的 API,Java仅仅是没有解决「写得优雅」这个问题。
由于 Android 的 UI 主线程不能阻塞,所以所有 IO 等耗时操作都要放在 work 线程中,并使用异步回调的方式来更新UI,导致会有大量的回调地狱。因此协程在 Android 上有非常明显的优势,能用同步的方式写出异步+回调的非人类代码。
而对于服务端来说,虽然也可以再一些阻塞态问题上使用协程,但目前都是同步编程,协程的效益并没有那么明显,如果使用的是异步编程框架,则有质的飞越。
参数默认值
函数参数可以有默认值,当省略相应的参数时使用默认值。否则就需要通过重载方法来实现参数默认值,如果有4个可选参数,则要写 2^4 = 16 个重载方法,而带有这个语法的语言(js、c++、python)会有明显优势。
1 | // Kotlin |
命名参数
在工程中,如果一个方法的参数过多,将参数依次对应起来是件比较麻烦的事情,尤其是当所有参数类型都一致的时候,更需要小心。如果对错了,编译能通过,但会出现逻辑错误,此类 bug 可能要排查很久。
而 Kotlin 在函数调用中可以使用命名参数,还可以自由更改它们的列出顺序,如果要使用它们的默认值,可以完全忽略此参数。
1 | // Java 入参过多,且类型都相似 |
扩展函数
对于 Java 而言,如果想扩展一个类的新功能需要继承原来的类,或者使用装饰者这样的设计模式。但有的类来自第三方库,无法修改,或者被 final 修饰,这些情况下,Java 将无能为力,只能使用静态方法,将此类的对象传入进去。而 Kotlin 的扩展方法则无任何限制,新增的函数就像原始类本来就有的函数一样,可以用普通的方法调用。(如果查看字节码,会发现其实现就是静态方法,第一个入参为扩展类的对象)
Kotlin 鼓励开发者尽量精简类的定义,一个类只定义框架,工具函数可以通过外部扩展一点点地添加,尽量不改动原有的类,这也是扩展方法的意义,让代码更加简单和整洁。
Java 里的许多工具类,比如 Collections、Arrays、Objects 等等,它们提供了一系列静态方法来充当工具函数,通过参数传入被操作的对象,既不直观又冗长无比,Kotlin 将这些常用的方法都改成了扩展方法,我们可以非常方便的使用。
1 | // 自义定一些扩展方法 |
扩展是静态解析的,扩展并不能真正的修改他们所扩展的类,通过定义一个扩展方法或熟悉,并没有在类中插入一个新成员,仅仅是通过该类的变量用点表达式去调用这个新函数。
如果一个类定义有一个成员函数和一个扩展函数,且两个函数有完全一样的方法签名,那么只会调用成员函数。
扩展属性
与函数类似,Kotlin 支持扩展属性,当一个类的某些属性可以由该类的其他属性推导出来时,可以使用扩展属性。它只是看着像操作属性,实际上还是方法的访问。
1 | open class Student( |
泛型
Kotlin 泛型与 Java 泛型相似,大都只是名字概念上的变化。这里只讲讲 Kotlin 增强的部分。
由于类型擦除, Java 和 Kotlin 的泛型类型实参都会在编译阶段被擦除,无法知道实际类型,这个问题,在 Java 中的解决方案通常是额外传递一个 Class
1 | // Java |
应用场景
inline 和 reified 比较有用的一个场景是用在反序列的时候。由于泛型运行时类型擦除的问题,目前用反序列化泛型类时步骤是比较繁琐的,工具类中都需要传入一个 Class
1 | val gson = Gson() |
还有一些冷门用法,用于实现不同的返回类型函数重载。无论是 Kotlin 还是 Java,函数签名都是由方法名称和参数构成,返回值不参与,因此无法重载返回值。
1 | // 需要实现一个 英尺转厘米 的方法,返回值可以是不精确的 int 类型,也可以是 double |
实际工程中,一个可能的情景是:有一个方法,需要被多个方法调用,但每个调用方需要的返回值可能是不同的 BO 对象,因此可以对该方法的返回值进行重载,进行类型转换。
原理
如果要是真泛型,则必须是内联函数。内联函数会将代码平铺到所有的调用点,有多少个调用点,就会将泛型方法编译多少次。 Kotlin 通过调用点知道了传入的类型,平铺后,泛型方法就可以知道泛型的类型了。
1 | // 源代码 |
高阶函数与 Lambda 表达式
在 Java 里,如果你有一个 a 方法,需要调用另外一个 b 方法,直接调用即可。而如果想在 a 调用时动态设置 b 方法的参数,就要把参数传给 a,再从 a 的内部把参数传给 b:
1 | // Java |
如果想动态设置的不是方法参数,而是方法本身呢?比如我在 a 的内部有一处对别的方法的调用,这个方法可能是 b,可能是 c,不一定是谁,我只知道,我在这里有一个调用,它的参数类型是 int ,返回值类型也是 int ,而具体在 a 执行的时候内部调用哪个方法,我希望可以动态设置:
1 | // Java |
想把方法作为参数传到另一个方法里,这个……可以做到吗?不行,Java 里是不允许把方法作为参数传递的,但有一个历史悠久的变通方案:接口。可以通过接口的方式把方法包装起来:
1 | // Java |
而在 Kotlin 里,函数的参数也可以是函数类型的,这是一种 Java 中不存在的类型,这种类型的对象可以当函数来用,还可以作为函数的参数、返回值,以及赋值给变量。这种「参数或者返回值为函数类型的函数」,在 Kotlin 中就被称为「高阶函数」——Higher-Order Functions。所谓的「高阶」,总给人神秘感,其实概念源自于数学中的高阶函数,没有其他特殊功能,唬人的概念罢了。
如下代码,funParam 就是一个函数类型的参数,它接受一个 int 类型的参数,返回类型是 String:
1 | // Kotlin |
Kotlin 里「函数可以作为参数」这件事的本质,是函数在 Kotlin 里可以作为对象存在——因为只有对象才能被作为参数传递。赋值也是一样道理,只有对象才能被赋值给变量。但函数本身的性质又决定了它没办法被当做一个对象,函数就是函数,没有类型,也不是对象。那怎么办?Kotlin 的选择是,那就创建一个和函数具有相同功能的对象。怎么创建?使用双冒号,这个是底层的逻辑。Kotlin 的 Lambda 本质也是一个函数类型的对象。
Java 的 Lambda
上面我们提到 Java 可以通过接口来传递方法,即:
1 | // Java |
但看看代码,实在是太长了。于是,Java 从 8 开始引入了对 Lambda 的支持,对于单抽象方法的接口——简称 SAM 接口,Single Abstract Method 接口——对于这类接口,Java 8 允许你用 Lambda 表达式来创建匿名类对象,但它本质上还是在创建一个匿名类对象,它没有属性自己的类型,必须要使用一个接口来接收 Lambda。它只是一种简化写法而已,本质上没有功能上的突破。
1 | // Java Lambda |
var、val
在声明一个变量时,我们习惯了敲打两次变量类型,第一次用于声明变量类型,第二次用于构造函数。Kotlin 可以使用 var 让编译器自己去推断类型,Java 10 也支持了这个语法。Kotlin 中还支持 val,表示该变量一旦初始化不可改变,即 final。
1 | var codefx = new URL("http://codefx.org"); |
这样可以少敲几个字,但更重要的是,它避免了信息冗余,而且对齐了变量名,更容易阅读。当然,这也需要付出一点代价:有些变量,比如例子当中的 connection,就无法立即知道它是什么类型的。虽说 IDE 可以辅助显示出这些变量的类型,但还是需要光标放上去或者跳转到方法定义查看返回值类型,并且在其他场景下可能就完全不行了,比如在代码评审的时候。
在 new 对象的时候使用 var 推导类型,而在接某个方法的返回值时,使用完整的类型而不是 var,可能是一个比较好的办法,便于阅读。
字符串模板
Java 中,如果要拼接复杂字符串,一般用 StringBuilder 或者 String.format 方法,但如果参数过多,也非常麻烦,参数还可能对岔了。
Kotlin 支持字符串模板,它是一段代码,会计算并将结果返回到字符串中。这块古老的糖从 shell 开始就有了,但 Java 却迟迟缺席。
1 | // Java |
解构
Kotlin 支持将一个对象解构为多个变量,如:
1 | val (name, age) = person |
这种语法叫做解构声明,解构声明一次创建多个变量,之后可以独立的使用这些变量。这种语法可以用来从一个函数返回多个值。不关心的变量,可以使用下划线 _ 代替。
多返回值
很多场景需要从一个函数中返回两件事,例如,一个状态和一个结果对象。在 Java 中,一般封装成一个对象,或手写 Pair 类,但出参并不好理解,一般都叫 first、second 之类。 但解构时需要正确命名数据,否则也不太好理解。
1 | // Kotlin |
解构声明和映射
1 | // for 遍历时,解构声明 Map 中的 Entry 对象 |
嵌套函数(本地函数)
在一个复杂方法中,常常会有一些相似逻辑,常规思路就是抽成一个私有方法,有时候作用域还是太大。Kotlin 支持在函数中定义作用域更小局部函数,这点类似于 JavaScript。
1 | fun login(user: String, password: String, illegalStr: String) { |
数据类
从此告别繁琐的 Java 数据类,会自动生成 toString、hashcode、equals、getter、setter、copy 等方法,再也不需要 lombok 了。
1 | data class User(val name: String, val age: Int) |
但数据类的坑或者用起来不舒服的地方也较多:
- 它只有全参构造函数,而一些序列化框架依赖于无参构造函数,此时会出现问题。而且 new 一个数据类的时候就要把所有入参一口气设置好。
- Json 字符串可能出现一些字段缺失或者 null,如果数据类的定义字段都不可空,那么序列化报错,怎么办?
- 所有字段都加上 ?,可空,那以后所有这个字段的使用都要加上 ?. 也是非常麻烦的,或者再加一个数据类转换一下。
- 约束好上游,做好定义,哪些不能为空等。
单例对象
这应该是来自于Scala,连关键字都一模一样。
1 | object Document { |
别名
支持为包、类型指定别名。在工程中如果有两个类名一样,包不一样,则容易出错,一些代码必须要写完整的包路径,代码冗长难读。例如 @Service 注解,项目中可能有两个包,一个是org.springframework.stereotype.Service; ,一个是 com.dianping.pigeon.remoting.provider.config.annotation.Service; 每次都需要查看一下 import 的是哪个包,易误导人。
1 | // 包别名,Python、Groovy 都这个语法 |
运算符重载
这个是 C# 和 Scala 的把戏。
1 | data class Point(val x: Int, val y: Int) |
没有受检查异常
Kotlin 并没有受检查异常。因为在大型的项目中,它对代码质量的提升极其有限,但是却大大降低了效率。这个灵感大概来源于 C#,可以参考 Why doesn’t C# have checked exceptions?
集合字面量
1 | // Java 不愿用糖来吸引小朋友,想快速创建一个带有初始值的集合都比较麻烦 |
when
Kotlin 提供了非常强大的 when 操作符,是增强版 switch。
1 | //Kotlin |
其他
参考 RxJava 创造出的 Flow
大量简单的流式集合操作 map(), forEach() 等,可以用于替换 Java 的 Stream,Java 3 行,此只需要 1 行。
最后也发现了一个有趣的网站,大家也可以自己再看下。 https://www.kotlinvsjava.com
文章参考: