开发效率 - Kotlin

开发效率 - Kotlin

五月 04, 2020

日常工作中,我们总是希望提高效率,比如快捷键、自动化等,其实开发语言本身在兼容的情况下,也是可以提高效率的。比如Javalambda表达式
这里介绍的Kotlin,并不是为了介绍Kotlin语言的入门或者使用方式,而是和Java对比,说明Kotlin提高效率的方面,下面会用一些小例子,说明一下日常开发中的小提升。

兼容性

版本

Kotlin会被编译为Java字节码,并且可以指定Java版本,这样一来,对于需要运行指定版本Java代码的环境来说,就非常友好了。

双向

Kotlin的代码中是可以直接调用Java类的方法的,因此对于老项目改造是很友好的。
Java中也可以直接使用Kotlin的类以及方法,虽然使用的时候看起来有点奇怪,但是还是可以直接兼容的,因此对于老项目的升级,是没有问题的。

类型处理

类型推导

Kotlin中,申明变量是这样的:

1
2
val sdf = SimpleDateFormat("")
var sdf2 = SimpleDateFormat("")

可以看到,前面就一个val或者var,看起来像是JavaScript的弱类型啊,其实并不是。
当申明变量并且直接初始化为明确类型的值时,可以自动推导类型,不用指定类型。
换句话说,有下面的情况

1
2
3
4
5
6
7
8
9
// 申明一个Int值
var index = 0
// 申明一个Lang值
var time = 0L

// 这样写就不行啦,因为变量的申明,要么可以推导确定类型,要么指定类型
var temp = null
// 可以改成这样
var temp: String? = null

你可能会说,这个好像也没多大区别吧,但是对于下面这两种场景呢?

1
2
3
4
5
SimpleDateFormat sdf = new SimpleDateFormat("");

MainFragment.LoadCallback loadCallback = new MainFragment.LoadCallback() { ... };

OutputStream out = xxx.getOutputStream();

这时候就很不友好了,每次接受一个对象需要写一遍类型再写名字。如果同样的内容换成kotlin呢?

1
2
3
4
5
val sdf = SimpleDateFormat("");

val loadCallback = object: MainFragment.LoadCallback() { ... };

val out = xxx.getOutputStream();

这样是不是就友好很多?
尽管编译器的.var快捷命令,可以帮助我们申明类型,但是它代码本身就在那里,还是会影响视觉。

valvar的区别请前往中文官网自行查看。
另外,Java高版本也支持了类型推导,但是个人认为没有Kotlin做得好,而且有版本兼容问题。


非空判断

Java中写的最多的,可能就是判空吧。

1
2
3
4
5
6
7
8
9
// 方法调用
if (a != null) {
a.b();
}
// 变量赋值
String b = "";
if (c != null) {
b = c.d();
}

现在好消息是,Kotlin里面可以大幅度的省略这部分代码了!
这分为两部分:类型非空,空判定

类型非空

上一个小节里面,指定变量类型的时候,在类型名称后面加上了一个?,就像这样:

1
var temp: String? = null

这就是类型非空了,当一个变量申明时,如果变量类型后面携带了?,或者类型推导时发现内容可能为null,那么就会在调用时直接报错或者发出警告。提示你对象可能为null,怎么调用这些可能为null的对象,是下一个标题的内容。
相反的,如果申明时没有携带?,那么它就认为这个变量是不能赋值为null的,如果你在后续的赋值中,赋值了null,就会自己报错,连编译都过不了,就算侥幸骗过了编译,也会在运行时赋值为null的时候直接报错。
这样可以一定程度上避免代码单元内的空指针问题,也可以一定程度尽早发现空指针问题,同样,也少了很多的非空判断

空判定

上面说的是类型指定,那么使用呢?如果一个可能为null的对象怎么使用?加if?不用,可以直接这样写:(沿用小节开头的Java例子)

1
2
3
4
5
6
7
8
9
// 方法调用
a?.b();
// 变量赋值
val b = c?.d()?:"";
// 参数判断
fun test(a: A?) {
a?:return
...
}

