读完这篇,你能做什么
能从空目录创建 Hello.java,编译运行并定位常见路径与版本错误。
学习理由、准备要求与知识点
为什么学它
后续代码都需要先能编译和运行;从第一个程序开始,就要走完编写源文件、编译、运行和检查输出的完整过程。
先读这些内容
这篇可以直接开始,不需要先读其他笔记。
知识点清单
- 源代码与字节码
- JDK
/JRE/JVM职责 - JDK 17 安装与架构选择
- PATH 与命令解析
- java
/javac版本核验 - 文件扩展名与 UTF-8
- main 与类名约定
- 编译、运行及输出证据
运行前先确认本篇与 Java SE 基础示例使用 JDK 17;具体框架项目按其版本要求选择 JDK。源码定位在本机 Microsoft OpenJDK 17.0.20.1 的 src.zip 核验,并参考上游 jdk-17.0.20-ga 标签。高级模块可先跳读。
01先弄懂它是什么
运行 Java 程序要经过三个动作:写源代码,用 javac 检查并编译成 class 字节码,再用 java 找到入口并运行。JVM 负责执行字节码,JRE 指运行需要的环境,JDK 还包含编译和诊断工具。初学写程序安装 JDK 即可,不必按这三个缩写分别安装三套软件。发行版的目录安排可能不同,不能只凭有没有 jre 文件夹判断是否装对。
按文末安装指南选择适合本机系统和处理器的 JDK 17,安装前先看看是否已有可用版本。之后打开新的 PowerShell,运行 java -version 和 javac -version,两者都应是 17;再用 Get-Command java,javac 核对实际路径。
PATH 决定终端先找到哪个程序,JAVA_HOME 是许多构建工具使用的目录约定。改了环境变量通常要新开终端,安装目录存在也不代表当前命令正在使用它。
第二步新建 C:\java-study\hello 文件夹,在文件管理器打开“文件扩展名”,创建 UTF-8 文本文件 Hello.java,避免保存成 Hello.java.txt。
完整内容为 public class Hello { public static void main(String[] args) { System.out.println("Hello, Java!"); } }。
初读只需记住外层类名 Hello 对应文件名,main 是本例入口,println 把文字写到终端;暂时不用背完 public、static、数组的全部语义。进入该目录后执行 javac -encoding UTF-8 Hello.java,再执行 java -cp . Hello,预期终端显示 Hello, Java!。
运行命令写类名而不是 Hello.class;点表示从当前目录找类。请分别记录两条命令、退出状态和输出,因为出现 class 文件只证明编译这一阶段成功。
文中这几个词,先认一认
- 工具链
- 从源文件到运行结果所用的一组工具,以及它们的版本、参数和实际路径。
- 字节码
- 编译后保存在 class 文件中的 JVM 指令和类型信息,需要由相应运行环境执行。
- 类路径
- 编译或运行时寻找应用类的目录或文件集合;本例用当前目录。
- 入口
- 程序从哪里开始执行;本篇 Java 17 类启动示例使用 public static
voidmain(String[] args)。
02看看它怎么工作
可以把失败定位成四道关口。第一道是操作系统能否找到 java 和 javac;第二道是编译器能否读到正确文件;第三道是源代码是否符合语言与类型规则;第四道是启动器能否加载类并找到入口。命令不存在时修改 Java 代码没有帮助;找不到源文件时应检查当前目录和扩展名;编译报错时从第一条诊断的位置开始看;编译成功而找不到主类时应检查类名、包名和类路径。先确定失败在哪一层,排错会比反复重新安装更快。
javac 不只是把字符机械替换成数字。它会解析语法、检查名称和类型并产生 class 文件;文件中记录版本信息、方法签名及执行代码。java 启动器依据启动模式解析参数,然后加载入口类。传统类启动模式下 main 的参数类型、可见性、静态属性和返回类型都有要求;名字看起来相同而签名不同仍不是有效入口。JDK 17 也支持单文件源代码启动,但本篇先显式分开编译与运行,方便理解中间产物和错误边界。
源文件、编译器和运行时版本是三个独立事实。用较新 JDK 编译可能产生旧运行时不接受的字节码;--release 17 同时约束目标语言、目标字节码及对应公开 API,而单独修改目标字节码版本不能保证 API 可用。这里先让两条命令都来自 JDK 17,后续再引入多工具链。流程图描述教学主路径,不表示所有启动模式的完整内部调用图,也不意味着编译器检查能够发现所有运行时错误。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["创建 UTF-8 的 Hello.java"] --> B["javac 检查并编译"]
B --> C["生成 Hello.class"]
C --> D["java 依据类路径加载类"]
D --> E["校验传统 main 入口"]
E --> F["执行并观察输出"]
B --> G["编译诊断:文件、语法或类型"]
D --> H["加载诊断:类名与类路径"]按顺序看,每一步做了什么
- 核验 java 与 javac 的版本和实际路径。
- 创建与公开类同名的 UTF-8 源文件。
- 进入正确目录,编译并读取第一条错误诊断。
- 确认 class 生成后以类名启动。
- 把终端输出与预期比较,区分编译通过和运行正确。
03对照实现代码
本机 Microsoft OpenJDK 17.0.20.1:java.base/sun/launcher/LauncherHelper.java 第 864–896 行;链接为上游 jdk-17.0.20-ga。 · sun.launcher.LauncherHelper#validateMainClass(Class<?>)
以下是原创、可独立编译的 Java 17 教学模型,不是复制的 JDK 源码。只模拟公开 main 的签名检查;省略本机启动器、模块、JavaFX、诊断国际化与实际执行。
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
public final class EntryRuleModel {
public static boolean accepts(Class<?> candidate) {
try {
// getMethod 只查找公开方法,并使用准确的参数类型。
Method entry = candidate.getMethod("main", String[].class);
// 入口属于类本身,且不向启动器返回业务值。
return Modifier.isStatic(entry.getModifiers())
&& entry.getReturnType() == void.class;
} catch (NoSuchMethodException missingEntry) {
return false;
}
}
}真实方法先以 main 和 String[] 参数类型查找公开方法,因此 public static void main(String text) 不符合规则。数组参数不是随意的装饰,而是用于接收启动命令后面的多个参数。教学模型把查找失败折叠为 false,真实启动器会组织面向用户的错误诊断;二者不能混为同一种异常处理策略。
找到公开方法后仍需检查静态属性。普通对象方法需要接收者实例,启动器不会为了这个约定先猜你该调用哪个构造器。因此 public void main(String[] args) 即使内容正确也不能作为这里的传统入口。这里用反射解释运行器规则,不要求初学者马上掌握反射 API,也没有尝试绕过访问控制。
返回类型必须是 void。程序向终端打印内容与 Java 方法返回一个值是两件不同的事;println 的显示内容不是 main 的返回值。进程退出状态还有自己的约定,不能把返回 int 的 main 套用到 Java 17 的传统入口规则上。源代码能编译成普通方法,也不代表它符合启动入口的约定。
这段源码说明启动器怎样检查入口,不表示整个启动过程只有这些步骤;还包括类加载、初始化和实际执行。本机供应商补丁版与上游标签也未必逐行相同。文章用能定位的方法把概念接到真实实现,教学模型只保留需要逐句理解的检查,不代替完整启动器。
代码先调用谁,再调用谁
- java 启动器解析启动目标
LauncherHelper.checkAndLoadMain加载目标类LauncherHelper.validateMainClass检查传统入口- 启动流程调用目标 main,执行应用代码
04动手写一个例子
用有限状态机检查学习流水线的阶段证据
输入已观察到的阶段事件序列,要求依次为 JDK_OK、SOURCE_OK、COMPILE_OK、RUN_OK。返回下一步、成功或第一处顺序错误;FAIL 表示当前阶段失败。它只检查事件顺序,不会安装 JDK,也不证明日志内容真实。
- 输入 1
[JDK_OK, SOURCE_OK, COMPILE_OK, RUN_OK]- 输出 1
READY
- 输入 2
[]- 输出 2
WAIT_JDK_OK
- 输入 3
[COMPILE_OK]- 输出 3
ORDER_ERROR_AT_1
任务不需要保存已经通过的全部事件,只需保存下一项应该出现的阶段下标。初始下标为零,每遇到一个符合预期的事件就增加一;第一次不符合时立即返回位置。这个变量就是有限状态机的状态,它把一串命令历史转成明确的学习进度,避免把“看见过运行命令”误当作前面阶段都已完成。
处理每个事件时先判断流程是否已经结束,再判断失败标记,然后比较当前预期事件。检查次序是接口约定的一部分:完整四阶段后再来任何事件均属于多余事件,而不是重启流程。空数组有清晰意义,表示还没有证据;null 则是调用错误,要显式拒绝,不能悄悄当成空数组。
算法返回第一处问题,使调用者能够按顺序修复,然后重新评估整个序列。这里的阶段总数固定为四,即使传入很长数组也会在首个多余事件停止;复杂度写成关于已检查事件数的线性上界,同时说明固定阶段限制。实际日志系统还需要验证事件来源、执行编号与时间,本例有意只研究顺序校验。
为什么这样写是对的每次迭代开始时,next 前面的阶段都已按顺序且恰好一次验证,next 指向唯一允许的下一阶段。
每个被检查事件进行固定次数比较;只维护一个下标和固定阶段表,不随输入长度保存历史。若阶段表改为外部 m 项配置,表存储为 O(m),扫描仍为 O(n)。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.Objects;
public class Main {
private static final String[] STAGES = {
"JDK_OK", "SOURCE_OK", "COMPILE_OK", "RUN_OK"
};
static String inspect(String[] events) {
if (events == null) throw new IllegalArgumentException("events");
// next 指向尚未完成的第一个阶段。
int next = 0;
for (int i = 0; i < events.length; i++) {
// 完成后拒绝多余事件,不能再次推进状态。
if (next == STAGES.length) return "EXTRA_EVENT";
if ("FAIL".equals(events[i])) return "FAIL_AT_" + (next + 1);
if (!STAGES[next].equals(events[i]))
return "ORDER_ERROR_AT_" + (i + 1);
// 只有预期事件匹配后,状态才向前移动。
next++;
}
return next == STAGES.length ? "READY" : "WAIT_" + STAGES[next];
}
static void check(String expected, String... events) {
String actual = inspect(events);
if (!Objects.equals(expected, actual))
throw new AssertionError(expected + " != " + actual);
}
public static void main(String[] args) {
// 正常路径、空输入、乱序、失败及多余事件都必须验证。
check("READY", "JDK_OK", "SOURCE_OK", "COMPILE_OK", "RUN_OK");
check("WAIT_JDK_OK");
check("ORDER_ERROR_AT_1", "COMPILE_OK");
check("FAIL_AT_2", "JDK_OK", "FAIL");
check("EXTRA_EVENT", "JDK_OK", "SOURCE_OK", "COMPILE_OK",
"RUN_OK", "RUN_OK");
check("ORDER_ERROR_AT_1", (String) null);
// 参数错误与空数组是两个不同边界。
boolean rejected = false;
try { inspect(null); } catch (IllegalArgumentException e) { rejected = true; }
if (!rejected) throw new AssertionError("null must be rejected");
System.out.println("PASS 7");
}
}这些特殊情况也要试一试
- 空输入返回第一项待办。
- 输入
null抛参数异常,数组内null是顺序错误。 - 完整序列后的任何事件均属于多余。
- FAIL 只描述尚未完成的当前阶段。
进阶选读 · 可选深入:把线性阶段推广成依赖图
场景多模块构建中,某些编译任务可以并行,而测试必须等待相关产物。
做法用有向图记录依赖,通过入度与队列找出可运行任务,发现环时拒绝调度;缓存还需绑定源文件、编译参数和工具链摘要。
复杂度拓扑处理 O(V+E),状态及图存储 O(V+E)。
好处与代价依赖图提高并行度,却引入缓存失效和失败传播管理;第一遍学习只需完成 Hello 的编译运行。
05项目里怎么用
手工命令的价值是让输入输出足够透明,适合学习与复现小问题。真实项目会加入源目录、依赖、资源文件和测试,继续手写很长命令容易遗漏参数;此时使用构建工具记录过程。IDE 的运行按钮也有项目 JDK、模块类路径和工作目录等配置,终端成功并不自动证明 IDE 使用同样配置,反过来也一样。遇到差异,应比较实际工具链与启动参数,而不是猜测编辑器是否坏了。
团队需要把目标版本写进构建配置,并让持续集成执行相同的编译测试流程。--release 约束的是编译目标,不替代在目标运行时上的行为测试;本地看到字符串输出也不证明数据库、网络或部署环境工作正常。保存源代码版本、构建命令、JDK 版本和运行结果能让他人复现故障,截图只有部分信息,最好同时保留可复制的文本诊断。
安装与环境变量调整应当最小化。先查命令来源,再决定是否修改路径;不要为了一个项目随意删除其它 JDK,多个项目可能有不同要求。学习目录使用容易识别的名字,编译产物与源码的管理方式后续再迁移到标准项目结构。完成练习时应保留实际命令和输出,按证据判断自己卡在哪一阶段,再继续学习下一项内容。
容易踩的坑
- 把
Hello.java保存成隐藏扩展名的Hello.java.txt。 - 以 java
Hello.class启动,混淆类名与文件名。 - 只设置
JAVA_HOME,却没有核验终端实际找到的 java。 - 把编译成功、进程启动和业务结果正确当作同一项证据。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 手工 javac 与 java | 路径、参数和中间产物清晰,排错容易复现。 | 项目变大后命令和依赖难维护。 | 入门、最小复现和编译机制实验。 |
| IDE 运行配置 | 编辑、跳转、断点和输出集中。 | 隐含配置较多,需核对项目与运行器 JDK。 | 日常开发与交互调试。 |
| 构建工具与持续集成 | 统一版本、依赖和检查流程。 | 需要理解配置及插件生命周期。 | 团队项目与可重复交付。 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01JDK、JRE、JVM 为什么不是三个都必须分别安装?
展开参考答案与推导
它们描述开发工具、运行环境与虚拟机的不同职责。JDK 通常已包含运行所需能力;现代发行版是否单独提供 JRE 与目录布局有关,学习开发安装合适的 JDK 并核验 java、javac 即可。
02生成 Hello.class 是否证明 Hello 已运行?
展开参考答案与推导
没有。它说明编译器产生了字节码,随后仍可能发生类路径、入口或运行时错误。必须实际执行启动命令并比较输出,才能确认这个小程序在该环境的运行结果。
03为什么传统入口不能只写 static void main(String[] args)?
展开参考答案与推导
缺少 public 的方法不是公开入口,真实检查通过 getMethod 查找公开方法。源文件可能可以编译,因为它仍是合法的普通方法,但启动器需要更严格的入口约定。
换个情况,还会处理吗
01javac 显示 17,java 显示另一个版本,先做什么?
展开参考答案与推导
用 Get-Command java,javac 查看两条命令来源,并比较 PATH 的查找顺序。先确定是否来自不同安装,再调整当前项目工具链或路径;不要直接删除其它 JDK,因为其它项目可能需要它。
02报错找不到 Hello.java,应该先改 main 吗?
展开参考答案与推导
先检查当前目录、文件名和真实扩展名;这是输入文件定位问题,编译器还没有读到 main。确认文件位置后再按诊断处理源代码,分层检查能减少无效修改。
03IDE 能运行,终端找不到主类,可能差在哪?
展开参考答案与推导
IDE 可能使用专门的编译输出目录和完整类名,终端却在源码目录启动。检查编译产物位置、包声明和 -cp 参数,同时对比运行 JDK;不能仅凭 IDE 成功推断终端配置相同。
写代码试一试
01给 Hello 加一个名字参数,无参数时显示 guest。
- 输入 / 条件
java -cp . Hello Qinghuan- 输出 / 预期
Hello, Qinghuan!
提示 先检查 args.length,再访问第零项。
展开参考答案与推导
在 main 内先写 String name = args.length == 0 ? "guest" : args[0];,再拼接输出。关键是先判断长度,空数组访问 args[0] 会抛异常;命令行参数是运行阶段传入的字符串。
02把流水线算法扩成失败后能说明下一步名称。
- 输入 / 条件
[JDK_OK, FAIL]- 输出 / 预期
FAIL_AT_SOURCE_OK
提示 失败位置对应 STAGES[next]。
展开参考答案与推导
在 FAIL 分支返回 "FAIL_AT_" + STAGES[next],保留结束检查在它之前。同步更新失败测试,同时保留成功、空输入和多余事件测试,避免修改消息时改变原有阶段语义。
03添加一个重复阶段的测试并解释结果。
- 输入 / 条件
[JDK_OK, JDK_OK]- 输出 / 预期
ORDER_ERROR_AT_2
提示 第二项应该是 SOURCE_OK。
展开参考答案与推导
调用 check("ORDER_ERROR_AT_2", "JDK_OK", "JDK_OK")。第一项使 next 前进到一,第二项与 SOURCE_OK 不符,因此在位置二停止;这个算法规定恰好一次,不把重复事件自动去重。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能从空目录创建 Hello.java,编译运行并定位常见路径与版本错误。
本篇重点加练
重点重复核验命令入口与实际运行产物,建立最初的可复现执行证据习惯。
- PATH 与命令解析
- 编译、运行及输出证据
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
画出
Hello.java被编译并运行的文件与工具流程,分别说明编辑器、编译器和虚拟机在其中做什么;说明看到 .class 文件能证明到哪个阶段。- 提交内容
- 三阶段流程图和产物说明。
- 怎样算完成
- 源代码与字节码区分
- 工具职责不互相代替
练到的知识点源代码与字节码 · JDK/JRE/JVM 职责
- 作业 02排错实验
读取操作系统架构,安装或复用匹配架构的 JDK 17;在终端记录 java、javac 实际路径与版本。若存在多个版本,给出本次练习实际使用哪一个的证据,不删除已有安装。
- 提交内容
- 架构与路径记录、版本输出及使用说明。
- 怎样算完成
- 安装来源与架构明确
- 命令解析结果可核验
练到的知识点JDK 17 安装与架构选择 · PATH 与命令解析 · java/javac 版本核验
- 作业 03编程实践
创建输出两行中文学习记录的
HelloStudy.java,使用 UTF-8 保存,从终端编译并运行;自行检查文件真实扩展名、类名以及输出行数。- 提交内容
- 源文件、自写检查清单、完整编译运行命令及输出证据。
- 怎样算完成
- 可在约定目录复现
- 中文和行数符合自己先写的合同
- 编译成功与运行成功分别记录
练到的知识点文件扩展名与 UTF-8 · main 与类名约定 · 编译、运行及输出证据
- 作业 04原理问答
终端提示“找不到命令”和“找不到主类”时分别已经到达哪一步?使用你已成功运行的 Hello 程序写出每种故障所需的最少证据。
- 提交内容
- 两条执行路径和证据表。
- 怎样算完成
- 命令解析与程序加载分别定位
- 不要求修改系统级环境
练到的知识点PATH 与命令解析
- 作业 05编程实践
创建第二个只有 main 与两条输出语句的程序,让两条输出明确标记程序名称和版本;保存后从新开的终端重复编译运行,不依赖 IDE。
- 提交内容
- Java 文件、自写输出核对表和两次独立运行证据。
- 怎样算完成
- 文件名类名和命令对应
- 输出可辨认实际运行版本
练到的知识点编译、运行及输出证据
综合应用
- 作业 06排错实验
复制自己的练习文件,分别制造一次类名与文件名不一致、一次缺少 main 入口、一次在错误目录运行;每次只引入一种故障并恢复。
- 提交内容
- 三组故障文本、所在阶段和恢复记录。
- 怎样算完成
- 不靠反复重装处理代码错误
- 错误阶段定位准确
练到的知识点文件扩展名与 UTF-8 · main 与类名约定 · 编译、运行及输出证据
- 作业 07面试场景
同学说“电脑能执行 java -version,所以一定能编译我的程序”。给出还需要检查的最少事实,并设计一个不足十行的验证文件。
- 提交内容
- 检查步骤和验证文件。
- 怎样算完成
- 区分运行环境与编译入口
- 每一步都有明确成功证据
练到的知识点源代码与字节码 · JDK/JRE/JVM 职责 · PATH 与命令解析 · java/javac 版本核验
- 作业 08排错实验
复制自己的 Hello 程序到单独练习目录,先修改输出文字但暂不编译,再运行已有产物,随后编译运行;记录每一步目录、命令和观察值。
- 提交内容
- 源文件版本记录、命令与三次观察表。
- 怎样算完成
- 源码变化和运行产物能够对应
- 无需循环、集合或自定义方法
练到的知识点PATH 与命令解析 · 编译、运行及输出证据
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
完成方法与参数基础后:编写一页“新电脑复现此程序”的运行说明,加入 JDK 版本不匹配、命令找不到和找不到主类三类分支,不要求创建复杂项目。
- 提交内容
- 可交给同学执行的复现文档及一次独立复现记录。
- 怎样算完成
- 先验条件明确
- 失败分支不破坏其他环境
练到的知识点源代码与字节码 · java/javac 版本核验 · 编译、运行及输出证据
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 反例构造
有人只给出一张打印“成功”的终端截图,声称指定源码已在指定 JDK 下运行。列出该证据尚不能排除的具体情况,并设计最少补充材料;只用本篇已学命令。
- 提交内容
- 证据缺口与补充验证步骤。
- 怎样算完成
- 每项疑点对应可执行检查
- 不以增加复杂工具替代明确证据
练到的知识点PATH 与命令解析 · 编译、运行及输出证据
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读教程英文
Dev.java:第一个 Java 程序 (新标签页)↗
阅读编译执行流程与首个程序;页面新语法随版本演进,本文固定使用 Java 17 传统 main。
- 实践实践指南英文
Microsoft OpenJDK:安装指南 (新标签页)↗
按操作系统选择 JDK 安装方式,检查 PATH 与
JAVA_HOME;为本路线选择 17 版本。 - 深入参考文档英文
Java 17:javac 命令 (新标签页)↗
查阅 -encoding、-d、--release 和 classpath,用于解释编译器实际输入与输出。
- 深入参考文档英文
Java 17:java 启动器 (新标签页)↗
阅读类名启动、-cp 和源文件启动模式,区分源文件、字节码和 JVM 参数。
- 实践实践指南中文
计算机基础能力手册:先把电脑和开发工具用明白 (新标签页)↗
先读“初步使用计算机”和“搜索和信息获取”。小练习:建一个练习目录,写出文件的完整路径,并用搜索找到一个软件的官方安装说明。已会的硬件与购机章节可跳过。 网页版可公开阅读,站点提示转换可能有疏漏。原书仓库可核对;这是作者与学生社团相关的手册,不是北京大学学位课程。
- 深入参考文档英文
OpenJDK 17u LauncherHelper (新标签页)↗
定位
validateMainClass,阅读公开方法查找与静态、返回类型检查。