qinghuan
← Java SE / 全部笔记

单元 01 / Java SE

P0 · 必会主线L0 · 零基础

从零开始:安装 JDK 并运行第一个 Java 程序

安装并核对 JDK 17,创建源文件,分别完成编译和运行,用终端输出确认每一步成功。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能从空目录创建 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 void main(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["加载诊断:类名与类路径"]

按顺序看,每一步做了什么

  1. 核验 java 与 javac 的版本和实际路径。
  2. 创建与公开类同名的 UTF-8 源文件。
  3. 进入正确目录,编译并读取第一条错误诊断。
  4. 确认 class 生成后以类名启动。
  5. 把终端输出与预期比较,区分编译通过和运行正确。

03对照实现代码

Java 启动器如何确认传统 main 的签名 (新标签页)↗

本机 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、诊断国际化与实际执行。

核心机制代码 · java
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 的传统入口规则上。源代码能编译成普通方法,也不代表它符合启动入口的约定。

这段源码说明启动器怎样检查入口,不表示整个启动过程只有这些步骤;还包括类加载、初始化和实际执行。本机供应商补丁版与上游标签也未必逐行相同。文章用能定位的方法把概念接到真实实现,教学模型只保留需要逐句理解的检查,不代替完整启动器。

代码先调用谁,再调用谁

  1. java 启动器解析启动目标
  2. LauncherHelper.checkAndLoadMain 加载目标类
  3. LauncherHelper.validateMainClass 检查传统入口
  4. 启动流程调用目标 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 指向唯一允许的下一阶段。

时间 O(n) 上界;本题固定四阶段时最多检查五个事件。额外空间 O(1) 额外空间。

每个被检查事件进行固定次数比较;只维护一个下标和固定阶段表,不随输入长度保存历史。若阶段表改为外部 m 项配置,表存储为 O(m),扫描仍为 O(n)。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 45 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
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 与命令解析
  • 编译、运行及输出证据

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    画出 Hello.java 被编译并运行的文件与工具流程,分别说明编辑器、编译器和虚拟机在其中做什么;说明看到 .class 文件能证明到哪个阶段。

    提交内容
    三阶段流程图和产物说明。
    怎样算完成
    • 源代码与字节码区分
    • 工具职责不互相代替

    练到的知识点源代码与字节码 · JDK/JRE/JVM 职责

  2. 作业 02排错实验

    读取操作系统架构,安装或复用匹配架构的 JDK 17;在终端记录 java、javac 实际路径与版本。若存在多个版本,给出本次练习实际使用哪一个的证据,不删除已有安装。

    提交内容
    架构与路径记录、版本输出及使用说明。
    怎样算完成
    • 安装来源与架构明确
    • 命令解析结果可核验

    练到的知识点JDK 17 安装与架构选择 · PATH 与命令解析 · java/javac 版本核验

  3. 作业 03编程实践

    创建输出两行中文学习记录的 HelloStudy.java,使用 UTF-8 保存,从终端编译并运行;自行检查文件真实扩展名、类名以及输出行数。

    提交内容
    源文件、自写检查清单、完整编译运行命令及输出证据。
    怎样算完成
    • 可在约定目录复现
    • 中文和行数符合自己先写的合同
    • 编译成功与运行成功分别记录

    练到的知识点文件扩展名与 UTF-8 · main 与类名约定 · 编译、运行及输出证据

  4. 作业 04原理问答

    终端提示“找不到命令”和“找不到主类”时分别已经到达哪一步?使用你已成功运行的 Hello 程序写出每种故障所需的最少证据。

    提交内容
    两条执行路径和证据表。
    怎样算完成
    • 命令解析与程序加载分别定位
    • 不要求修改系统级环境

    练到的知识点PATH 与命令解析

  5. 作业 05编程实践

    创建第二个只有 main 与两条输出语句的程序,让两条输出明确标记程序名称和版本;保存后从新开的终端重复编译运行,不依赖 IDE。

    提交内容
    Java 文件、自写输出核对表和两次独立运行证据。
    怎样算完成
    • 文件名类名和命令对应
    • 输出可辨认实际运行版本

    练到的知识点编译、运行及输出证据

综合应用

  1. 作业 06排错实验

    复制自己的练习文件,分别制造一次类名与文件名不一致、一次缺少 main 入口、一次在错误目录运行;每次只引入一种故障并恢复。

    提交内容
    三组故障文本、所在阶段和恢复记录。
    怎样算完成
    • 不靠反复重装处理代码错误
    • 错误阶段定位准确

    练到的知识点文件扩展名与 UTF-8 · main 与类名约定 · 编译、运行及输出证据

  2. 作业 07面试场景

    同学说“电脑能执行 java -version,所以一定能编译我的程序”。给出还需要检查的最少事实,并设计一个不足十行的验证文件。

    提交内容
    检查步骤和验证文件。
    怎样算完成
    • 区分运行环境与编译入口
    • 每一步都有明确成功证据

    练到的知识点源代码与字节码 · JDK/JRE/JVM 职责 · PATH 与命令解析 · java/javac 版本核验

  3. 作业 08排错实验

    复制自己的 Hello 程序到单独练习目录,先修改输出文字但暂不编译,再运行已有产物,随后编译运行;记录每一步目录、命令和观察值。

    提交内容
    源文件版本记录、命令与三次观察表。
    怎样算完成
    • 源码变化和运行产物能够对应
    • 无需循环、集合或自定义方法

    练到的知识点PATH 与命令解析 · 编译、运行及输出证据

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    完成方法与参数基础后:编写一页“新电脑复现此程序”的运行说明,加入 JDK 版本不匹配、命令找不到和找不到主类三类分支,不要求创建复杂项目。

    提交内容
    可交给同学执行的复现文档及一次独立复现记录。
    怎样算完成
    • 先验条件明确
    • 失败分支不破坏其他环境

    练到的知识点源代码与字节码 · java/javac 版本核验 · 编译、运行及输出证据

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 反例构造

    有人只给出一张打印“成功”的终端截图,声称指定源码已在指定 JDK 下运行。列出该证据尚不能排除的具体情况,并设计最少补充材料;只用本篇已学命令。

    提交内容
    证据缺口与补充验证步骤。
    怎样算完成
    • 每项疑点对应可执行检查
    • 不以增加复杂工具替代明确证据

    练到的知识点PATH 与命令解析 · 编译、运行及输出证据

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