Kotlin vs Java:你需要知道的一些不同点

目的

对于 Java 开发者来说,我认为最好了解下 Kotlin,它确实提升了开发效率,有非常多的优点。这篇文章将介绍 Kotlin 的现状和一些优秀特性,并和 Java 做对比。希望大家能对这个语言感兴趣,平常有一些小玩意的话,也可以用 Kotlin 尝试一下。

什么是 Kotlin?

Kotlin 是一种开源静态类型编程语言,面向 JVM、Android、JavaScript 和 Native。它由 JetBrains 开发。该项目始于 2010 年,第一个官方 1.0 版本于 2016 年 2 月发布。

它能做什么?

  1. Android 开发,2017 年谷歌宣布 Kotlin 为开发 Android 的首选语言。
  2. 服务端开发,与 JVM 100% 兼容,可以使用现有的框架。Spring 也在加快对 Kotlin 的支持,https://start.spring.io/ 上 Language 可选 Kotlin,且部分源代码已使用 Kotlin 改写,博客中还有非常多相关文章。
  3. Web开发,在针对 JavaScript 时,Kotlin 会转译为 ES5.1 并生成与包括 AMD 和 CommonJS 在内的模块系统兼容的代码。
  4. 原生开发,Kotlin/Native 是 Kotlin 项目的一部分,它将 Kotlin 编译为无需 VM 即可运行的特定于平台的代码。(目前处于测试阶段)

发展

  1. JetBrains 于 2011 年推出 Kotlin
  2. 2016 年发布第一个稳定版本 Kotlin 1.0
  3. 2017 年 Google 宣布 Kotlin 正式成为Android 的开发首选语言

2021 开发者生态系统现状

兼容性

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
2
3
4
5
6
// Kotlin
var output: String
output = null // Compilation error

val name: String? = null // Nullable type
println(name.length()) // Compilation error

如果一个变量 b 可能是空,那该如何访问它的属性?

判断 null

这和 Java 几乎差不多。先判断是否为 null,不为 null 再操作

1
2
// Kotlin
val l = if (b != null) b.length else -1
安全的调用 ?.

或者可以使用安全调用操作符 ?. :

1
2
3
4
5
// Kotlin
val a = "Kotlin"
val b: String? = null
println(b?.length)
println(a?.length) // 无需安全调用

如果 b 非空,就返回 b.length, 否则返回 null, b?.length 表达式的类型是 Int?

在这个例子中,安全调用看起来很普通,但在链式调用中能简化非常多代码。例如,在业务中经常会获取某个嵌套很深的属性,在 Java 中,就是要做逐层判断,或者使用 Java8 新增的 Optional 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Java 逐层判断
if(a != null) {
if (a.b != null) {
if (a.b.c != null) {
// 在此处访问 d 属性
ans = a.b.c.d;
}
}
}

// Java 使用 Optional,但还是有不适感
Optional.ofNullable(a)
.map(it -> it.b)
.map(it -> it.c)
.map(it -> it.d)
.get()

// Kotlin 安全操作符 一行即可
val d = a?.b?.c?.d
Elvis 操作符 ?:

当我们有一个可空的引用 b 时,我们可以说“如果 b 非空,我使用它;否则使用某个非空的值”:

1
2
3
4
5
6
7
8
9
// Java
if(b != null) {
return b.length;
} else {
return -1;
}

// Kotlin
return b?.length ?: -1

如果左侧的 b?.length 不为空,那就返回,否则就返回右侧的表达式。仅当左侧为空时,才会对右侧进行运算。

由于 throw 和 return 在 Kotlin 中都是表达式,所以非常方便用于检测一些入参或者快速返回:

1
2
3
4
5
6
// Kotlin
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ……
}
!! 操作符

这是不优雅的操作,这是非空断言运算符,可以将任何值转换非空类型,如果为空,则会抛出异常,所以使用该运算符可能会得到一个 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
2
3
4
5
6
7
8
// Kotlin
fun bar(a:Int, b: String = "hello", c: Int = 0) {}