上面列举了三种常见的场景。
对于对象的方法调用,直接加上一个?即可,等价于上面的if中调用。
对于参数的赋值,可以在后面添加默认值,使用的是?:
对于方法内的参数判断,可以直接使用像是赋值一样的写法,然后执行return操作。
代码是不是就简单了很多?

Getter & Setter

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
29
30
31
32
33
34
35
public class Bean {
private String a;
private String b;
private int c;
public Bean(String a, String b, String c) {
this.a = a;
this.b = b;
this.c = c;
}

public Bean() {
this("", "", 0);
}

public String getA() {
return a;
}

public String getB() {
return b;
}

public int getC() {
return c;
}

public void setA(String a) {
this.a = a;
}

public void setC(int c) {
this.c = c;
}
// 省略toString及equals方法的重写
}

可以看到,尽管只有三个成员变量,但是一个类还是非常的长,尽管有编译器的一键生成,但是代码仍然很多,不是很友好,而且全都是非常机械非常模板化的代码。那么在Kotlin中会是什么效果,简化会达到什么程度呢?

1
data class Bean(var a: String = "", val b: String = "", var c: Int = 0)

是不是感觉有点离谱?这里可以告诉你的是:确实已经写完了一个Bean。那么它符合Java规范吗?它是符合的,只是简化了这个过程,class的前面加上了data,表示这是一个数据类,Kotlin会自动为我们根据成员变量生成toStringequals等方法,我们也可以选择重写它,如果不需要特别重写,那么我们连类的大括号都不用写。
上面的Java代码中,b是不能被赋值的,这里只要把var改成val就好了。而gettersetter呢?
Kotlin中的每个变量都默认有个gettersetter的实现,也就是都帮我们写好了默认实现。
而成员变量的申明,因为变量会在构造器赋值,所以只要在构造器的参数中加上变量前缀,就自动变成了成员变量,当然,还可以前缀一个private,让这个变量只能内部可见。
那么又有另一个问题了,三个参数都有默认值,我只想设定其中两个怎么办?Java上的做法就只有提供足够多的重载了,但是Kotlin里面可以这样写:

1
2
3
fun test() {
val bean = Bean(a = "hello", c = 2)
}

是的,还可以直接使用变量名指定传值,这样连重载也不用了。
但是这里要说明一点,如果是需要反射调用的方法或者构造器,是需要单独申明的,否则是找不到的,毕竟参数数量本身没有变。

另一个方面,上面说了,Kotlin帮我们写好了gettersetter,那么我们能重写吗?请看下面的例子:

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
class A {

private val colorArray = intArrayOf(Color.RED, Color.GREEN, Color.BLUE)

var index = 0
private set

var color: Int
get() {
return colorArray[index]
}
set(value) {
colorArray[index] = value
}

fun next() {
index++
index %= colorArray.size
}

}

fun test(a: A) {
// 就像直接普通变量一样使用,完全感知不到getter方法
val index = a.index
// 报错,提示找不到设置的方法
// a.index = 1

// 就像常规变量一样获取
val color = a.color
// 直接对变量赋值,其实是走到了setter方法
a.color = Color.PINK
}

可以看到,我们的A中维护了一个颜色数组,提供了一个方法来移动下标。
外部可以拿到下标,也可以通过一个叫color的变量来获取和设置当前下标对应的颜色。但是我们其实没有color这个成员变量,我们只是通过完全重写gettersetter来模拟了一个变量。

扩展方法

都说Kotlin是语法糖,但是这糖也是真的甜。比如这里介绍的扩展方法
它其实不是真的为对象类型增加了方法,只是从语法上做到了效果而已,但是使用的时候却可以大大提升效率。
举个Java的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 将一个int转换为字符串的方法
private String formatInt(int i) {
if (i < 10) {
return "0" + i;
}
return "" + i;
}

private void funA() {
String str = formatInt(
object1.funB(
object2.funC())
.funD());
}

