0%

kotlinc vs javac

前言

对比一下 java 和 kotlin 编译源码文件(source.java 或 source.kt) 到 class 的区别。这种区别产生的结果是什么,看看其中的优缺点。

javac

1
2
3
4
5
public class Hello {
public static void main(String[] args) {
System.out.println("hello world");
}
}

以上代码执行 javac Hello.java 将生成 Hello.class 文件,用 javap 命令对生成的 Hello.class 文件做一下解析,得到结果

Hello.clsss 解析后的信息

点击展开
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

Classfile /Users/mac/Desktop/javadir/Hello.class
Last modified 2019-8-24; size 415 bytes
MD5 checksum cb7981a0b53212d3f3005746f14b245a
Compiled from "Hello.java"
public class Hello
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // hello world
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // Hello
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Hello.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 hello world
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 Hello
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public Hello();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "Hello.java"

可以得到一些关键信息:

  • 生成的 .class 文件大小 415 bytes
  • .class 文件中除了 main 方法的指令之外,还默认添加了一个 Hello 类的构造函数,即 init 方法。

kotlinc

下面看看,用 Kotlin 语言实现同样的功能,然后编译之后的.class 文件又是怎样的。

kotlinc

安装 kotlinc

命令行输入

brew install kotlin

Hello1.kt

1
2
3
4
5
6
object Hello1 {
@JvmStatic
fun main(args: Array<String>) {
println("hello world")
}
}

这段代码和上述 Hello.java 是实现了同样的功能,方法参数也是一样。

以上代码执行 kotlinc Hello1.kt 后将生成 Hello1.class 文件,用 javap 命令对生成的 Hello1.class 文件做一下解析,得到结果

Hello1.clsss 解析后的信息

