1. 现实问题:为什么“安装好了”仍然跑不起来
刚开始学 Java 时,最容易把“IDEA 里点运行成功”和“真正知道程序怎样运行”混为一谈。换一台电脑、换一个目录,或者把 java 和 javac 指到了不同版本,程序就可能报错。工程学习需要一个最小闭环:能看到源文件,能明确调用哪一个编译器,能确认生成了什么,再让同一套运行时启动它。
JDK 是开发工具包,里面至少包含编译器 javac、运行器 java 和标准类库;JRE 这个说法在新版本里不再是独立下载的重点。javac 的工作是把 .java 源码检查并翻译成 .class 字节码,java 的工作是找到类的入口方法并交给 JVM 执行。它们都来自当前命令行能找到的 JDK,但路径不一定相同,所以第一步永远是记录版本。
2. 最小可运行示例:一个文件、一个入口、两条命令
把下面内容保存为 HelloRoute.java。文件名和 public class 名必须一致,这不是风格要求,而是 Java 编译器对公共顶层类的文件约束。
public class HelloRoute {
public static void main(String[] args) {
String learner = args.length == 0 ? "新同学" : args[0];
System.out.println("你好," + learner + ",今天从能运行开始。");
}
}
在同一目录执行:
java --version
javac --version
javac --release 21 HelloRoute.java
java HelloRoute Luna
编译之后会多出 HelloRoute.class。运行命令写的是类名而不是文件名,也不写 .class;JVM 会按照类路径寻找这个二进制类。若使用 JDK 21 的单文件源代码启动,也可以执行 java HelloRoute.java Luna,但这条命令会临时完成编译,适合快速试验,排查编译产物时仍然要用显式 javac。
3. 调用链与对象变化
调用链可以拆成四段。第一段是终端把字符串命令和参数交给操作系统,操作系统按 PATH 找到 javac.exe 或 java.exe。第二段是 javac 读取字符,先做词法、语法和类型检查,再把 main 方法、字符串常量等信息写进字节码。第三段是 java 启动 JVM,加载 HelloRoute,检查它是否有签名为 public static void main(String[] args) 的入口。第四段是 JVM 创建 args 数组,把命令行中的 Luna 变成数组元素,执行字符串拼接和标准输出。
这里发生了一个重要的对象变化:终端参数最初是文本,进入 main 后仍然是 String 引用;learner 只是指向该字符串或条件表达式产生的新字符串的引用。System.out 是 JVM 暴露的标准输出对象,println 接收一个字符串并产生外部可见副作用。程序结束时,进程返回码通常为 0,这和屏幕上打印了一行文字是两种不同的结果。
4. 为什么这样设计
Java 不直接把源代码交给某一种操作系统执行,是因为字节码提供了稳定的中间契约。编译器负责语言规则,JVM 负责加载、验证、内存管理和执行。这样,同一份 .class 可以在不同系统的 JVM 上运行;但“跨平台”不等于完全没有环境差异,文件编码、默认时区、JDK 版本和类路径依然会影响结果。
入口方法使用 static,是因为 JVM 还没有你的业务对象可以调用 main;使用 String[],是因为进程启动时只能可靠传递文本参数。理解这个理由后,看到 main 就不会把它当作需要死记的魔法签名。
5. 项目落点:把实验变成可复现的启动说明
在博客系统或命令行工具中,建议把运行说明写进 README:记录 JDK 主版本、编码、编译命令、运行命令和预期输出。源码目录、生成目录和临时日志分开,避免把 .class 误提交。以后接入 Maven 时,Maven 只是替你管理目录、依赖和编译参数,底层仍然是同一条“源码 → 字节码 → JVM”链。
本课的练习不是修改 IDE 设置,而是新建一个空目录,分别用 JDK 21 和错误版本尝试编译,观察 --release 21 如何把不兼容 API 提前变成编译错误。把终端输出保存下来,下一次遇到构建失败时就有可以比较的证据。
6. 易错排查
javac 不是内部或外部命令:先执行where.exe java和where.exe javac,确认命令是否来自同一个 JDK,再检查PATH,不要先重装。class HelloRoute is public, should be declared in a file named HelloRoute.java:文件名、大小写和公共类名不一致。Could not find or load main class:运行目录不对,或命令中错误地写成了文件路径;用java -cp . HelloRoute明确当前目录类路径。UnsupportedClassVersionError:编译器比运行器新。分别记录两个版本,并让运行器至少不低于生成字节码的版本。
7. 一页复习
记住四个动作:where 找工具,--version 记版本,javac 生成字节码,java 启动入口。编译错误属于源码和工具链,类加载错误属于类路径和版本,运行时异常属于程序执行;把错误归类,排查就不会变成盲目重装。JDK 21 是本路线的代码基线,JDK 25 的新特性另行标注,不把两套行为混在第一课里。
再做一轮有记录的实验:把命令、工作目录、输入参数、生成文件、标准输出和返回码写成六列。javac --release 21 的输入是 UTF-8 源文件,输出是当前目录的 class;java HelloRoute Luna 的输入是类名、类路径和参数,输出是进程产生的文本。若 class 没生成,问题停在编译阶段;若 class 生成却找不到主类,问题进入类路径阶段;若主类启动后报错,才继续看运行时对象和业务逻辑。
项目里可以建 scripts/run-java.ps1 或 README 命令片段,但不要把绝对路径写死。脚本开始时打印 Get-Location、java --version 和 javac --version,结束时检查 $LASTEXITCODE,这样“在我的 IDEA 里能跑”才有可以交接的证据。Windows 的路径空格、文件编码和 PowerShell 的参数转义,都应作为环境差异单独记录,而不是修改源码来迁就。
再观察一次失败:故意把运行命令改成 java HelloRoute.class,会看到运行器把文件名当成类名;故意切到父目录,会看到类路径找不到;故意删掉 .class,会把问题重新推回编译。三个现象的屏幕输出相似,却对应三个不同修复位置。先判断工具、产物、类路径和入口哪一项缺失,再动配置,后续连接 Maven、Docker 时也沿用这张状态表。
把这个小实验放进课程项目的 docs/runtime-baseline.md,并约定 JDK 21、UTF-8、输出目录和清理命令。编译器版本是构建输入,class 文件是中间产物,JVM 进程是执行者,终端文字只是观察结果;这四者分开后,排查“依赖找不到”和“代码逻辑错”不会再混在一起。
还可以用 javap -classpath . HelloRoute 对照三个层次:源文件里有语句,class 文件里有常量和方法描述,JVM 执行时才创建 args 数组和局部变量。看到 class 文件存在,只能说明编译器完成了工作;看到输出,只能说明入口和当前运行时完成了工作,不能推出数据库、网络或配置也正确。
把第一课的命令放进后续项目的 docs/runtime-baseline.md:指定 JDK 主版本、默认编码、工作目录、清理命令和预期返回码。以后换 Maven 时,mvn test 会替你调用编译器;换 Docker 时,镜像里的 JRE 会替你启动 JVM,但每次仍要能回答“实际用了哪一个 JDK、执行的是哪一个 class/JAR、结果如何验证”。
最后把验证分为三条命令:javac --release 21 验证源码与语言基线,java -cp . HelloRoute 验证类路径与入口,$LASTEXITCODE 验证进程结果。三条都成功,才可以把实验标成可复现;少一条就保留失败输出和当前目录,避免下次从记忆重建现场。
验证清单:在同一个干净目录分别执行编译、运行、清理和错误运行四组命令,记录当前目录、JDK 版本、生成的 class、标准输出和退出码;再把工作目录切到父目录,确认类路径错误与编译错误不是同一种问题。使用 Java 21 编译基线,若本机有 JDK 25,只记录它作为独立实验,不把 25 专属语法放进主示例。复盘时能从一条错误输出回答“工具、源码、产物、类路径、入口”哪一层先偏离,才算完成环境验证。
进阶附录:查看字节码与返回码
执行 javap -c -v HelloRoute.class 可以看到常量池、方法表和字节码指令。Windows PowerShell 中可用 $LASTEXITCODE 查看上一条 Java 进程的返回码;在 CI 里,返回码比“日志看起来像成功”更值得信任。不要把 javap 输出当作必须背下来的汇编,它的价值是帮助你验证“编译器到底生成了什么”。
本课按「JDK 21 环境、编译器与运行时基础」的学习范围组织,正文与示例均为本站原创整理。