java、kotlin编译器是怎样将代码编译为class的
Table of Contents
Java 和 Kotlin 编译器如何将代码编译为 Class 文件
Java 和 Kotlin 是 Android 开发中广泛使用的两种编程语言,它们的代码最终都会被编译为 JVM 字节码(.class
文件)。本文将详细解析 Java 和 Kotlin 编译器如何将源代码编译为 .class
文件,并重点展开“使用标记流构建抽象语法树(AST)”这一步骤。
编译过程的详细步骤
1. 词法分析(Lexical Analysis)
目标:将源代码分解为有意义的标记(tokens)。
Java (javac
)
- 读取
.java
文件,逐字符扫描源代码。 - 将字符流分解为标记(tokens),如关键字(
class
、public
)、标识符(类名、变量名)、运算符(+
、-
)、分隔符({
、}
)等。 - 忽略空白字符和注释。
- 例如,
int a = 10;
会被分解为int
(关键字)、a
(标识符)、=
(运算符)、10
(字面量)、;
(分隔符)。
Kotlin (kotlinc
)
- 与 Java 类似,读取
.kt
文件并分解为标记。 - Kotlin 的标记可能更复杂,因为它支持更多特性,如空安全符号(
?
)、扩展函数等。
2. 语法分析(Syntax Analysis)
目标:根据语法规则将标记组织成抽象语法树(AST)。
Java (javac
)
- 使用标记流构建抽象语法树(AST)。
- 检查语法是否正确,例如类定义、方法声明、表达式结构是否符合 Java 语法规则。
- 例如,
int a = 10;
会被解析为一个赋值表达式节点,包含变量声明和初始值。
Kotlin (kotlinc
)
- 同样构建 AST,但 Kotlin 的语法更复杂,支持更多特性(如数据类、密封类、Lambda 表达式等)。
- 检查 Kotlin 特有的语法规则,例如空安全、扩展函数等。
3. 语义分析(Semantic Analysis)
目标:检查代码的逻辑正确性,确保语义合法。
Java (javac
)
- 检查变量是否已声明、类型是否匹配、方法是否存在等。
- 检查继承关系、接口实现是否正确。
- 检查常量表达式的值是否合法。
- 例如,
int a = "hello";
会报错,因为类型不匹配。
Kotlin (kotlinc
)
- 进行类似的语义检查,但 Kotlin 的语义分析更复杂。
- 检查空安全(
?
和!!
的使用是否正确)。 - 检查类型推断是否正确(Kotlin 支持自动类型推断)。
- 检查扩展函数的作用域和调用是否合法。
4. 中间代码生成(Intermediate Code Generation)
目标:将 AST 转换为中间表示(IR),便于后续优化和字节码生成。
Java (javac
)
- 将 AST 转换为一种低级的三地址代码(Three-Address Code)或类似中间表示。
- 例如,
a = b + c;
可能被转换为:t1 = b + c a = t1
Kotlin (kotlinc
)
- 同样生成中间表示,但 Kotlin 的中间表示可能更复杂,因为它需要支持 Kotlin 的特性(如 Lambda 表达式、协程等)。
- Kotlin 的中间表示可能包含额外的元数据,以支持 Kotlin 的运行时特性。
5. 优化(Optimization)
目标:对中间代码进行优化,提高运行效率。
Java (javac
)
- 进行简单的优化,例如:
- 常量折叠:
int a = 10 + 20;
优化为int a = 30;
。 - 死代码删除:移除永远不会执行的代码。
- 方法内联:将小方法直接嵌入调用处。
- 常量折叠:
- 由于
javac
的优化能力有限,大部分优化由 JVM 在运行时完成。
Kotlin (kotlinc
)
- 进行类似的优化,但 Kotlin 的优化可能更复杂。
- 例如,内联函数(
inline
关键字)会在编译时直接展开。 - 空安全检查的优化:移除不必要的空安全检查。
6. 字节码生成(Bytecode Generation)
目标:将优化后的中间代码转换为 JVM 字节码。
Java (javac
)
- 将中间代码转换为 JVM 字节码(
.class
文件)。 - 字节码是一种平台无关的低级指令集,由 JVM 解释或编译执行。
- 例如,
int a = 10;
可能被转换为:bipush 10 istore_1
Kotlin (kotlinc
)
- 同样生成 JVM 字节码,但 Kotlin 的字节码可能包含额外的元数据。
- 例如,Kotlin 的空安全特性会在字节码中插入额外的检查指令。
7. 输出(Output)
目标:将字节码写入 .class
文件。
Java (javac
)
- 每个类生成一个
.class
文件。 .class
文件包含字节码、常量池、方法表、字段表等。
Kotlin (kotlinc
)
- 同样生成
.class
文件,但可能包含额外的元数据文件(如.kotlin_module
文件),以支持 Kotlin 的特性。
8. 后续处理(Post-processing)
目标:处理额外的元数据或生成其他文件。
Java (javac
)
- 无额外步骤。
Kotlin (kotlinc
)
- 生成
.kotlin_module
文件,用于模块化支持。 - 生成额外的元数据文件,以支持 Kotlin 的反射和空安全特性。
什么是抽象语法树(AST)?
抽象语法树(Abstract Syntax Tree, AST) 是源代码的一种树状表示形式,它抽象了源代码的语法结构,忽略了一些无关紧要的细节(如分号、括号等),只保留程序的核心逻辑结构。
-
特点:
- 每个节点表示源代码中的一个语法结构(如表达式、语句、声明等)。
- 树的层次结构反映了代码的嵌套关系。
- 叶子节点通常是变量、常量或操作符,而非叶子节点表示更复杂的结构(如函数、类、循环等)。
-
例子: 对于代码
int a = 10 + 20;
,其 AST 可能如下:Assignment ├── Variable: a └── BinaryExpression ├── Operator: + ├── Literal: 10 └── Literal: 20
AST 的构建过程
AST 的构建是通过 语法分析器(Parser) 完成的。语法分析器根据语言的语法规则,将标记流(tokens)转换为 AST。
1. 定义语法规则
- 每种编程语言都有其语法规则,通常用 上下文无关文法(Context-Free Grammar, CFG) 描述。
- 例如,Java 的赋值语句可以用以下规则表示:
Assignment → Variable '=' Expression Expression → Literal | BinaryExpression BinaryExpression → Expression Operator Expression
2. 解析标记流
- 语法分析器读取标记流,并根据语法规则逐步构建 AST。
- 解析方法通常有两种:
- 自顶向下解析(Top-Down Parsing):
- 从根节点开始,逐步推导出子节点。
- 例如,递归下降解析器(Recursive Descent Parser)。
- 自底向上解析(Bottom-Up Parsing):
- 从叶子节点开始,逐步合并为更复杂的结构。
- 例如,LR 解析器。
- 自顶向下解析(Top-Down Parsing):
3. 构建 AST 节点
- 语法分析器根据语法规则创建 AST 节点,并将它们连接起来。
- 每个节点通常包含以下信息:
- 节点类型(如赋值、表达式、变量等)。
- 子节点(如操作符、操作数等)。
- 附加信息(如变量名、常量值等)。
4. 处理嵌套结构
- 语法分析器需要处理嵌套的语法结构(如嵌套的表达式、循环、条件语句等)。
- 例如,
a = (b + c) * d;
的 AST 需要正确反映括号的优先级。
具体构建过程示例
以下是一个具体的例子,展示如何从标记流构建 AST。
源代码
int a = 10 + 20;
标记流
[int, a, =, 10, +, 20, ;]
构建过程
-
识别赋值语句:
- 语法规则:
Assignment → Variable '=' Expression
- 创建
Assignment
节点。 - 左子节点为
Variable
节点,值为a
。
- 语法规则:
-
解析表达式:
- 语法规则:
Expression → BinaryExpression
- 创建
BinaryExpression
节点。 - 操作符为
+
。 - 左操作数为
Literal
节点,值为10
。 - 右操作数为
Literal
节点,值为20
。
- 语法规则:
-
连接节点:
- 将
BinaryExpression
节点作为Assignment
节点的右子节点。
- 将
最终 AST
Assignment
├── Variable: a
└── BinaryExpression
├── Operator: +
├── Literal: 10
└── Literal: 20
AST 的作用
- 语义分析:AST 是语义分析的基础,编译器通过遍历 AST 检查类型、作用域等语义信息。
- 代码优化:优化器通过分析 AST 进行常量折叠、死代码删除等优化。
- 代码生成:代码生成器通过遍历 AST 生成目标代码(如字节码、机器码)。
总结
Java 和 Kotlin 的编译过程非常相似,都包括词法分析、语法分析、语义分析、中间代码生成、优化、字节码生成和输出。Kotlin 的编译过程更复杂,因为它需要支持 Kotlin 特有的特性(如空安全、扩展函数、协程等)。最终,两者都生成 .class
文件,可以在 JVM 上运行。AST 是编译器的核心数据结构,后续的语义分析、优化和代码生成都依赖于 AST。
明天了解一下[[20250124-apk打包过程]]中提到的 dex 编译,d8、r8 是怎样将 class 转换为 dex 的