Kotlin的设计主管宣称“Kotlin修复了Joshua Bloch的Java Puzzlers丛书中提及的半数问题”,《Effective Java》应该也算丛书的一本吧。本人在使用Kotlin时已经强烈的感受到了这一点。此篇主要记录本人在使用Kotlin重构Java代码时中遇到的一些问题。
命令行编译与运行
假设现在有一个写好的kotlin文件Ak.kt,首先对其进行编译:
|
|
-include-runtime用来打包kotlin运行时库。
接下来运行程序:
|
|
伴生对象硬核原理
虽然在Kotlin的世界里,人们一直强调没有static没有static,但本人还是想通过查看字节码文件这种硬核的方式理解一些概念。编写一个Kotlin例子,package为ObjectDemo:
|
|
这个例子在AA中放一个i属性,一个methodA方法;伴生对象内放一个j属性以及methodB方法。
编译之后会生成三个文件AA$BB.class
、AA.class
、ObjectDemoKt.class
。
虽说main在源文件里是一个所谓的顶层函数,但其实编译后会将其放入一个ObjectDemoKt类中,默认类名就是包名+Kt。
接下来本人将主要分析AA$BB.class、AA.class这两个文件。
首先是AA.class,对其进行反编译有:
|
|
通过这个反编译结果,可以迅速得到一些结论:
- 写在伴生对象内j属性会被编译成AA的静态字段j;
- 写在AA中的var属性i会被编译为普通字段i,并配有final的普通get、set方法;
- j还会在AA中被编译出隐含的static final的get、set方法 ;
- methodA()在AA中是final的普通方法;
- AA内还会存在一个static、final的BB对象的静态初始化器;
再来看看AA$BB.class:
|
|
通过这个反编译结果,又可以得出些结论:
- BB按照Java中来理解,相当于是AA的内部类;
- 在BB内会编译出j的get、set方法,它们是普通的、final的,包装了AA中隐含的static final的get、set方法;
- methodB()也是final、非静态的;
由此很多概念就不陌生了。
Java静态块改写
Kotlin中没有静态的概念,可以使用伴生对象来实现。例如:
|
|
对其使用javap反编译,摘取其中static{}部分:
|
|
内部会创建静态字段i、j以及指向内部类对象的静态引用(public static final kotlindemo01.StaticDemo$Companion Companion)。
在上面的例子中,StaticDemo可以直接调用i或j属性,和Java不一样的是,StaticDemo对象并不能调用i或j的属性。在main方法中执行StaticDemo.i语句,反编译有:
|
|
可见StaticDemo.i会被编译器编译为内部类的实例调用后者的getI方法(结合后文另一种调用方式解释了为什么在Java中需要写全为StaticDemo.Companion.j,Java的编译器不会编译出Kotlin编译器的结果),反编译该内部类:
|
|
每一个普通方法调用的又是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(!!.)…”,例如:
|
|
此时采用双感叹号会立马解决问题,但是本人认为采用了这种方法后和在Java中使用null指针别无二致了,因为一旦方法接收者或者字段持有对象等为空就会抛出异常。另外的方法:
|
|
注释中?:
是猫王操作符,用法类似三元运算符。
Kotlin之迭代
通常一般性的迭代可以有两种等价的方式,下面用一个小例子说明:
|
|
在Java中对集合进行迭代时不能进行remove等操作,在Kotlin中分可变集合和不可变集合,对不可变集合可以进行相应的更改操作,通常后者类名或接口名的前缀会多出“Mutable”字样,表示可变。
在写代码的过程中,forEach可能嵌套多层,这个时候可能出现it重复的问题,可以更改一层的it:
|
|
数组的运用
用一组代码比较Java和Kotlin:
|
|
采用Array(3)编译不会通过,基本类型的Array可以采用类型名
加Array
加数组范围
的初始化形式(例如InitArray(3)),非基本类型可以使用arrayOfNulls。
Array初始化的基本方法:
|
|
相当于对一个[0, 1, 2]数组的每个元素进行Lambda表达式的处理,上述代码是支持Lambda柯里化的:
|
|
当然参数也可以传入函数变量:
|
|
创建空数组:
|
|
必须配置泛型。
小结
本人在重构Java代码的时候遇到了一些常见的代码问题,以上是解决这些问题后的总结,后续还会适时更新。
参考
Kotlin的诞生:专访JetBrains的Andrey Breslav
How to overcome “same JVM signature” error when implementing a Java interface?