// Java 重载
void bar(int a, String b) { bar(a, b, 0);}
void bar(int a) { bar(a, "hello", 0);}
void bar(int a, int c) { bar(a, "hello", c);}
void bar(int a, String b, int c) {}

命名参数

在工程中,如果一个方法的参数过多,将参数依次对应起来是件比较麻烦的事情,尤其是当所有参数类型都一致的时候,更需要小心。如果对错了,编译能通过,但会出现逻辑错误,此类 bug 可能要排查很久。

而 Kotlin 在函数调用中可以使用命名参数,还可以自由更改它们的列出顺序,如果要使用它们的默认值,可以完全忽略此参数。

1
2
3
4
5
6
7
8
// Java  入参过多,且类型都相似
void bar(int a, int b, int c, int d, int e) {}

// Kotlin
// 方法定义
fun bar(a: Int, b: Int, c: Int, d: Int, e: Int) {}
// 方法调用
bar(c = 3, d = 4, e = 5, a = 1, b = 2)

扩展函数

对于 Java 而言,如果想扩展一个类的新功能需要继承原来的类,或者使用装饰者这样的设计模式。但有的类来自第三方库,无法修改,或者被 final 修饰,这些情况下,Java 将无能为力,只能使用静态方法,将此类的对象传入进去。而 Kotlin 的扩展方法则无任何限制,新增的函数就像原始类本来就有的函数一样,可以用普通的方法调用。(如果查看字节码,会发现其实现就是静态方法,第一个入参为扩展类的对象)

Kotlin 鼓励开发者尽量精简类的定义,一个类只定义框架,工具函数可以通过外部扩展一点点地添加,尽量不改动原有的类,这也是扩展方法的意义,让代码更加简单和整洁。

Java 里的许多工具类,比如 Collections、Arrays、Objects 等等,它们提供了一系列静态方法来充当工具函数,通过参数传入被操作的对象,既不直观又冗长无比,Kotlin 将这些常用的方法都改成了扩展方法,我们可以非常方便的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 自义定一些扩展方法
// 为所有类新增一个 toJson 方法,用于转成 json
fun Any.toJson(): String {
return gson.toJson(this);
}

// 还可以为可空类型做扩展,将一些常用的空判断放在扩展方法中
fun Any?.toString(): String {
if(this == null) {
return "null"
}
return toString();
}

// Kotlin 定义好的一些常用扩展方法
fun String.toInt(): Int = java.lang.Integer.parseInt(this)
fun String.toLong(): Long = java.lang.Long.parseLong(this)
String.toBigDecimal(): java.math.BigDecimal = java.math.BigDecimal(this)
// 还有其他 todouble, tofloat ...

// 集合判空 etc...
public inline fun <T> Collection<T>?.isNullOrEmpty(): Boolean {
return this == null || this.isEmpty()
}

扩展是静态解析的,扩展并不能真正的修改他们所扩展的类,通过定义一个扩展方法或熟悉,并没有在类中插入一个新成员,仅仅是通过该类的变量用点表达式去调用这个新函数。

如果一个类定义有一个成员函数和一个扩展函数,且两个函数有完全一样的方法签名,那么只会调用成员函数。

扩展属性

与函数类似,Kotlin 支持扩展属性,当一个类的某些属性可以由该类的其他属性推导出来时,可以使用扩展属性。它只是看着像操作属性,实际上还是方法的访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
open class Student(
val stuNo: Int,
var name: String,
val englishScore: Int,
val chineseScore: Int,
val mathScore: Int
)

// 判断是否通过了考试
var Student.pass: Boolean
get() = englishScore >= 60 && chineseScore >= 60 && mathScore >= 60
// 这里的 set 方法单纯演示
set(value) {name = value.toString()}

泛型

Kotlin 泛型与 Java 泛型相似,大都只是名字概念上的变化。这里只讲讲 Kotlin 增强的部分。

