Java 环境搭建全攻略:JDK 安装、环境变量与第一个程序

bee2026-10-0835 分钟0 次阅读
JDK / JRE / JVM 到底是什么关系?三平台安装实操、JAVA_HOME 与 PATH 的真相、多版本共存管理,以及 90% 新手都会踩的 6 个坑。
1 / 114
小节
〇、30 秒看懂
2 / 114

写 Java 的第一步不是写代码,而是把「翻译官 + 执行引擎 + 查找目录」三样东西摆好位置。你写的 .java 文件人类看得懂、电脑看不懂,所以要一个编译器(javac)把它翻译成字节码(.class);字节码哪台装了 JVM 的电脑都能跑,所以要一个虚拟机(java)当场执行它;而这两样命令默认都不在你的终端能直接看到的地方,所以要用两个环境变量告诉它们「装在哪」。整篇文章讲的就是这三件事:装什么、怎么让系统找到、找到了之后一条命令里发生了什么。

3 / 114
类比

把 JDK 想成一座图书馆。JAVA_HOME 是「这座馆在城里哪条街」登记在市政系统里的地址——Maven、Gradle、IDEA 这些「送货员」都按这条地址来取书;PATH 是「借书窗口开在几楼」——你自己走进大门(敲 java)时,前台按这个指引把你带到窗口。地址写成「街区」还是写成「窗口所在的房间」,就是新手最常见的翻车点。

4 / 114
架构图
图 · 本篇地图:环境这件事到底要装几样东西
图 · 本篇地图:环境这件事到底要装几样东西
5 / 114

学完这一篇,你应该能回答三个问题:

6 / 114
  • 我到底装的是 JDK 还是 JRE?为什么现在没人单独装 JRE 了?
  • JAVA_HOME 和 PATH 分别给谁看?写错了各会报什么错?
  • java Hello 这七个字符敲下去,机器里依次发生了哪五件事?
7 / 114
小节
一、先搞清楚你在装什么:JDK / JRE / JVM
8 / 114

几乎每个初学者都在这一步懵过:网上教程一会儿让你装 JDK,一会儿说 JRE,打开任务管理器又冒出来一个 JVM。这三者不是三个平行的东西,而是一层套一层的俄罗斯套娃。

9 / 114
架构图
图 1 · JDK / JRE / JVM 体系结构
图 1 · JDK / JRE / JVM 体系结构
10 / 114
对照表
名词全称一句话解释里面有什么
JVMJava Virtual Machine真正执行程序的「虚拟电脑」解释器、JIT、GC、内存模型
JREJava Runtime Environment运行 Java 程序的最小集合JVM + 核心类库
JDKJava Development Kit开发 Java 程序的完整工具包JRE + javac / jar / javadoc 等工具
11 / 114
说明

只运行别人做好的程序,装 JRE 就够;只要写哪怕一行 Java 代码,就必须装 JDK。现代 JDK 安装包已经内置 JRE,所以别再单独去找 JRE 下载页了。

12 / 114
小节
1.1 为什么版本要选 17 或 21
13 / 114

Java 每半年发一个版本,但不是每个版本都值得用。带 LTS 字样的才进生产环境——这是行业铁律:

14 / 114
对照表
版本类型支持截止建议
Java 8LTS(上古)2030只用于维护老项目
Java 11LTS2026过渡期选择
Java 17LTS2029+Spring Boot 3 的最低要求
Java 21LTS2031+新项目首选,虚拟线程已转正
15 / 114
坑

Spring Boot 3.x 的硬性门槛是 JDK 17。用 JDK 8 打开 Spring Boot 3 项目,会在启动第一秒抛出 UnsupportedClassVersionError,而且错误信息又长又吓人。

16 / 114
小节
二、三平台安装实操
17 / 114
原理动画
动图 · 从零到跑通第一个程序
动图 · 从零到跑通第一个程序
18 / 114
小节
2.1 Windows:优先用包管理器
19 / 114

手动去 Oracle 官网下载虽然可行,但版本管理麻烦。推荐用 winget 或 Scoop,一条命令搞定:

20 / 114
powershell
# 方案一:winget(Win10 1809+ 自带)winget install EclipseAdoptium.Temurin.21.JDK# 方案二:Scoop(版本切换更灵活)scoop bucket add javascoop install temurin21-jdk
21 / 114

装完后不需要手动配置环境变量——包管理器会自动注册。手动解压安装包的方式则需要自己配,见第三节。

22 / 114
小节
2.2 macOS:brew 一条命令
23 / 114
bash
brew install --cask temurin@21# 让系统把 JAVA_HOME 指向它echo 'export JAVA_HOME=$(/usr/libexec/java_home -v 21)' >> ~/.zshrcsource ~/.zshrc
24 / 114

macOS 的 /usr/libexec/java_home 是系统自带的 JDK 定位工具,比手写路径聪明得多——装了多个 JDK 时它能按版本号挑选。

25 / 114
小节
2.3 Linux:包管理器 + SDKMAN
26 / 114
代码对照
代码bash
# Debian / Ubuntusudo apt update && sudo apt install -y openjdk-21-jdk# 想要更自由的版本切换,用 SDKMANcurl -s "https://get.sdkman.io" | bashsdk install java 21.0.2-temsdk use java 17.0.10-tem    # 临时切换到 17
解读

提示:服务器上安装完 JDK 后,java -version 显示的可能是系统预装的旧版本。原因是 PATH 里旧版本排在前面,用 which java 确认实际用的是哪一个。

27 / 114
小节
三、环境变量:JAVA_HOME 与 PATH 的真相
28 / 114

这是新手最容易抄错、也最该理解原理的一步。两个变量分工完全不同:

29 / 114
代码对照
代码bash
JAVA_HOME = D:\dev\jdk-21        # 告诉「工具」:JDK 装在哪儿PATH     += %JAVA_HOME%\bin      # 告诉「终端」:去哪找 java 命令
解读
  • JAVA_HOME 是给谁用的?Maven、Gradle、Tomcat、IDEA 这些工具会读它来定位 JDK。
  • PATH 是给谁用的?命令行 shell——你敲 java 时,它在 PATH 列出的目录里挨个找。
30 / 114
架构图
图 · 两条查找链:工具按 JAVA_HOME,终端按 PATH
图 · 两条查找链:工具按 JAVA_HOME,终端按 PATH
31 / 114

两套规则互不通气,所以「写错了谁报错」这件事背是背不住的。来玩一局配对:左边是配置项,右边点它的真实职责——配错了当场告诉你为什么。

32 / 114
配对闯关
闯关谁读它、写错了谁会报错已配对 0/6 · 配错 0
六组都是「配置项 ↔ 职责」的硬映射,别靠位置猜——两列都打乱了
先点左边一个
33 / 114
类比

JVM 找一个类,像在大学里找一本书先看总馆——先去总馆藏书库(JDK 自带的基础类库),那里有就不用去别的楼;总馆说「我没这本」,才轮到院系分馆(你 -cp 里列的 jar 和目录)去找。这套「先问上级、上级没有才自己找」的规矩叫双亲委派,它保证你在自己项目里写一个 String.java 也永远盖不掉 JDK 的那个 String。

34 / 114
坑

把 JAVA_HOME 写成 D:\dev\jdk-21\bin 是最经典的错误。工具会自动在后面拼 \bin\java,拼出来变成 bin\bin\java,于是 Maven 报「JAVA_HOME is set to an invalid directory」。

35 / 114

配置好之后,用一个命令验证——注意看输出的大写版本号:

36 / 114
代码对照
代码bash
$ java -versionopenjdk version "21.0.2" 2024-01-16 LTSOpenJDK Runtime Environment Temurin-21.0.2+13 (build 21.0.2+13-LTS)OpenJDK 64-Bit Server VM Temurin-21.0.2+13 (build 21.0.2+13-LTS, mixed mode)
解读
  • 三行输出分别对应:JDK 版本、运行环境、虚拟机实现
  • 64-Bit Server VM 表示 64 位服务端模式——生产标准配置
  • 如果显示 1.8.0_xxx,说明你的 PATH 仍然指向 Java 8

注意:java -version(一个横杠)与 java --version(两个横杠)都能用,但前者是历史遗留写法,后者才是规范的 GNU 风格。

