← 返回 Java 后端知识路线
阶段 01Java 基础

环境与编译运行:先把一行 Java 跑起来

从 JDK、javac 到 JVM 的最小闭环,建立看懂错误信息和复现实验的第一套方法。

第 01 / 35 篇
JDK 21javacJVM命令行

先看这一课值不值得学

学完后,你手里多了哪些代码积木

先建立源码、编译、字节码、运行四步闭环。

本课正式新增

语法 / API / 命令你必须会到什么程度
public class 类名 { ... }声明一个可被 JVM 找到的类
public static void main(String[] args)程序入口;args 接收命令行参数
System.out.println(...) 向控制台输出一行结果

本课只借用,先别硬背

  • String 暂时只当作文字类型使用
  • 数组先认识 String[] 的外形,第 4 课再系统学习

学完必须能独立写

  • 手写并运行一个单文件 Java 程序
  • 根据编译错误定位类名、文件名和入口方法问题
本课目录
  1. 1. 现实问题:为什么“安装好了”仍然跑不起来
  2. 2. 最小可运行示例:一个文件、一个入口、两条命令
  3. 3. 调用链与对象变化
  4. 4. 为什么这样设计
  5. 5. 项目落点:把实验变成可复现的启动说明
  6. 6. 易错排查
  7. 7. 一页复习

1. 现实问题:为什么“安装好了”仍然跑不起来

刚开始学 Java 时,最容易把“IDEA 里点运行成功”和“真正知道程序怎样运行”混为一谈。换一台电脑、换一个目录,或者把 javajavac 指到了不同版本,程序就可能报错。工程学习需要一个最小闭环:能看到源文件,能明确调用哪一个编译器,能确认生成了什么,再让同一套运行时启动它。

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.exejava.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 javawhere.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-Locationjava --versionjavac --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 环境、编译器与运行时基础」的学习范围组织,正文与示例均为本站原创整理。