由于类型擦除, Java 和 Kotlin 的泛型类型实参都会在编译阶段被擦除,无法知道实际类型,这个问题,在 Java 中的解决方案通常是额外传递一个 Class 类型的参数。但 Kotlin 中存在一个额外手段,即内联函数,此时可以说是【真泛型】。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Java
public <T> void test(Integer arg) {
boolean b = arg instanceof T; // 编译错误
T t = new T(); // 编译错误
T[] array = new T[100]; // 编译错误
T[] array2 = (T[]) new Object[100]; // 警告
}


// Kotlio
// inline 内联, reified 具象化的
inline fun <reified T> test(arg: Int) {
val clazz: Class<T> = T::class.java // 获取泛型的实际类型
val b = arg is T // 判断参数是否是 T 类型
val newInstance = clazz.newInstance() // 创建一个 T 对象
}
应用场景

inline 和 reified 比较有用的一个场景是用在反序列的时候。由于泛型运行时类型擦除的问题,目前用反序列化泛型类时步骤是比较繁琐的,工具类中都需要传入一个 Class 参数,利用 inline 和 reified 我们就可以简化掉这个参数。

1
2
3
4
5
6
val gson = Gson()

// 只需一个参数,无需传入类型信息
inline fun <reified T> toBean(json: String): T {
return gson.fromJson(json, T::class.java)
}

还有一些冷门用法,用于实现不同的返回类型函数重载。无论是 Kotlin 还是 Java,函数签名都是由方法名称和参数构成,返回值不参与,因此无法重载返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 需要实现一个 英尺转厘米 的方法,返回值可以是不精确的 int 类型,也可以是 double


// 这种方法无法通过编译
fun inchToCm(inch: Double): Int {
val cm: Double = inch * 2.54
return cm.toInt()
}
fun inchToCm(inch: Double): Double {
val cm: Double = inch * 2.54
return cm
}


// 使用 reified,实现不同的返回类型函数重载
inline fun <reified T> inchToCm(inch: Double): T {
val cm: Double = inch * 2.54
return when(T::class) {
Double::class -> cm as T
Int::class -> cm.toInt() as T
else -> throw IllegalStateException("Type not supported")
}
}

fun main() {
val cm1: Int = inchToCm(12.0)
val cm2: Double = inchToCm(12.0)
}

实际工程中,一个可能的情景是:有一个方法,需要被多个方法调用,但每个调用方需要的返回值可能是不同的 BO 对象,因此可以对该方法的返回值进行重载,进行类型转换。

原理

如果要是真泛型,则必须是内联函数。内联函数会将代码平铺到所有的调用点,有多少个调用点,就会将泛型方法编译多少次。 Kotlin 通过调用点知道了传入的类型,平铺后,泛型方法就可以知道泛型的类型了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 源代码
inline fun <reified T> inlineTest() {
println(T::class.java.name)
}

fun <T> test() {
println("aaa")
}

fun main() {
inlineTest<Int>()
test<Int>()
}

