相较于Java,Kotlin 提供了委托属性、高阶函数等众多新的语言特性,亦在语言层面提供了丰富的语法糖为开发者提供便利,若对这些语言特性和语法糖背后的原理不甚了解,很容易因误用造成程序错误和性能损耗;此外,我们在使用 Java 时的一些习惯若延续到 Kotlin 等其他编程语言中去时,亦不一定完全适用,使用不当亦很有可能会带来错误或性能问题。为避免产生这样的问题,本文对 Kotlin 语言层面的一些常见的使用细节与 Java 进行了对比探究,以供参考。
- 本文中总结的这些性能开销 Tips 多数来自于网络上现有的资料,但是会在基于近日发布的新版 Kotlin 1.4 进行重新分析验证
- Kotlin 是跨平台实现的语言,本文不考虑其在 native、web 等平台的实现,仅仅基于 Kotlin 在 JVM 上的实现探讨其语言特性的一些实现细节
- 为数不多的人知道的 Kotlin 技巧以及 原理解析 - 掘金
- 为数不多的人知道的 Kotlin 技巧以及 原理解析(二) - 掘金
- https://medium.com/rsq-technologies/comparative-evaluation-of-selected-constructs-in-java-and-kotlin-part-1-dynamic-metrics-2592820ce80
- https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62
- https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-2-324a4a50b70
- https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-3-3bf6e0dbf0a4
- 探索 Java 隐藏的开销
Intelij Idea 或者 Android Studio 中可以打开 Kotlin ByteCode 工具查看对应字节码,亦可以 Decompile 该字节码以反编译成对应 Java 代码.
我们可以通过对比 Kotlin 和 Java 的同一实现下的差异,对 Kotlin 的一些语言特性的实现、使用时的性能损耗等进行分析学习.
1.单例的实现在 kotlin 中可以轻松使用 object 声明一个单例,如下:
1
2
3
4
5
6
object Single {
val hello =
"hello world"
fun sayHello() =
"hello world"
}
使用该单例时直接使用类名即可访问对应的成员,
1
2
Single.hello
Single.sayHello()
无论是单例的实现还是使用都比 Java 简洁了许多。实际上其背后的原理也没有多么神奇,将上述单例反编译为 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
public
final
class
Single {
@NotNull
private
static
final
String hello;
public
static
final
Single INSTANCE;
@NotNull
public
final
String getHello() {
return
hello;
}
@NotNull
public
final
String sayHello() {
return
"hello world"
;
}
private
Single() {
}
static
{
Single var0 =
new
Single();
INSTANCE = var0;
hello =
"hello world"
;
}
}
可见其背后的原理仅仅是利用 JVM 的类加载机制实现了一个传统的 Java 饿汉式的单例模式而已。
饿汉式单例的缺点是类一旦加载对应的实例便被初始化,那么如何使用 Kotlin 方便的实现线程安全的、懒汉式的单例模式呢,示例代码如下:
1
2
3
4
5
6
7
8
9
10
class
LazySingle
private
constructor() {
fun sayHello() =
"hello world"
companion object {
val INSTANCE by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
LazySingle()
}
}
}
此处使用了 Kotlin 中提供的委托属性特性,可以参考 Kotlin 的文档,了解 by lazy 的作用。
2.companion objectKotlin 并不具备 Java 语言的 static 关键字特性,但是可以通过使用 companion object 实现类似的“语法效果”,但 companion object 实现的这种语法效果与 Java Static 特性本质还是有差别的,这其中其实存在一定的性能损耗。
以下代码展示了一个简易的 companion object 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class
OuterClazz {
private
val outerSalary =
12000
val outerName =
"OuterName"
fun outerSayHi() {
print(
"Hi"
)
}
companion object Inner {
private
val innerSalary =
12000
val innerName =
"InnerName"
fun innerSayHi() {
print(
"Hi"
)
}
}
}
在其他代码处访问 innerName 成员时,可以直接使用 OuterClazz.innerName 的形式来访问,语法看上去与 Java 中访问 static 成员类似。
OuterClazz.innerName 这种形式实际上时 OuterClazz.Inner.innerName 的缩写
将上述代码反编译后得到对应 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
36
37
38
39
40
public
final
class
OuterClazz {
private
final
int
outerSalary =
12000
;
@NotNull
private
final
String outerName =
"OuterName"
;
private
static
final
int
innerSalary =
12000
;
@NotNull
private
static
final
String innerName =
"InnerName"
;
public
static
final
OuterClazz.Inner Inner =
new
OuterClazz.Inner((DefaultConstructorMarker)
null
);
@NotNull
public
final
String getOuterName() {
return
this
.outerName;
}
public
final
void
outerSayHi() {
String var1 =
"Hi"
;
boolean
var2 =
false
;
System.out.print(var1);
}
public
static
final
class
Inner {
@NotNull
public
final
String getInnerName() {
return
OuterClazz.innerName;
}
public
final
void
innerSayHi() {
String var1 =
"Hi"
;
boolean
var2 =
false
;
System.out.print(var1);
}
private
Inner() {
}
public
Inner(DefaultConstructorMarker $constructor_marker) {
this
();
}
}
}
外部类实际上实例化了一个 companion object 的单例,我们访问 innerName 的过程实际上是 OuterClazz.Inner.getInnerName, 而 getInnerName 方法又去访问了外部类的一个静态成员. 这其中着实兜了一个大圈子,造成了一定的性能损耗。
可以通过为 Inner 的成员添加 const 或者 @JvmField 声明,避免内部类产生多余的 getXXX 方法、造成多余的访问。
1
2
3
4
5
6
class
OuterClazz2 {
companion object Inner {
// 此处可以使用 const 或者 @JvmField 将其声明为常量
@JvmField
val innerName =
"InnerName"
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public
final
class
OuterClazz2 {
@JvmField
@NotNull
// 并未再再 Inner 中生成 getter 方法进行冗余的访问
public
static
final
String innerName =
"InnerName"
;
public
static
final
OuterClazz2.Inner Inner =
new
OuterClazz2.Inner((DefaultConstructorMarker)
null
);
public
static
final
class
Inner {
private
Inner() {
}
// $FF: synthetic method
public
Inner(DefaultConstructorMarker $constructor_marker) {
this
();
}
}
}
至此,我们如果再回过头看上一节中使用 companion object 实现的懒汉式的单例,其中实际上也存在比较蠢的一点实现:尽管外部类的实例化延迟了,但是一旦加载外部类,内部也会立即会去初始化为一个 companion object 的实例, 依旧造成了一丢丢内存浪费.
3. 委托属性前面的 Kotlin 版懒汉式单例使用到了 by lazy 这一 Kotlin 内置的委托属性,并且通过指定 lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) 保证了对实例进行初始化、读取时的线程安全. 若不指定此处的 LazyThreadSafetyMode,那么默认值即为 SYNCHRONIZED,在实际开发时,若是在单线程环境下使用 lazy 委托属性,则可以考虑将 mode = LazyThreadSafetyMode.NONE 来优化性能.
todo: 反编译字节码说明原理. Effective Class Delegation - zsmb.co