点击展开
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
Classfile /Users/mac/Desktop/javadir/Hello1.class
Last modified 2019-8-24; size 1236 bytes
MD5 checksum 884060fdf34740300f1c8d187afb4c6a
Compiled from "Hello1.kt"
public final class Hello1
minor version: 0
major version: 50
flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
Constant pool:
#1 = Utf8 Hello1
#2 = Class #1 // Hello1
#3 = Utf8 java/lang/Object
#4 = Class #3 // java/lang/Object
#5 = Utf8 main
#6 = Utf8 ([Ljava/lang/String;)V
#7 = Utf8 Lkotlin/jvm/JvmStatic;
#8 = Utf8 Lorg/jetbrains/annotations/NotNull;
#9 = Utf8 args
#10 = String #9 // args
#11 = Utf8 kotlin/jvm/internal/Intrinsics
#12 = Class #11 // kotlin/jvm/internal/Intrinsics
#13 = Utf8 checkParameterIsNotNull
#14 = Utf8 (Ljava/lang/Object;Ljava/lang/String;)V
#15 = NameAndType #13:#14 // checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
#16 = Methodref #12.#15 // kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
#17 = Utf8 hello world
#18 = String #17 // hello world
#19 = Utf8 java/lang/System
#20 = Class #19 // java/lang/System
#21 = Utf8 out
#22 = Utf8 Ljava/io/PrintStream;
#23 = NameAndType #21:#22 // out:Ljava/io/PrintStream;
#24 = Fieldref #20.#23 // java/lang/System.out:Ljava/io/PrintStream;
#25 = Utf8 java/io/PrintStream
#26 = Class #25 // java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/Object;)V
#29 = NameAndType #27:#28 // println:(Ljava/lang/Object;)V
#30 = Methodref #26.#29 // java/io/PrintStream.println:(Ljava/lang/Object;)V
#31 = Utf8 [Ljava/lang/String;
#32 = Utf8 <init>
#33 = Utf8 ()V
#34 = NameAndType #32:#33 // "<init>":()V
#35 = Methodref #4.#34 // java/lang/Object."<init>":()V
#36 = Utf8 this
#37 = Utf8 LHello1;
#38 = Utf8 INSTANCE
#39 = Utf8 <clinit>
#40 = Utf8 Lkotlin/Metadata;
#41 = Utf8 mv
#42 = Integer 1
#43 = Integer 15
#44 = Utf8 bv
#45 = Integer 0
#46 = Integer 3
#47 = Utf8 k
#48 = Utf8 d1
#49 = Utf8 \n\n\n\n\n\n\n\nÆ20B¢J02 00H¢
#50 = Utf8 d2
#51 = Utf8
#52 = Methodref #2.#34 // Hello1."<init>":()V
#53 = NameAndType #38:#37 // INSTANCE:LHello1;
#54 = Fieldref #2.#53 // Hello1.INSTANCE:LHello1;
#55 = Utf8 Hello1.kt
#56 = Utf8 Code
#57 = Utf8 LineNumberTable
#58 = Utf8 LocalVariableTable
#59 = Utf8 RuntimeVisibleAnnotations
#60 = Utf8 RuntimeInvisibleParameterAnnotations
#61 = Utf8 SourceFile
#62 = Utf8 SourceDebugExtension
{
public static final Hello1 INSTANCE;
descriptor: LHello1;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

public static final void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: ldc #10 // String args
3: invokestatic #16 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: ldc #18 // String hello world
8: astore_1
9: iconst_0
10: istore_2
11: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
14: aload_1
15: invokevirtual #30 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
18: return
LineNumberTable:
line 4: 6
line 5: 18
LocalVariableTable:
Start Length Slot Name Signature
0 19 0 args [Ljava/lang/String;
RuntimeVisibleAnnotations:
0: #7()
RuntimeInvisibleParameterAnnotations:
0:
0: #8()

static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: new #2 // class Hello1
3: dup
4: invokespecial #52 // Method "<init>":()V
7: astore_0
8: aload_0
9: putstatic #54 // Field INSTANCE:LHello1;
12: return
LineNumberTable:
line 1: 0
}
SourceFile: "Hello1.kt"
SourceDebugExtension:
SMAP
Hello1.kt
Kotlin
*S Kotlin
*F
+ 1 Hello1.kt
Hello1
*L
1#1,6:1
*E
RuntimeVisibleAnnotations:
0: #40(#41=[I#42,I#42,I#43],#44=[I#42,I#45,I#46],#47=I#42,#48=[s#49],#50=[s#37,s#51,s#33,s#5,s#51,s#9,s#51,s#51,s#6])

可以得到一些关键信息:

  • 首先最直观的一点,这个 class 文件的信息变多了
  • Hello1.class 文件的信息,达到了 1236 bytes
  • 多出来的信息,主要是 SourceDebugExtension,RuntimeVisibleAnnotations,RuntimeInvisibleParameterAnnotations 和 LocalVariableTable(本地变量表的信息)
  • 构造函数的 init 基本相似,变化不大
  • 常量池 Constant Pool 变大了近乎一倍之多。
  • main 方法由于要做参数检测,也是变大了许多。

总结

Hello.class vs Hello1.class

最终直观感受一下:

1
2
3
4
-rw-r--r--   1 mac  staff   415  8 24 09:37 Hello.class
-rw-r--r--@ 1 mac staff 114 8 24 08:37 Hello.java
-rw-r--r-- 1 mac staff 1236 8 24 09:37 Hello1.class
-rw-r--r-- 1 mac staff 92 8 24 09:34 Hello1.kt

用 kotlin 实现和 java 代码相同的功能,kotlin 在源文件上是占优的,这也是 kotlin 的优势,代码简洁,但是产生的代价就是 class 文件的变大(当然这里的为了实现 static 的 main 方法,使用了类似单例的写法,可能不太恰当,但是总体趋势是类似的)

扩展

我们知道 javac 编译生成的 xxx.class 文件是可以通过 java xxx 直接运行的,那么 kotlinc 生成的 yyy.class 文件改怎么运行呢?可以直接用 java yyy 吗?我们试一下

1
2
3
──> java Hello1                                                 ──(六, 824)─┘
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
at Hello1.main(Hello1.kt)

结果是不行的,虽然源码可以互相调用,但是 class 文件还的各来各的。因此需要用 kotlin yyy

1
2
──> kotlin Hello1                                           1 ↵ ──(六, 824)─┘
hello world
加个鸡腿呗.