这是一个很简单的例子,但是看起来就不是很友好了,业务场景可能还会有更麻烦的结构。你可能会说多申明几个变量,然后分开写,但是那样又会显得很臃肿,写起来没有这种链条一样调用的爽快感觉。
所以可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 将一个int转换为字符串的方法
private fun Int.format(): String {
if (i < 10) {
return "0$i"
}
return "$i"
}

private fun funA() {
val str = object1.funB(object2.funC())
.funD()
.format()
}

这个扩展方法就像它原本就有的方法一样,可以直接调用,完美的混入了链条中。
而使用是,方法的增加和去掉,也不用像以前一样,考虑左边括号到哪里,右边括号是哪一个了。

函数式编程

Java中是先有Class再有方法,也就是必须要有对象。而在Kotlin中,也是面向对象,但是它可以有顶级函数,也就是不依托于Class的函数,当然,这也是语法糖而已。但是使用效果非常不错,我们先举例子再说明:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class TaskUtil {
private static final Executor threadPool = Executors.newCachedThreadPool();
private static final Handler mainHandler = new Handler(Looper.getMainLooper());

public static void doAsync(ErrorCallback err, RunCallback run) {
threadPool.execute(new Task(err, run));
}

public static void onUI(ErrorCallback err, RunCallback run) {
mainHandler.post(new Task(err, run));
}

private static class Task extends Runnable {

private ErrorCallback errorCallback;
private RunCallback runCallback;

public Task(ErrorCallback e, RunCallback r) {
errorCallback = e;
runCallback = r;
}

@Override
public void run() {
try {
runCallback.run();
} catch (e: Throwable) {
if (errorCallback != null) {
errorCallback.onError(e);
}
}
}
}

interface ErrorCallback {
void onError(Throwable e);
}

interface RunCallback {
void run();
}

}

class Test {
public void test() {

final String str = "";

TaskUtil.doAsync(null, new TaskUtil.RunCallback() {
@Override
public void run() {
// 子线程
...
Test.this.testB(str);
TaskUtil.onUI(null, new TaskUtil.RunCallback() {
@Override
public void run() {
...
// UI线程
Test.this.testB(str);
}
});
}
});
}

public void testB(String str) {}

}

上面是一个简单的线程同步的工具类以及调用示例,可以看到,尽管简化了一部分代码,但是使用时的调用仍然非常繁琐,而且会存在上下文问题(回调函数中的this到底是哪个this?。那么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
43
44
45
46
47
48
val threadPool: Executor by lazy {
Executors.newCachedThreadPool()
}

val mainHandler: Handler by lazy {
Handler(Looper.getMainLooper())
}

inline fun <reified T: Any> T.doAsync(
noinline err: ((Throwable) -> Unit)? = null,
noinline run: T.() -> Unit) {
threadPool.execute {
try {
run.invoke(this)
} catch (e: Throwable) {
err?.invoke(e)
}
}
}

inline fun <reified T: Any> T.onUI(
noinline err: ((Throwable) -> Unit)? = null,
noinline run: T.() -> Unit) {
mainHandler.post {
try {
run.invoke(this)
} catch (e: Throwable) {
err?.invoke(e)
}
}
}

class Test {
fun test() {
val str = ""
doAsync {
// 子线程中
testB(str)
onUI {
// UI线程中
testB(str)
}
}
}

fun testB(str: String) {}

}

可以看到,对比效果非常明显。调用简单而且结构清晰
那么,现在来说解释一下,上面用到了哪些东西。

顶级函数

顶级函数,也就是前面方法申明的时候,外面没有套一层Class,减少了类的约束,成为了全局的方法。Kotlin中的Math类也是这样的,相比Java中的Math,调用时可以直接写作min(a, b),不用加上类名。

内联 inline

它的作用类似于我们日常中的封装,但是又和我们的封装有些不一样。我们遇到多次出现但是内容一样的代码时,一般都是抽取并且包装成一个方法,如果是多个类共用,那么可能还要再抽取一个类出来,尽管方法实现还是那样的,但是毕竟还是抽了个方法出来,可能还跨了类调用,方法寻址还是需要时间的。
而内联的作用与它类似,但是它是反过来的,当编译时,会把内联方法的代码整个复制到调用的地方,然后顺便做一下结构优化,这样就满足了我们的本来目的,一次写多份代码。

reified

这个关键一般是配合inline使用的,从含义上来讲,是使它更真实,我的理解就是推导泛型。
上面讲过了,编译时内联代码是以复制的形式贴到调用的地方的,而这个时候,如果方法存在泛型,那么基本上都是可以直接明确类型的,而这个关键字就是做这个的。
它可以避免泛型和类型强转引发的安全隐患,也可以实现另类的重载。

函数引用

Kotlin一切函数皆可lambda,可以直接拿到函数的引用,然后在必要时调用,就像上面的run.invoke()
而在使用是,由于表达式的特性,直接使用大括号就好了,也省了很多事情,不用反复申明接口,new对象。
甚至,可以通过::来获取对象中的函数引用,只要函数,不要对象,使用和传递过程也少了很多限制。

懒加载

就是上面申明线程池的那个关键字by lazy,它要求变量必须是val,其实就是帮我们做了在get的时候调用后面的表达式实例化对象,然后后面一直使用这个对象,就像帮我们做了个小单例。

糖衣内的Java

上面的糖衣看起来是真的很甜啊,可以省略很多代码,大大的提高开发的效率,那么糖衣里面是什么呢?
有个方法,在IntelliJ IDEA中,打开一个kt结尾的Kotlin代码文件,然后依次打开菜单中的Tool - Kotlin - Show Kotlin Bytecode
这时,右侧会打开一个窗口,显示这个文件的字节码,再次点击窗口的Decompile按钮,那么就会出现一个将字节码反编译为Java的窗口了。
我们以此来了解一下语法糖帮我们做了什么?

顶级函数

首先,我们先写一个文件,名字就叫做Test.kt,里面放一个顶级函数,就像这样

1
2
3
4
5
package lollipop.test

fun sayHello() {
print("Hello")
}

然后我们按照上面的方法,反编译一下。然后就得到了这样的一个Java类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package lollipop.test;

import kotlin.Metadata;

@Metadata(
mv = {1, 1, 16},
bv = {1, 0, 3},
k = 2,
d1 = {"\u0000\b\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001¨\u0006\u0002"},
d2 = {"sayHello", "", "HelloWorld"}
)
public final class TestKt {
public static final void sayHello() {
String var0 = "Hello";
boolean var1 = false;
System.out.print(var0);
}
}

然后我们找一下不同,看看它帮我们做了什么。

  1. 它帮我们声明了一个类,类名就是文件名然后加上Kt后缀,我们都知道,Java要求每一个Java文件都需要有一个与文件名同名的且为public的类,显然它帮我们做了这件事。
  2. 类名和方法自动加上了final,这也是Kotlin在不加open时不能被继承和重写的原因。
  3. sayHello方法本来是写在外面的,现在它帮我们放在了类中,同时加上了static,说明它是一个可以直接使用的方法。

那么顶级函数就是一个静态方法吗?我们再去使用的地方验证一下。
我们创建两个文件,一个是Test.java,另一个是Test2.kt
然后我们先看看在Java中的调用.

1
2
3
4
5
6
7
8
9
10
11
12
13
package lollipop.test;

class Test {

private Test(String value) {}

public static void with(int value) {

TestKt.sayHello();

}

}

经过一番尝试,发现需要按照我们刚刚反编译的方式调用,
那么Kotlin里面的调用呢?

1
2
3
4
5
6
7
8
9
10
11
package lollipop.test

class Test2 {

fun world() {

sayHello()

}

}

这里很简单,就像本来就有这个方法一样,直接使用,看起来很舒服,那么它实际上是什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package lollipop.test;

import kotlin.Metadata;

@Metadata(
mv = {1, 1, 16},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005"},
d2 = {"Llollipop/test/Test2;", "", "()V", "world", "", "HelloWorld"}
)
public final class Test2 {
public final void world() {
TestKt.sayHello();
}
}

实际上和我们手写的Java类是一样的调用。

伴生对象

我们都知道,在写kotlin时,要在一个类中写一些静态方法时,一般都是写在companion object中,中文翻译就是伴随对象。那么它和我们的静态方法有什么关系和区别呢?
首先,我们就用上面的Test2.kt加一点代码看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package lollipop.test

class Test2 {

companion object {
const val NAME = "Test2"
val pre = "I am"
var id = 0
private var id2 = 1
private var id3 = 2
fun name(n: String): String {
return "Hey $n, $pre $NAME, id is $id-$id2"
}
}

fun world() {
id = 1
sayHello()
}
}

现在是这样,为了做对比,我加了三个变量一个方法,分别用不同的关键字。
现在我们来看看它的反编译结果。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package lollipop.test;

import kotlin.Metadata;
import kotlin.jvm.internal.DefaultConstructorMarker;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

@Metadata(
mv = {1, 1, 16},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\b\u0002\u0018\u0000 \u00052\u00020\u0001:\u0001\u0005B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0006"},
d2 = {"Llollipop/test/Test2;", "", "()V", "world", "", "Companion", "HelloWorld"}
)
public final class Test2 {
@NotNull
public static final String NAME = "Test2";
@NotNull
private static final String pre = "I am";
private static int id;
private static int id2 = 1;
private static int id3 = 2;
public static final Test2.Companion Companion = new Test2.Companion((DefaultConstructorMarker)null);

public final void world() {
id = 1;
TestKt.sayHello();
}

// $FF: synthetic method
public static final void access$setId2$cp(int var0) {
id2 = var0;
}

@Metadata(
mv = {1, 1, 16},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\b\n\u0002\b\f\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u000e\u0010\u0010\u001a\u00020\u00042\u0006\u0010\u0011\u001a\u00020\u0004R\u000e\u0010\u0003\u001a\u00020\u0004X\u0086T¢\u0006\u0002\n\u0000R\u001a\u0010\u0005\u001a\u00020\u0006X\u0086\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\u0007\u0010\b\"\u0004\b\t\u0010\nR\u000e\u0010\u000b\u001a\u00020\u0006X\u0082\u000e¢\u0006\u0002\n\u0000R\u000e\u0010\f\u001a\u00020\u0006X\u0082\u000e¢\u0006\u0002\n\u0000R\u0014\u0010\r\u001a\u00020\u0004X\u0086D¢\u0006\b\n\u0000\u001a\u0004\b\u000e\u0010\u000f¨\u0006\u0012"},
d2 = {"Llollipop/test/Test2$Companion;", "", "()V", "NAME", "", "id", "", "getId", "()I", "setId", "(I)V", "id2", "id3", "pre", "getPre", "()Ljava/lang/String;", "name", "n", "HelloWorld"}
)
public static final class Companion {
@NotNull
public final String getPre() {
return Test2.pre;
}

public final int getId() {
return Test2.id;
}

public final void setId(int var1) {
Test2.id = var1;
}

@NotNull
public final String name(@NotNull String n) {
Intrinsics.checkParameterIsNotNull(n, "n");
return "Hey " + n + ", " + ((Test2.Companion)this).getPre() + " Test2, id is " + ((Test2.Companion)this).getId() + '-' + Test2.id2;
}

private Companion() {
}

// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}

我们一个个的看。

  1. 生成了一个叫做Companion的内部类,它是静态的,并且提供了一系列方法。它应该就是我们的伴生对象了。
  2. Companion类在主类中有一个实例,它是同时被static final修饰的,Kotlin中没有静态修饰符,因此它一定程度上就代表了Kotlin的静态方法区和静态变量区。
  3. const修饰的NAME变成了一个常量,前面是public,是一个标准的常量。
  4. 而只是少了个constpre却是private,但是在伴生对象中提供了一个getPre方法,说明const关键字是用来声明真·常量
  5. id初始化为0,但是在反编译代码中没有看到初始化为0的代码。
  6. private修饰的其他几个变量,没有提供getter方法。
  7. 所有声明在伴生对象中的变量对外接口几乎都是通过伴生对象,但是实际都是在主类中保存为静态的。
  8. 伴生对象中的方法,最后会在生成的伴生类中,并且是静态方法。
  9. 字符串最后生成结果仍然是用+拼接的,而变量的使用,对于有提供getter方法的,会使用getter方法。

函数引用

函数式编程是Kotlin的一大特色,这次我们看看它怎么包装的。
这次因为涉及函数的互相调用,所以改了改上面的三个类,改成了这样:
Test.java

1
2
3
4
5
6
7
8
9
package lollipop.test;

class Test {

public static void say() {
TestKt.sayHello();
}

}

Test.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
package lollipop.test

fun sayHello() {
print("Hello")
}

fun add(num: () -> Int): Int {
return num() + num.invoke()
}

fun textSay(run: () -> Unit) {
run.invoke()
}

Test2.kt

1
2
3
4
5
6
7
8
9
10
11
12
package lollipop.test

import java.util.*

class Test2 {

fun world() {
val random = Random()
val value = add { random.nextInt() }
textSay(Test::say)
}
}

Java类没什么好看的,我们直接看后面的,Test.kt

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
package lollipop.test;

import kotlin.Metadata;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

@Metadata(
mv = {1, 1, 16},
bv = {1, 0, 3},
k = 2,
d1 = {"\u0000\u0016\n\u0000\n\u0002\u0010\b\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0003\u001a\u0014\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00010\u0003\u001a\u0006\u0010\u0004\u001a\u00020\u0005\u001a\u0014\u0010\u0006\u001a\u00020\u00052\f\u0010\u0007\u001a\b\u0012\u0004\u0012\u00020\u00050\u0003¨\u0006\b"},
d2 = {"add", "", "num", "Lkotlin/Function0;", "sayHello", "", "textSay", "run", "HelloWorld"}
)
public final class TestKt {
public static final void sayHello() {
String var0 = "Hello";
boolean var1 = false;
System.out.print(var0);
}

public static final int add(@NotNull Function0 num) {
Intrinsics.checkParameterIsNotNull(num, "num");
return ((Number)num.invoke()).intValue() + ((Number)num.invoke()).intValue();
}

public static final void textSay(@NotNull Function0 run) {
Intrinsics.checkParameterIsNotNull(run, "run");
run.invoke();
}
}

可以看到,我们声明的函数,现在变成了一个叫做Function0的类,其他的都没变,方法内使用的是Function0invoke方法,让我们看看这个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

package kotlin.jvm.functions

/** A function that takes 0 arguments. */
public interface Function0<out R> : Function<R> {
/** Invokes the function. */
public operator fun invoke(): R
}
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}
/** A function that takes 2 arguments. */
public interface Function2<in P1, in P2, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2): R
}
// ...省略中间的多个接口
/** A function that takes 22 arguments. */
public interface Function22<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, in P22, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21, p22: P22): R
}

可以看到,这是跳转到了Koltin标准库中的一个文件,它声明了非常多的方法,最多是到了22个参数,每个都有自己的范型,而刚刚的Function0就是无参数的接口。所以其实就是它帮我们写好了很多通用接口,然后回调触发?
那么问题来了,如果我超过22个呢?

1
2
3
4
5
6
7
8
9
10
fun args(num: (Int, String, Float,
Int, String, Float,
Int, String, Float,
Int, String, Float,
Int, String, Float,
Int, String, Float,
Int, String, Float,
Int, String, Float) -> Int): Int {
return 0
}

于是我加了上面的这个方法,24个参数,看看它做了啥。

1
2
3
4
public static final int args(@NotNull FunctionN num) {
Intrinsics.checkParameterIsNotNull(num, "num");
return 0;
}

结果是这样的,(///▽///),所以它是知道有我们这种流氓,所以干脆弄个N的出来呗?
那么这个FunctionN里面是啥呢?

1
2
3
4
interface FunctionN<out R> : Function<R>, FunctionBase<R> {
operator fun invoke(vararg args: Any?): R
override val arity: Int
}

emmm,构建者给出长度,然后参数全放数组里面,一了百了了。

行,那么我们再看看Test2.kt,看看调用的地方怎么样的。

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
package lollipop.test;

import java.util.Random;
import kotlin.Metadata;
import kotlin.jvm.functions.Function0;

@Metadata(
mv = {1, 1, 16},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005"},
d2 = {"Llollipop/test/Test2;", "", "()V", "world", "", "HelloWorld"}
)
public final class Test2 {
public final void world() {
final Random random = new Random();
int value = TestKt.add((Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
return this.invoke();
}

public final int invoke() {
return random.nextInt();
}
}));
TestKt.textSay((Function0)null.INSTANCE);
}
}

首先是add方法,不出所料的是创建了一个匿名内部类,但是里面的写法很有意思,它先是实现一个final的方法,返回值是int的,来包裹我们的业务代码,然后另一个再去调用它。所以推测返回Object的方法才是接口的真正方法,这样做的原因推测是跟范型擦除有点关系吧。(瞎猜的)

而另一个方法就很奇怪了,不明白使用Java的函数引用,怎么就反编译成了这样。为了验证,我又改成了Kotlin的方法传了进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package lollipop.test

import java.util.*

class Test2 {

fun world() {
val random = Random()
val value = add { random.nextInt() }
textSay(this::test)
}

fun test() {
sayHello()
}

}
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package lollipop.test;

import java.util.Random;
import kotlin.Metadata;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.internal.Reflection;
import kotlin.reflect.KDeclarationContainer;

@Metadata(
mv = {1, 1, 16},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004J\u0006\u0010\u0005\u001a\u00020\u0004¨\u0006\u0006"},
d2 = {"Llollipop/test/Test2;", "", "()V", "test", "", "world", "HelloWorld"}
)
public final class Test2 {
public final void world() {
final Random random = new Random();
int value = TestKt.add((Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
return this.invoke();
}

public final int invoke() {
return random.nextInt();
}
}));
TestKt.textSay((Function0)(new Function0((Test2)this) {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}

public final void invoke() {
((Test2)this.receiver).test();
}

public final KDeclarationContainer getOwner() {
return Reflection.getOrCreateKotlinClass(Test2.class);
}

public final String getName() {
return "test";
}

public final String getSignature() {
return "test()V";
}
}));
}

public final void test() {
TestKt.sayHello();
}
}

这个结果看起来更加奇怪了,看样子它是做了反射,所以上面我们直接拿Java的方法,应该也是反射了,中间也许出了点问题,不过感觉问题不大。(也许)

扩展函数

终于说到了扩展函数,Kotlin的标准库中包含了大量的扩展函数,比如最常用的applylet
让我们看看他们是怎么样的吧。
首先是他们的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

这个没啥说的,只是返回值有点区别而已,我们看书看重点,看看扩展函数会变成什么吧。
代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package lollipop.test

class Test2 {

fun test() {
"hello".let {
print("" + it.length)
}
"world".apply {
print("" + this.length)
}
}

}

反编译后是这样的

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
package lollipop.test;

import kotlin.Metadata;

@Metadata(
mv = {1, 1, 16},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005"},
d2 = {"Llollipop/test/Test2;", "", "()V", "test", "", "HelloWorld"}
)
public final class Test2 {
public final void test() {
String var1 = "hello";
boolean var2 = false;
boolean var3 = false;
int var5 = false;
String var6 = "" + var1.length();
boolean var7 = false;
System.out.print(var6);
var1 = "world";
var2 = false;
var3 = false;
var5 = false;
var6 = "" + var1.length();
var7 = false;
System.out.print(var6);
}
}

尽管代码被inline优化过了,但是还是可以看出,它就是把被扩展的对象当作参数使用了。
这个可能看起来不明显,我们自己写个试试。
我们稍微改改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package lollipop.test

class Test2 {

fun test() {
val result = "hello".link("world")
}

private fun String.link(value: String): String {
return this + value
}

}

可以看到,我们自己声明了一个扩展函数,而且没有inline。那么反编译结果呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package lollipop.test;

import kotlin.Metadata;
import org.jetbrains.annotations.NotNull;

@Metadata(
mv = {1, 1, 16},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004J\u0014\u0010\u0005\u001a\u00020\u0006*\u00020\u00062\u0006\u0010\u0007\u001a\u00020\u0006H\u0002¨\u0006\b"},
d2 = {"Llollipop/test/Test2;", "", "()V", "test", "", "link", "", "value", "HelloWorld"}
)
public final class Test2 {
public final void test() {
String result = this.link("hello", "world");
}

private final String link(@NotNull String $this$link, String value) {
return $this$link + value;
}
}

emmm,这次是真的非常明显了,它就是把被扩展的对象作为第一个参数,传了进去,这样一看,瞬间觉得不神奇了,索然无味啊。
PS:不过用起来是真的香。

懒加载

接着我们来看看懒加载,懒加载可以一定程度上减少了软件初始化时的爆发式内存消耗,不过它真的是这样吗?它又是怎么实现的呢?我们试试看。
我们再改改代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package lollipop.test

import java.util.*

class Test2 {

private val myRandom: Random by lazy {
Random()
}

fun test() {
myRandom.nextInt()
}

}

可以看到,我们懒加载了一个随机数对象,它会在我们使用的时候再初始化。真的是这样吗?

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
package lollipop.test;

import java.util.Random;
import kotlin.Lazy;
import kotlin.LazyKt;
import kotlin.Metadata;
import kotlin.jvm.functions.Function0;

@Metadata(
mv = {1, 1, 16},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0005\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\t\u001a\u00020\nR\u001b\u0010\u0003\u001a\u00020\u00048BX\u0082\u0084\u0002¢\u0006\f\n\u0004\b\u0007\u0010\b\u001a\u0004\b\u0005\u0010\u0006¨\u0006\u000b"},
d2 = {"Llollipop/test/Test2;", "", "()V", "myRandom", "Ljava/util/Random;", "getMyRandom", "()Ljava/util/Random;", "myRandom$delegate", "Lkotlin/Lazy;", "test", "", "HelloWorld"}
)
public final class Test2 {
private final Lazy myRandom$delegate;

private final Random getMyRandom() {
Lazy var1 = this.myRandom$delegate;
Object var3 = null;
boolean var4 = false;
return (Random)var1.getValue();
}

public final void test() {
this.getMyRandom().nextInt();
}

public Test2() {
this.myRandom$delegate = LazyKt.lazy((Function0)null.INSTANCE);
}
}

可以看到,最后的类型,声明的是一个叫做Lazy的类型,而获取的时候,是从它那里拿的,而它的初始化,是在构造器中。我们好像猜中了什么。有点似曾相识的感觉啊。
首先是Lazy类。

1
2
3
4
5
6
public interface Lazy<out T> {

public val value: T

public fun isInitialized(): Boolean
}

emmm,从方法名就有点感觉了,似乎就是那么回事啊。

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
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE

private val lock = lock ?: this

override val value: T
get() {
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}

return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}

override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

private fun writeReplace(): Any = InitializedLazyImpl(value)
}

没跑了,就是这个了,这不就是我们写单例模式的代码吗?只不过这里用的是懒汉模式

总结

这里简单的看了下常用的Kotlin的基础使用和基础的Java实现,看起来确实是语法糖,只要合理使用设计模式,一样可以做到这样的效果,可是它有编译器支持,比我们土制的好看啊。
所以这里,我还是建议,可以尝试把Kotlin用起来,可以减少一大部分的低级Bug