37 / 114
小节
四、第一个程序:编译与运行的完整链路
38 / 114

新建文件夹,用任意文本编辑器写下这段代码(记得保存为 Hello.java,不要用记事本默认的 .txt):

39 / 114
java
public class Hello {    public static void main(String[] args) {        System.out.println("Hello, Java!");        // main 是 JVM 约定的入口:固定签名,一个字都不能改        for (int i = 0; i < args.length; i++) {            System.out.println("参数 " + (i + 1) + ": " + args[i]);        }    }}
40 / 114

然后打开终端,进入该文件夹,执行两条命令:

41 / 114
代码对照
代码bash
# 第一步:编译,把源码翻译成字节码javac Hello.java        # 生成 Hello.class# 第二步:运行,JVM 加载并执行java Hello 张三 李四     # 注意:没有 .class 后缀
解读
  • javac Hello.java:调用编译器,产出 Hello.class——一份平台无关的字节码
  • java Hello:启动 JVM,把 Hello.class 加载进来执行 main
  • 命令行参数 张三 李四 会装进 String[] args 数组,被循环打印出来
42 / 114

为什么要有 .class 这一步? 因为 Java 的跨平台靠的就是它:源码只编译一次,生成的字节码在任何装了 JVM 的系统上都能跑——「一次编写,到处运行」说的正是字节码。

43 / 114
警告

java Hello.class 是错的。给 java 命令的永远是类名(Hello),不是文件名。多打四个字符,JVM 就会报找不到类的错。

44 / 114

上面那五件事(找到命令 → 装上 JVM → 拼 classpath → 加载主类 → 调 main)读起来像顺口溜,但真正出事的是其中某一格。把它摊成一次单步执行:左边是要走的六行,右边同步刷新「此刻的变量」和「谁在调用谁」。连点下一步,重点盯第 4 步——第九节那句报错就诞生在那里:

45 / 114
单步调试台
单步台逐行走一遍:java Hello 这七个字符做了什么1 / 6
按下一步走六拍,第 4 步的 classpath 是怎么拼出来的,直接决定第 5 步能不能找到类
被调试的代码
1javac Hello.java # 终端沿 PATH 逐个目录找到 javac
2java Hello # 同一个规则,这次找到的是 java.exe
3// java.exe 只是引导程序:把 jvm.dll / libjvm.so 装进进程
4// 拼 classpath:-cp 优先,其次 CLASSPATH 环境变量,最后当前目录
5// 按全限定名找 Hello.class 并加载成 Class 对象
6// 反射调用 public static void main(String[])
此刻的变量
要找的命令javac
依据PATH 的目录顺序
产出Hello.class
调用栈
1shell → PATH → javac.exe
1这一步和 Java 没关系,是操作系统在找可执行文件:从左到右挨个目录试,第一个命中就用。所以装了三个 JDK 时,谁排在 PATH 前面谁就是「你的 javac」。
46 / 114
小节
五、多版本共存:别让老项目绑住手脚
47 / 114

真实工作中,你很可能上午维护 JDK 8 的老系统,下午开发 JDK 21 的新项目。永远不要卸载重装来切换版本,用工具管理:

48 / 114
对照表
工具平台核心命令适用场景
SDKMANmacOS / Linuxsdk use java 17.0.10-tem命令行爱好者
jenvmacOS / Linuxjenv global 21需要 per-project 配置
winget + 手改 PATHWindows环境变量界面拖动偶尔切换
IDEA 内置 JDK 下拉框全平台File → Project Structure只在 IDE 内切换
49 / 114

多版本共存这件事的坑不在「怎么装」,而在三方各按各的规矩挑:终端按 PATH 的顺序、工具按 JAVA_HOME、IDE 按项目 SDK。下面这段动画把三条线并排走了一遍,注意第 6 帧——三方不一致的那一刻,就是「IDE 能跑、命令行崩」诞生的那一刻:

50 / 114
原理动画
动图 · 装了三个 JDK,到底哪个在干活
动图 · 装了三个 JDK,到底哪个在干活
51 / 114

而「别让本机状态决定构建结果」这句话,落到工程上就是一段 pom。别去抄——勾一遍,看它长什么样,尤其注意 java.version 那一行和三处 scope:

52 / 114
生成器
生成器把 JDK 版本写进项目,而不是写在电脑上pom.xml2 / 5
上方 Java 版本切到 21,看它落进 <java.version>;再勾 MySQL / Lombok / Test,对照生成的三行 scope——runtime、provided、test 各是下一篇 Maven 第四节的主角
产物
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.4</version> <!-- 版本由 BOM 统管,子依赖不写 version -->
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>21</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
勾了这些,代价与理由在这里
parent继承 3.3.4 的 starter-parent 之后,所有 spring-boot-starter-* 都不用写版本号;一旦有人手写给某个 starter 加 version,就以那条为准——这是依赖版本漂移最常见的原因。
Web做接口就绕不开它: DispatcherServlet、内嵌 Tomcat、JSON 序列化全在这个 starter 里。
Testscope=test;@SpringBootTest、MockMvc、AssertJ 都在里面,漏了就找不到 @Test。
53 / 114
决策
决策团队项目里,你的 IDE 用 JDK 21,但 CI 流水线用的是 JDK 17,编写的代码在本地跑得好好的,提交后却编译失败。最应该做的第一步是什么?
54 / 114
小节
六、六个高频坑,提前绕开
55 / 114
对照表
报错 / 现象根因解决
javac 不是内部或外部命令PATH 没配或没生效重开终端;检查 PATH 是否包含 %JAVA_HOME%\bin
JAVA_HOME is set to an invalid directoryJAVA_HOME 多写了 \bin改为 JDK 根目录
错误: 找不到或无法加载主类 Hello类名与文件名不一致 / 运行了 .class改名一致;java Hello
UnsupportedClassVersionError用低版本 JDK 运行高版本编译的 class统一升级 JDK 到 17+
编码 GBK 的不可映射字符源码含中文且编译未指定编码javac -encoding UTF-8 Hello.java
IDEA 里能跑、终端里不能跑两者用的不是同一个 JDK终端 which java 对比 IDEA 设置
56 / 114
小节
七、动手实验:把「看不见的那一层」调出来看
57 / 114

光看文字记不住环境这件事,因为它是看不见的路径查找。下面四个内核实验在浏览器里真实执行 JDK 的那几层,每个都可以自己切参数。

58 / 114

第一个实验把「一行代码的一生」拆开。字节码是 javac 产出的中间语言,类加载器是把 .class 文件读进内存的那个角色,JIT(即时编译)是 JVM 跑热了之后把热点字节码翻译成机器码的加速器。切到「JIT 分层编译」你会看到同一段循环先被解释执行、再被编译成机器码——这就是「Java 开头慢、跑久了变快」的原因:

59 / 114
内核实验
TeaVM一行 Hello.java 的旅程:编译期 → 类加载 → 解释 → JIT未启动
依次点四个按钮;切到 JIT 时盯住执行速度的变化
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
60 / 114

第二个实验直接回答第三节那个类比:为什么你写的 String.java 盖不掉 JDK 的 String。选「双亲委派」看请求如何一路往上问;选「jar 包冲突」看两个 jar 里有同一个类时谁赢——答案是 classpath 里排前面的那个,这也正是第九节速查表里 NoClassDefFoundError 那一行的根因:

61 / 114
内核实验
TeaVM找书先看总馆:classpath 与双亲委派未启动
切到「jar 包冲突」,看重复类为什么只有一份生效
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
62 / 114

同一套机制反过来用,就是新手最崩溃的那句报错。切到「ClassNotFoundException」,你能亲眼看到 JVM 把 classpath 上每个目录都翻了一遍、最后没找到:

63 / 114
内核实验
TeaVM主类到底去哪找了:一次失败的查找未启动
对照第六节和第九节的「找不到或无法加载主类」,看它翻了哪几个目录
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
64 / 114

第三个实验解释下一篇会大量遇到的现象——IDEA 点 Run 之前偷偷帮你做了一件事。「编译产物」那一步能看到 .java 变成 target/classes 里的 .class;不先编译就运行,用的还是旧字节码:

65 / 114
内核实验
TeaVMIDEA 从保存到运行:改稿后要重新印刷未启动
先点「编译产物」,再点「启动应用」,顺序反了就明白什么叫跑了个旧的
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
66 / 114

第四个实验专答第三节那句「到底哪个 JDK 在干活」。切到「谁说了算」,你会看到终端沿 PATH 逐个目录试、第一个命中就用;切到「JAVA_HOME 与 PATH」,两个变量各自的读者就浮出水面了;「装了好几个版本」这一档最真实——8、17、21 三份共存时,工具、终端、IDE 三方各挑各的:

67 / 114
内核实验
TeaVM装了三个 JDK,哪个在干活未启动
按 which → home → multi 的顺序切;最后一档把 8 / 17 / 21 三份摆在一起,看谁赢
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
68 / 114
内核实验
TeaVM验证与修复:改完怎么确认真的生效未启动
这一档给出可执行的自查顺序,对应对照第五节决策卡里那句「版本要写进项目」
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
69 / 114

上一条链的执行顺序,光看图记不住,得点。下面这张图把「找书先看总馆」拆成五格,一格一格点着看:

70 / 114
交互图解
流程一次类查找的完整路线(点着看)1 / 5
从 ① 点到 ⑤,重点是第 ③ 格——上级说「没有」,才轮到自己翻
→
→
→
→
① 拿到全限定名
`java Hello` 里的 Hello、或 import 进来的类名,最后都变成 com/example/Foo.class 这样一个路径。包名不是装饰,它就是目录结构——这也是第 3 篇那句「包名与路径不符」报错的根源。
全部看懂了一句话:查找顺序就是优先级,而 JDK 永远排在你的 jar 前面。
71 / 114

最后提前看一眼「环境不对时应用是怎么死的」。它不是 找不到主类 那种一句话报错,而是一整屏——下面的报错急救台练的就是这种现场:

72 / 114
内核实验
TeaVM启动失败现场:那一大坨该怎么从上往下读未启动
重点找 caused by 那一行,它通常在栈底;这就是第九节速查表和急救台共用的读法
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
73 / 114

实验做够了可以换成命令行自己敲。下面这台控制台连着浏览器里的同一个 Java 内核,回显全部由内核算出来——先敲 whoami 看当前生效的是哪份 JDK,再逐条 lab envpath:

74 / 114
内核控制台
75 / 114
说明

lab envpath home 和 lab envpath fix 要连着敲才有意义——前者复现「JAVA_HOME 多写了 \bin」,后者给出验证顺序。同一个报错,先看清它为什么发生,再看怎么确认修好了。

76 / 114
小节
八、沙盘:JAVA_HOME 该指向哪,版本号怎么选
77 / 114
原理动画
动图 · 你敲下 java Hello 之后,JDK 里面发生了什么
动图 · 你敲下 java Hello 之后,JDK 里面发生了什么
78 / 114

上面这条链里,第 1 步和第 3 步都能被你写错。左边改地址写法,右边立刻给出「谁会读到它、报什么错」;下面再选一个 JDK 版本,看看 Spring Boot 和语言特性分别要求到哪一档:

79 / 114
沙盘
沙盘JAVA_HOME 该指向哪 + JDK 版本 vs 语言特性
运行结果
mvn -v: OK
Spring Boot 3.x: OK
switch 模式匹配可用(预览特性已转正的部分)
当前教材与主流教程的标准配置。
80 / 114
说明

沙盘里最容易忽略的一格是「jre-only」。Oracle 从 JDK 11 起不再单独发布 JRE,所以你现在下载到的所谓「JRE」其实是个精简 JDK——遇到 javac 不是内部或外部命令,第一件事是确认手上这份到底带不带编译器(去 bin 目录看有没有 javac)。

81 / 114
小节
九、常见报错速查
82 / 114

这一表的「报错原文」都是可以直接整段粘进搜索框的关键词,别意译、别缩写:

83 / 114
对照表
报错原文(片段)真实原因30 秒自救深挖看第几篇
错误: 找不到或无法加载主类 Hello / Error: Could not find or load main class Hello运行的是文件名而不是类名,或 bin 里没有这个 .class,或包名与目录不一致改成 java Hello(不带 .class);dir target\classes 确认字节码真的存在本篇第四、六节 · #3 IDEA 工程
java.lang.UnsupportedClassVersionError: Hello has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 52.0用 JDK 8 的运行器去跑 JDK 17 编出来的字节码(61 = 17,52 = 8)java -version 与 mvn -v 打印的 Java version 对齐,统一升到 17+本篇第三节 · #36 打包部署
The JAVA_HOME is not defined correctly / JAVA_HOME is set to an invalid directoryJAVA_HOME 没设、指到了 bin、或指到了一个只剩 JRE 的目录改成 JDK 根目录(如 D:\dev\jdk-21),然后重开终端再看 echo %JAVA_HOME%本篇第三节 · 上方沙盘
'mvn' 不是内部或外部命令,也不是可运行的程序或批处理文件。Maven 的 bin 不在 PATH 里,或改完环境变量没重开终端把 %MAVEN_HOME%\bin 加进 PATH 并新开窗口;mvn -v 验证#2 Maven 入门
'java' 不是内部或外部命令PATH 里没有 %JAVA_HOME%\bin补 PATH;Windows 记得往下拖动列表顺序本篇第三节
错误: 编码 GBK 的不可映射字符源文件是 UTF-8 含中文,编译按系统默认 GBK 解码javac -encoding UTF-8 Hello.java;IDEA 里全局设 UTF-8#35 日志与环境 · #3 IDEA
Exception in thread "main" java.lang.NoClassDefFoundError: com/example/Foo编译时有、运行时没有——classpath 少了那个 jarjava -cp "target/classes;lib/*" com.example.Main本篇第七节 classpath 实验
84 / 114
坑

Could not find or load main class 这句话有两个成因被合并在一起了——找不到类文件(路径/classpath 问题)和找到了但加载失败(版本不匹配、静态初始化块抛异常)。分不清时加 -verbose:class 重跑一次,JVM 会把每个类的来源打出来。

85 / 114

上面那条「两个成因合并」的报错,最典型的例子就是版本不匹配。下面这段是真实堆栈,先别看答案——点出你认为的凶手行:

86 / 114
报错急救
报错急救UnsupportedClassVersionError: class file version 61.0
编译用的 JDK 比运行用的新:一屏报错没有一个业务类

本地用 JDK 21 编出 jar,交给 CI 上那台只有 JDK 8 的机器运行,进程连 main 都没进去。

java.lang.UnsupportedClassVersionError: com/bee/ordersvc/OrderServiceApplication has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 52.0
at java.base/java.lang.ClassLoader.defineClass1(Native Method)
at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1017)
at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:174)
at java.base/java.net.URLClassLoader.defineClass(URLClassLoader.java:555)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:589)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:635)
点你认为的「凶手行」(可反复试)
不会也没关系:先猜异常名,再猜哪一行在做决定。
87 / 114
小节
十、随堂自测
88 / 114
随堂自测
随堂自测你在 Windows 上配好了 JAVA_HOME 和 PATH,然后在已经打开的 PowerShell 窗口里敲 java -version,仍然报「不是内部或外部命令」。最可能的原因是?
先自己选一个,选中立刻告诉你对不对
89 / 114
随堂自测
随堂自测同事说:「我的项目在 JDK 21 上跑得好好的,pom.xml 里不用写版本,反正大家都装了 21。」这句话最大的问题是?
先自己选一个,选中立刻告诉你对不对
90 / 114
小节
十一、动手练习
91 / 114
小节
第一档 · 照做
92 / 114

目标:在纯命令行里走通「编译 → 运行 → 故意改错 → 读懂报错」全链路,不借助任何 IDE。

93 / 114
java
// EnvCheck.java —— 放在任意空目录下,注意文件名必须是 EnvCheck.javapublic class EnvCheck {    public static void main(String[] args) {        System.out.println("java.version = " + System.getProperty("java.version"));        System.out.println("java.home    = " + System.getProperty("java.home"));        System.out.println("user.dir     = " + System.getProperty("user.dir"));        System.out.println("classpath    = " + System.getProperty("java.class.path"));        for (int i = 0; i < args.length; i++) {            System.out.println("arg[" + i + "] = " + args[i]);        }    }}
94 / 114

依次执行并核对输出:

95 / 114
bash
javac EnvCheck.javajava EnvCheck 张三 李四java -cp . EnvCheck          # 显式写出 classpath,理解「-cp 是什么」java EnvCheck.class          # ← 故意做错一次,看清那句中文报错长什么样
96 / 114

预期输出(版本号会随你的 JDK 变化):

97 / 114
text
java.version = 21.0.2java.home    = D:\dev\jdk-21user.dir     = D:\tmp\envcheckclasspath    = .arg[0] = 张三arg[1] = 李四
98 / 114

验收清单:四行属性都能打印;最后一条错误命令能让你口头说出「为什么 java 后面不能带 .class」。

99 / 114
小节
第二档 · 变体
100 / 114

三个小改动,各自只动一个变量,观察结论不同:

101 / 114
  1. 把 JAVA_HOME 临时改成带 \bin 的写法,再跑 mvn -v。你会观察到 Maven 报 The JAVA_HOME is set to an invalid directory,而 java -version 依然正常——因为它们读的不是同一个东西。
  2. 用 java -cp " nonexistent_dir " EnvCheck 之类把 classpath 写歪。你会观察到 找不到或无法加载主类,此时第七节的 classpath 实验能告诉你 JVM 究竟翻了哪几个目录。
  3. 在同一目录放两份同名类的 class(javac -d out1 EnvCheck.java 与 -d out2,改掉一行打印内容),再用 java -cp "out1;out2" EnvCheck 和 "out2;out1" 各跑一次。你会观察到 输出跟着 classpath 的顺序变——这就是「重复 jar 谁排前面谁生效」。
102 / 114

提示:做完第 3 条再回来看第七节的「jar 包冲突」实验,抽象规则会立刻落地。

103 / 114
小节
第三档 · 造一个
104 / 114

给自己做一个「环境体检脚本」,以后每次换电脑、被同事的电脑气到时都能一键自查。

105 / 114
  • Windows 用 check-env.ps1,macOS / Linux 用 check-env.sh
  • 至少打印:java -version 的实际版本、javac -version、mvn -v 的 Maven 与 Java 两行、which java / where java 命中的完整路径、JAVA_HOME 是否指向根目录(不是 bin)
  • 每项要么打 [OK] 要么打 [FAIL] 原因 + 一句修法,全部通过时退出码为 0
  • 额外挑战:检测「同时装了多个 JDK」并把它们全列出来(Linux/macOS 可用 /usr/libexec/java_home -V 或 update-alternatives --list java)