// 将其反编译为 Java 代码后再观察
// main 方法并没有直接调用 inlineTest 方法
public final class KTestKt {
// $FF: synthetic method
public static final void inlineTest() {
int $i$f$inlineTest = 0;
Intrinsics.reifiedOperationMarker(4, "T");
String var1 = Object.class.getName();
System.out.println(var1);
}

public static final void test() {
String var0 = "aaa";
System.out.println(var0);
}

public static final void main() {
int $i$f$inlineTest = false;
String var1 = Integer.class.getName(); // 非调用方法,直接平铺代码并传入调入点的 Integer 类型
System.out.println(var1);
test(); // 非内联方法,调用
}

// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}

高阶函数与 Lambda 表达式

在 Java 里,如果你有一个 a 方法,需要调用另外一个 b 方法,直接调用即可。而如果想在 a 调用时动态设置 b 方法的参数,就要把参数传给 a,再从 a 的内部把参数传给 b:

1
2
3
4
5
6
// Java
int a(int param) {
return b(param);
}
a(1); // 内部调用 b(1)
a(2); // 内部调用 b(2)

如果想动态设置的不是方法参数,而是方法本身呢?比如我在 a 的内部有一处对别的方法的调用,这个方法可能是 b,可能是 c,不一定是谁,我只知道,我在这里有一个调用,它的参数类型是 int ,返回值类型也是 int ,而具体在 a 执行的时候内部调用哪个方法,我希望可以动态设置:

1
2
3
4
5
6
// Java
int a(??? method) {
return method(1);
}
a(method1);
a(method2);

想把方法作为参数传到另一个方法里,这个……可以做到吗?不行,Java 里是不允许把方法作为参数传递的,但有一个历史悠久的变通方案:接口。可以通过接口的方式把方法包装起来:

1
2
3
4
5
6
7
8
9
10
11
12
// Java
public interface Wrapper {
int method(int param);
}

int a(Wrapper wrapper) {
return wrapper.method(1);
}

// 在调用外部方法时,传递接口的对象来作为参数:
a(wrapper1);
a(wrapper2);

而在 Kotlin 里,函数的参数也可以是函数类型的,这是一种 Java 中不存在的类型,这种类型的对象可以当函数来用,还可以作为函数的参数、返回值,以及赋值给变量。这种「参数或者返回值为函数类型的函数」,在 Kotlin 中就被称为「高阶函数」——Higher-Order Functions。所谓的「高阶」,总给人神秘感,其实概念源自于数学中的高阶函数,没有其他特殊功能,唬人的概念罢了。

如下代码,funParam 就是一个函数类型的参数,它接受一个 int 类型的参数,返回类型是 String:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Kotlin 
fun a(funParam: (Int) -> String): String {
// somet code ...
return funParam(1)
}

// 定义一个方法,以 lambda 的形式
val bar: (Int) -> String = { param -> param.toString() }
// 传进去
a(bar)

// 或者直接传进去
a({param -> param.toString()})

// 如果 Lambda 是函数的最后一个参数,你可以把 Lambda 写在括号的外面:
a() { param -> param.toString() }
// 而如果 Lambda 是函数唯一的参数,你还可以直接把括号去了:
a { param -> param.toString() }
// 另外,如果这个 Lambda 是单参数的,它的这个参数也省略掉不写,且有默认名字 it
a { it.toString() }



// 对于一个声明好的函数,不管是你要把它作为参数传递给函数,还是要把它赋值给变量,都得在函数名的左边加上双冒号才行
fun b(param: Int): String {
return param.toString()
}
a(::b)

Kotlin 里「函数可以作为参数」这件事的本质,是函数在 Kotlin 里可以作为对象存在——因为只有对象才能被作为参数传递。赋值也是一样道理,只有对象才能被赋值给变量。但函数本身的性质又决定了它没办法被当做一个对象,函数就是函数,没有类型,也不是对象。那怎么办?Kotlin 的选择是,那就创建一个和函数具有相同功能的对象。怎么创建?使用双冒号,这个是底层的逻辑。Kotlin 的 Lambda 本质也是一个函数类型的对象。

Java 的 Lambda

上面我们提到 Java 可以通过接口来传递方法,即:

1
2
3
4
5
6
7
8
// Java
Wrapper wrapper1 = new Wrapper() {
@Override
public int method(int param) {
return param * 2;
}
};
a(wrapper1);

但看看代码,实在是太长了。于是,Java 从 8 开始引入了对 Lambda 的支持,对于单抽象方法的接口——简称 SAM 接口,Single Abstract Method 接口——对于这类接口,Java 8 允许你用 Lambda 表达式来创建匿名类对象,但它本质上还是在创建一个匿名类对象,它没有属性自己的类型,必须要使用一个接口来接收 Lambda。它只是一种简化写法而已,本质上没有功能上的突破。

1
2
3
// Java Lambda
Wrapper wrapper2 = param -> param * 2;
a(wrapper2);

var、val

在声明一个变量时,我们习惯了敲打两次变量类型,第一次用于声明变量类型,第二次用于构造函数。Kotlin 可以使用 var 让编译器自己去推断类型,Java 10 也支持了这个语法。Kotlin 中还支持 val,表示该变量一旦初始化不可改变,即 final。

1
2
3
4
var codefx = new URL("http://codefx.org");
var connection = codefx.openConnection();
var reader = new BufferedReader(
new InputStreamReader(connection.getInputStream()));

这样可以少敲几个字,但更重要的是,它避免了信息冗余,而且对齐了变量名,更容易阅读。当然,这也需要付出一点代价:有些变量,比如例子当中的 connection,就无法立即知道它是什么类型的。虽说 IDE 可以辅助显示出这些变量的类型,但还是需要光标放上去或者跳转到方法定义查看返回值类型,并且在其他场景下可能就完全不行了,比如在代码评审的时候。

在 new 对象的时候使用 var 推导类型,而在接某个方法的返回值时,使用完整的类型而不是 var,可能是一个比较好的办法,便于阅读。

字符串模板

Java 中,如果要拼接复杂字符串,一般用 StringBuilder 或者 String.format 方法,但如果参数过多,也非常麻烦,参数还可能对岔了。

Kotlin 支持字符串模板,它是一段代码,会计算并将结果返回到字符串中。这块古老的糖从 shell 开始就有了,但 Java 却迟迟缺席。

1
2
3
4
5
// Java
String message = String.format("您好%s,晚上好!您目前余额:%.2f元,积分:%d", "张三", 10.155, 10);

// Kotlin
val message = "您好${user.name},晚上好!您目前余额:${cashAmount + presentAmount}元,积分:$point"

解构

Kotlin 支持将一个对象解构为多个变量,如:

1
val (name, age) = person

这种语法叫做解构声明,解构声明一次创建多个变量,之后可以独立的使用这些变量。这种语法可以用来从一个函数返回多个值。不关心的变量,可以使用下划线 _ 代替。

多返回值

很多场景需要从一个函数中返回两件事,例如,一个状态和一个结果对象。在 Java 中,一般封装成一个对象,或手写 Pair 类,但出参并不好理解,一般都叫 first、second 之类。 但解构时需要正确命名数据,否则也不太好理解。

1
2
3
4
5
6
7
8
// Kotlin
fun getInfo() : Pair<Int, Any> {
// 不能创建任意元组,可以使用内置的数据类型,如 Pair 和 Triple,也可自定义
return Pair(200, Any())
}

// 调用
val (status, data) = getInfo()
解构声明和映射
1
2
3
4
5
6
7
8
9
// for 遍历时,解构声明 Map 中的 Entry 对象
for ((key, value) in map) {
// do something with the key and the value
}

// lambdas 中的解构
map.mapValues { entry -> "${entry.value}!" }
// 未使用的 key 可以用 _ 代替
map.mapValues { (_, value) -> "$value!" }

嵌套函数(本地函数)

在一个复杂方法中,常常会有一些相似逻辑,常规思路就是抽成一个私有方法,有时候作用域还是太大。Kotlin 支持在函数中定义作用域更小局部函数,这点类似于 JavaScript。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
fun login(user: String, password: String, illegalStr: String) {
// 验证 user 是否为空
if (user.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
// 验证 password 是否为空
if (password.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
}


fun login(user: String, password: String, illegalStr: String) {
// 嵌套函数中可以访问在它外部的所有变量或常量,因此不再需要传参 illegalStr
fun validate(value: String) {
if (value.isEmpty()) {
throw IllegalArgumentException(illegalStr)
}
}
validate(user, illegalStr)
validate(password, illegalStr)
}

// 其实还有更简单的方法,使用内置的 require 方法和 lambda 表达式
fun login(user: String, password: String, illegalStr: String) {
require(user.isNotEmpty()) { illegalStr }
require(password.isNotEmpty()) { illegalStr }
}

数据类

从此告别繁琐的 Java 数据类,会自动生成 toString、hashcode、equals、getter、setter、copy 等方法,再也不需要 lombok 了。

1
data class User(val name: String, val age: Int)

但数据类的坑或者用起来不舒服的地方也较多:

  1. 它只有全参构造函数,而一些序列化框架依赖于无参构造函数,此时会出现问题。而且 new 一个数据类的时候就要把所有入参一口气设置好。
  2. Json 字符串可能出现一些字段缺失或者 null,如果数据类的定义字段都不可空,那么序列化报错,怎么办?
    1. 所有字段都加上 ?,可空,那以后所有这个字段的使用都要加上 ?. 也是非常麻烦的,或者再加一个数据类转换一下。
    2. 约束好上游,做好定义,哪些不能为空等。

单例对象

这应该是来自于Scala,连关键字都一模一样。

1
2
object Document {
}

别名

支持为包、类型指定别名。在工程中如果有两个类名一样,包不一样,则容易出错,一些代码必须要写完整的包路径,代码冗长难读。例如 @Service 注解,项目中可能有两个包,一个是org.springframework.stereotype.Service; ,一个是 com.dianping.pigeon.remoting.provider.config.annotation.Service; 每次都需要查看一下 import 的是哪个包,易误导人。

1
2
3
4
5
6
// 包别名,Python、Groovy 都这个语法
import org.springframework.stereotype.Service as SpringService
import com.dianping.pigeon.remoting.provider.config.annotation.Service as PigeonService

// 类型别名,可以一些复杂的类型设置一个别名。 和 Scala 相似,Scala 关键字为 type。
typealias Table = Map<Int, Map<String, Int>>

运算符重载

这个是 C# 和 Scala 的把戏。

1
2
3
data class Point(val x: Int, val y: Int)
operator fun Point.plus(val point: Point) = Point(x + point.x, y + point.y)
Point(1, 2) + Point(3, 4) // Point(x=4, y=6)

没有受检查异常

Kotlin 并没有受检查异常。因为在大型的项目中,它对代码质量的提升极其有限,但是却大大降低了效率。这个灵感大概来源于 C#,可以参考 Why doesn’t C# have checked exceptions?

集合字面量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Java 不愿用糖来吸引小朋友,想快速创建一个带有初始值的集合都比较麻烦
// 1. 最啰嗦的办法,无法使用单个表达式来完成
List<String> colors = new ArrayLis<>();
list.add("red");
list.add("blue");

// 2. 但这样创建出来的 ArrayList 非彼 java.util.ArrayList ,只是个内部类,没有实现 add remove 等方法
List<String> colors = Arrays.asList("red", "blue");

// 3. 使用 Stream
List<String> colors = Stream.of("red", "blue").collect(Collectors.toList());

// 4. 用第三方的轮子 Guava
List<String> colors = ImmutableList.of("red", "blue");

// Kotlin 集合字面量 http://openjdk.java.net/jeps/269
val list = listOf("red", "blue")
val set = setOf("red", "blue")
val map = mapOf("red" to "1", "blue" to "2")

when

Kotlin 提供了非常强大的 when 操作符,是增强版 switch。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//Kotlin
when (x) {
1 -> print("x == 1")
2,3 -> print("x == 2 || x ==3")
parseInt(s) -> print("s encodes x") // 可以使用任意表达式作为分支条件,Java 仅仅支持常量
else -> { // 每个 when 表达式都必须要有这个块
print("x is neither 1 nor 2")
}
}


// 支持任意类型的值(调用 equals 方法进行判断), Java 只支持一些基本类型 char, byte, short, int, Character, Byte, Short, Integer, String, or an enum
// 由于智能转换,在类型判断后,也无需再次转换
fun hasPrefix(x: Any) = when(x) {
is String -> x.startsWith("prefix")
else -> false
}


// 还可以无参,用来取代 if-else if链,一直判断直到匹配到一个分支
when {
x.isOdd() -> print("x is odd")
y.isEven() -> print("y is even")
else -> print("x+y is odd.")
}

其他

  • 参考 RxJava 创造出的 Flow

  • 大量简单的流式集合操作 map(), forEach() 等,可以用于替换 Java 的 Stream,Java 3 行,此只需要 1 行。

最后也发现了一个有趣的网站,大家也可以自己再看下。 https://www.kotlinvsjava.com

文章参考: