一些Java场景在Kotlin下的解决之道

Kotlin的设计主管宣称“Kotlin修复了Joshua Bloch的Java Puzzlers丛书中提及的半数问题”,《Effective Java》应该也算丛书的一本吧。本人在使用Kotlin时已经强烈的感受到了这一点。此篇主要记录本人在使用Kotlin重构Java代码时中遇到的一些问题。

命令行编译与运行

假设现在有一个写好的kotlin文件Ak.kt,首先对其进行编译:

1
$ kotlinc Ak.kt -include-runtime -d Ak.jar

-include-runtime用来打包kotlin运行时库。

接下来运行程序:

1
$ java -jar Ak.jar

伴生对象硬核原理

虽然在Kotlin的世界里,人们一直强调没有static没有static,但本人还是想通过查看字节码文件这种硬核的方式理解一些概念。编写一个Kotlin例子,package为ObjectDemo:

1
2
3
4
5
6
7
8
9
10
11
12
class AA {
var i = 19
fun methodA() {}
companion object BB{
var j = 29
fun methodB() {}
}
}
fun main(args: Array<String>) {}

这个例子在AA中放一个i属性,一个methodA方法;伴生对象内放一个j属性以及methodB方法。

编译之后会生成三个文件AA$BB.classAA.classObjectDemoKt.class

虽说main在源文件里是一个所谓的顶层函数,但其实编译后会将其放入一个ObjectDemoKt类中,默认类名就是包名+Kt。

接下来本人将主要分析AA$BB.class、AA.class这两个文件。

首先是AA.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
public final class ObjectDemo.AA {
public static final ObjectDemo.AA$BB BB;
public final int getI();
Code:
0: aload_0
1: getfield #10 // Field i:I
4: ireturn
public final void setI(int);
Code:
0: aload_0
1: iload_1
2: putfield #10 // Field i:I
5: return
public final void methodA();
Code:
0: return
public ObjectDemo.AA();
Code:
0: aload_0
1: invokespecial #20 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 19
7: putfield #10 // Field i:I
10: return
static {};
Code:
0: new #43 // class ObjectDemo/AA$BB
3: dup
4: aconst_null
5: invokespecial #46 // Method ObjectDemo/AA$BB."<init>":(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
8: putstatic #48 // Field BB:LObjectDemo/AA$BB;
11: bipush 29
13: putstatic #27 // Field j:I
16: return
public static final int access$getJ$cp();
Code:
0: getstatic #27 // Field j:I
3: ireturn
public static final void access$setJ$cp(int);
Code:
0: iload_0
1: putstatic #27 // Field j:I
4: return
}

通过这个反编译结果,可以迅速得到一些结论:

  1. 写在伴生对象内j属性会被编译成AA的静态字段j;
  2. 写在AA中的var属性i会被编译为普通字段i,并配有final的普通get、set方法;
  3. j还会在AA中被编译出隐含的static final的get、set方法 ;
  4. methodA()在AA中是final的普通方法;
  5. AA内还会存在一个static、final的BB对象的静态初始化器;

再来看看AA$BB.class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final class ObjectDemo.AA$BB {
public final int getJ();
Code:
0: invokestatic #11 // Method ObjectDemo/AA.access$getJ$cp:()I
3: ireturn
public final void setJ(int);
Code:
0: iload_1
1: invokestatic #18 // Method ObjectDemo/AA.access$setJ$cp:(I)V
4: return
public final void methodB();
Code:
0: return
public ObjectDemo.AA$BB(kotlin.jvm.internal.DefaultConstructorMarker);
Code:
0: aload_0
1: invokespecial #27 // Method "<init>":()V
4: return
}

通过这个反编译结果,又可以得出些结论:

  1. BB按照Java中来理解,相当于是AA的内部类;
  2. 在BB内会编译出j的get、set方法,它们是普通的、final的,包装了AA中隐含的static final的get、set方法;
  3. methodB()也是final、非静态的;

由此很多概念就不陌生了。

Java静态块改写

Kotlin中没有静态的概念,可以使用伴生对象来实现。例如:

1
2
3
4
5
6
7
8
9
10
11
12
class StaticDemo {
companion object {
var i: Int = 3
var j: Int = 7
init {
j = 4
}
}
}
fun main(args: Array<String>) {
}

对其使用javap反编译,摘取其中static{}部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static {};
Code:
0: new #42 // class kotlindemo01/StaticDemo$Companion
3: dup
4: aconst_null
5: invokespecial #45 // Method kotlindemo01/StaticDemo$Companion."<init>":(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
8: putstatic #47 // Field Companion:Lkotlindemo01/StaticDemo$Companion;
11: iconst_3
12: putstatic #20 // Field i:I
15: bipush 7
17: putstatic #26 // Field j:I
20: iconst_4
21: putstatic #26 // Field j:I
24: return

内部会创建静态字段i、j以及指向内部类对象的静态引用(public static final kotlindemo01.StaticDemo$Companion Companion)。