106 / 114

验收清单:① 故意删掉 PATH 里的 java 一项,脚本报 FAIL 而不是崩溃;② 在两台机器上跑,输出差异能直接指出「版本不一致」这件事;③ 三个月后的你自己读得懂这些 FAIL 文案。

107 / 114
小节
十二、要点自查
108 / 114
自检

不看上文,说出 JDK / JRE / JVM 的包含关系,以及「现在为什么不用单独装 JRE」。

109 / 114
自检

JAVA_HOME 和 PATH 各自给谁读?把 JAVA_HOME 写成 ...\bin 会让谁报错、报哪句?

110 / 114
自检

java Hello 后面为什么不能加 .class?加了之后 JVM 会把你输入的字符串当成什么去找?

111 / 114
自检

字节码为什么能让「一次编写,到处运行」成立?跨的是哪一层——源码层还是机器码层?

112 / 114
自检

classpath 里出现两个同名类,哪一份生效?这条规则和「找书先看总馆」的类比怎么对上?

113 / 114
口诀

JDK 管装、JAVA_HOME 管找、PATH 管喊、字节码管跑——装的是工具,找的是路径,喊的是命令,跑的是平台无关的 class。

114 / 114
总结

这一节真正要记住的只有三件事——JDK 包含 JRE 包含 JVM;JAVA_HOME 指向根目录、PATH 指向 bin;版本管理交给工具,别手动折腾。这三件事理顺,后面所有教程都不会再被环境问题打断。