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),如关键字(classpublic)、标识符(类名、变量名)、运算符(+-)、分隔符({})等。
  • 忽略空白字符和注释。
  • 例如,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。
  • 解析方法通常有两种:
    1. 自顶向下解析(Top-Down Parsing)
      • 从根节点开始,逐步推导出子节点。
      • 例如,递归下降解析器(Recursive Descent Parser)。
    2. 自底向上解析(Bottom-Up Parsing)
      • 从叶子节点开始,逐步合并为更复杂的结构。
      • 例如,LR 解析器。

3. 构建 AST 节点

  • 语法分析器根据语法规则创建 AST 节点,并将它们连接起来。
  • 每个节点通常包含以下信息:
    • 节点类型(如赋值、表达式、变量等)。
    • 子节点(如操作符、操作数等)。
    • 附加信息(如变量名、常量值等)。

4. 处理嵌套结构

  • 语法分析器需要处理嵌套的语法结构(如嵌套的表达式、循环、条件语句等)。
  • 例如,a = (b + c) * d; 的 AST 需要正确反映括号的优先级。

具体构建过程示例

以下是一个具体的例子,展示如何从标记流构建 AST。

源代码

int a = 10 + 20;

标记流

[int, a, =, 10, +, 20, ;]

构建过程

  1. 识别赋值语句

    • 语法规则:Assignment → Variable '=' Expression
    • 创建 Assignment 节点。
    • 左子节点为 Variable 节点,值为 a
  2. 解析表达式

    • 语法规则:Expression → BinaryExpression
    • 创建 BinaryExpression 节点。
    • 操作符为 +
    • 左操作数为 Literal 节点,值为 10
    • 右操作数为 Literal 节点,值为 20
  3. 连接节点

    • 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 的