在上面的例子中,StaticDemo可以直接调用i或j属性,和Java不一样的是,StaticDemo对象并不能调用i或j的属性。在main方法中执行StaticDemo.i语句,反编译有:

1
2
3
4
5
6
7
8
9
public static final void main(java.lang.String[]);
Code:
0: aload_0
1: ldc #9 // String args
3: invokestatic #15 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: getstatic #21 // Field kotlindemo01/StaticDemo.Companion:Lkotlindemo01/StaticDemo$Companion;
9: invokevirtual #27 // Method kotlindemo01/StaticDemo$Companion.getI:()I
12: pop
13: return

可见StaticDemo.i会被编译器编译为内部类的实例调用后者的getI方法(结合后文另一种调用方式解释了为什么在Java中需要写全为StaticDemo.Companion.j,Java的编译器不会编译出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
public final class kotlindemo01.StaticDemo$Companion {
public final int getI();
Code:
0: invokestatic #11 // Method kotlindemo01/StaticDemo.access$getI$cp:()I
3: ireturn
public final void setI(int);
Code:
0: iload_1
1: invokestatic #18 // Method kotlindemo01/StaticDemo.access$setI$cp:(I)V
4: return
public final int getJ();
Code:
0: invokestatic #24 // Method kotlindemo01/StaticDemo.access$getJ$cp:()I
3: ireturn
public final void setJ(int);
Code:
0: iload_1
1: invokestatic #28 // Method kotlindemo01/StaticDemo.access$setJ$cp:(I)V
4: return
public kotlindemo01.StaticDemo$Companion(kotlin.jvm.internal.DefaultConstructorMarker);
Code:
0: aload_0
1: invokespecial #34 // Method "<init>":()V
4: return
}

每一个普通方法调用的又是StaticDemo下的kotlindemo01/StaticDemo.access$getI$cp等静态方法。这也解释了为什么StaticDemo.Companion.j也可以运行的原因。

通过反编译还可以了解的是:companion object括号内声明的属性,是属于StaticDemo的字段(kotlindemo01/StaticDemo.i:I),而且是静态字段。

使用@JvmStatic可以将半生对象中的字段转化为纯Java中的静态字段。

避免双感叹号使用

在改写Java代码的时候,最常遇见的问题是IDE提示“Only safe(?.) or non-null asserted(!!.)…”,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class NotNullDemo {
var b: B? = null
fun testMethod() {
var s = b.pr // 点号处报错
print(s)
}
}
class B {
var pr:Int = 3
}
fun main(args: Array<String>) {
var n = NotNullDemo()
n.testMethod()
}

此时采用双感叹号会立马解决问题,但是本人认为采用了这种方法后和在Java中使用null指针别无二致了,因为一旦方法接收者或者字段持有对象等为空就会抛出异常。另外的方法:

1
2
3
var s = b?.let { it.pr }
// 或者: var s = b?.pr
// 如果希望返回指定值: var s = b?.pr?: 2

注释中?:是猫王操作符,用法类似三元运算符。

Kotlin之迭代

通常一般性的迭代可以有两种等价的方式,下面用一个小例子说明:

1
2
3
4
5
6
7
8
9
10
11
12
fun main(args: Array<String>) {
var l = listOf<Int>(2, 1, 3, 4)
for (i in l.iterator())
print("$i ")
println()
var iter = l.iterator()
while (iter.hasNext()) {
var e = iter.next()
print("$e ")
}
}

在Java中对集合进行迭代时不能进行remove等操作,在Kotlin中分可变集合和不可变集合,对不可变集合可以进行相应的更改操作,通常后者类名或接口名的前缀会多出“Mutable”字样,表示可变。

在写代码的过程中,forEach可能嵌套多层,这个时候可能出现it重复的问题,可以更改一层的it:

1
l?.let { it.forEach {e -> print("$e ")} }

数组的运用

用一组代码比较Java和Kotlin:

1
2
3
int[] intArr = {1, 2} // var intArr = arrayOf(1, 2)
int[] intArr01 = new int[3] // var intArr01 = InitArray(3)
String[] intArr02 = new String[4] // var arr04 = arrayOfNulls<String>(4)

采用Array(3)编译不会通过,基本类型的Array可以采用类型名Array数组范围的初始化形式(例如InitArray(3)),非基本类型可以使用arrayOfNulls。

Array初始化的基本方法:

1
var arr = Array(3) {Array(3, {it -> it.inc() - 1}) }

相当于对一个[0, 1, 2]数组的每个元素进行Lambda表达式的处理,上述代码是支持Lambda柯里化的:

1
var arr = Array(3) { Array(3) {it -> it.inc() - 1} }

当然参数也可以传入函数变量:

1
2
3
4
5
var f = (fun(x: Int): Int {
return x
})
var arr02 = Array(3, f)

创建空数组:

1
var empty = emptyArray<Int>()

必须配置泛型。

小结

本人在重构Java代码的时候遇到了一些常见的代码问题,以上是解决这些问题后的总结,后续还会适时更新。

参考

Kotlin的诞生:专访JetBrains的Andrey Breslav

How to overcome “same JVM signature” error when implementing a Java interface?