确保 JDK 已安装并配置好环境变量。可在命令行输入 javac -version
检查是否安装成功,若成功会显示 javac
版本信息。
使用文本编辑器编写 Java 代码,例如创建 HelloWorld.java
文件:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Win + R
组合键,输入 cmd
并回车。cd
命令,如 cd D:\java_projects
。javac HelloWorld.java
,编译成功后会在同一目录下生成 HelloWorld.class
文件。java HelloWorld
运行编译后的程序,输出结果为 Hello, World!
。-d
选项,如 javac -d D:\output HelloWorld.java
,将生成的 .class
文件输出到指定目录。假设两个文件在同一目录,并且 a.java
定义了一个类,b.java
引用了该类。以下是详细步骤:
a.java
文件内容:
// a.java
public class A {
public static void sayHello() {
System.out.println("Hello from A!");
}
}
b.java
文件内容:
// b.java
public class B {
public static void main(String[] args) {
A.sayHello();
}
}
在命令行中,导航到这两个文件所在的目录,然后可以采用以下两种编译方式。
先编译 a.java
,再编译 b.java
:
javac a.java
javac b.java
javac a.java
会生成 A.class
文件,javac b.java
会在编译时找到 A.class
文件,因为它们在同一目录下。
可以同时编译两个文件:
javac a.java b.java
javac
会自动处理文件之间的依赖关系,先编译 a.java
,再编译 b.java
。
编译成功后,使用 java
命令运行 B
类:
java B
假设 a.java
在 path/to/a
目录,b.java
在 path/to/b
目录。
a.java
文件内容保持不变。
b.java
文件内容:
// b.java
import path.to.a.A;
public class B {
public static void main(String[] args) {
A.sayHello();
}
}
这里使用 import
语句引入 A
类。
在命令行中,需要通过 -classpath
(或 -cp
)选项指定 A.class
文件的路径,以便 javac
能够找到它。
javac -cp path/to/a b.java
-cp
选项告诉 javac
在 path/to/a
目录中查找所需的类文件。
运行 B
类时,同样需要指定类路径:
java -cp path/to/a:path/to/b B
在 Linux 或 macOS 系统中,类路径使用冒号 :
分隔;在 Windows 系统中,使用分号 ;
分隔。
Java 核心类库(如 java.lang
、java.util
、java.io
等包中的类)是 JRE 的一部分,javac
编译和 java
运行时默认包含其路径,无需手动指定。例如:
import java.util.Date;
public class CoreLibraryExample {
public static void main(String[] args) {
Date currentDate = new Date();
System.out.println("Current date: " + currentDate);
}
}
可直接使用 javac CoreLibraryExample.java
编译,java CoreLibraryExample
运行。
javac -cp "path/to/jre/lib/rt.jar" CoreLibraryExample.java
。javac
编译多个文件时,通常能分析文件间的依赖关系,按正确顺序编译。先编译被引用的类,再编译引用类。如 A.java
引用 B.java
时,使用 javac A.java B.java
或 javac *.java
可自动处理。
循环依赖:类 A 依赖类 B,类 B 又依赖类 A 时,编译器可能无法自动解决,应尽量避免这种设计。
外部库依赖:代码引用外部库(非 Java 核心类库)时,需使用 -classpath
(或 -cp
)选项手动指定外部库路径,如 javac -cp path/to/library.jar A.java B.java
。
跨包依赖:不同包中的类相互引用时,编译器能处理,但需保证包结构正确,且编译时指定正确的源文件路径。
字符流到词法单元的转换:正如你所讲,.java
文件里的代码最初就是一连串的字符组成的字符串。词法分析器的任务就是按照 Java 语言规定的词法规则,把这串字符切割成一个个有意义的词法单元,像关键字(int
、class
)、标识符(变量名、类名)、运算符(+
、=
)、常量(数字、字符串)和分隔符(;
、{
、}
)等。这就好比把一篇文章拆分成一个个单词,方便后续处理。
词法规则的遵循:这些词法规则是 Java 语言定义好的,例如标识符必须以字母、下划线或美元符号开头,后面可以跟字母、数字、下划线或美元符号等。词法分析器严格按照这些规则进行分割,确保得到的词法单元是合法的。
构建抽象语法树:拿到词法单元后,语法分析器就开始根据 Java 的语法规则来构建抽象语法树。语法规则规定了各种语句和表达式的正确形式,比如赋值语句、方法调用语句、循环语句等的结构,类似于英语里的主谓宾,主谓宾宾补语句,在英语中做语法分析的时候,就是先把每个词拎出来,然后去主谓宾、主谓宾宾补等语法规定的语句中去匹配,匹配上了就能够确认这些词组成的是一个什么语句。通过对词法单元的组合和分析,将它们组织成树状结构,树的每个节点代表一个语法结构或操作,节点之间的关系反映了代码的层次和逻辑顺序。比如i=1对应一个树,比如树的根节点表示这是一个赋值语句,左节点则是变量i,右节点则是值1.
语法错误检查:在构建抽象语法树的过程中,如果发现词法单元的组合不符合语法规则,就会抛出语法错误。例如,缺少分号、括号不匹配等情况都会被检测出来。
语义正确性验证:抽象语法树构建完成后,语义分析器会对其进行检查,确保代码在语义上是正确的。这涉及到很多方面,如类型检查,保证变量和表达式的类型匹配;作用域检查,确保变量在使用前已经声明且在其作用域内;访问权限检查,防止对私有成员的非法访问等。
符号表的使用:在语义分析过程中,通常会使用符号表来记录变量、类、方法等的信息,包括它们的类型、作用域、访问权限等。符号表可以帮助编译器快速查找和验证这些信息。
从抽象语法树到字节码指令:将抽象语法树转换为字节码指令是一个关键步骤。编译器会遍历抽象语法树,根据每个节点的类型和信息,将其转换为对应的字节码指令。例如,对于赋值语句,会生成将值加载到操作数栈、存储到变量内存位置等一系列指令。
字节码指令集:Java 字节码有一套自己的指令集,每个指令都有特定的功能和操作数。编译器根据抽象语法树的语义选择合适的指令来生成字节码。
提高执行效率:字节码优化的目的是让生成的字节码在 Java 虚拟机上执行得更快、更节省资源。通过常量折叠、死代码消除、方法内联等优化技术,可以减少不必要的计算和方法调用,提高程序的性能。
优化策略的选择:编译器会根据代码的特点和运行环境选择合适的优化策略。不同的优化策略对不同类型的代码可能有不同的效果,编译器会在保证程序正确性的前提下尽可能提高性能。
.class
文件文件格式遵循:最后,编译器将优化后的字节码指令按照 .class
文件的格式要求写入文件。.class
文件有严格的结构,包括文件头、常量池、类信息、方法信息、字段信息等。编译器会将字节码指令和相关的元数据准确地填充到相应的位置,生成一个完整的 .class
文件。
文件的可加载性:生成的 .class
文件可以被 Java 虚拟机加载和执行。Java 虚拟机通过读取 .class
文件的内容,解析其中的字节码指令和元数据,将类加载到内存中并执行相应的方法。
通过这样一系列的步骤,Java 编译器将人类可读的 .java
源代码转换为 Java 虚拟机可执行的 .class
文件,实现了代码从高级语言到低级指令的转换。