适用读者:已经装完 IDE、跑通 Hello World(S01-B01),但开始遇到"命令行和 IDE 用的不是同一个 JDK"的人。
前置:S01-B01。
实测环境:Ubuntu 24.04.1 LTS / OpenJDK 21.0.12.1(/usr/lib/jvm/java-21-openjdk-amd64)/ Apache Maven 3.8.7。
版本基线:JDK 17 与 21 双装;IDEA 2025.x。
覆盖旧文:A-1373(35 个原文点位)。
新增扩展:E-S01-001(发行版对比)、E-S01-002(sdkman / mise)、E-S01-003(JAVA_HOME 与 PATH)、E-S01-008(SDK / Language Level / target 对齐)。
上一篇:S01-B01 · 开发环境全景:JDK、Maven、IDE、终端各自管什么
下一篇:S01-B03 · Maven 安装、镜像与第一次构建。
1. 一句话结论
java 命令靠 PATH 找,构建工具和 IDE 常常靠 JAVA_HOME 找,IDE 项目又单独存了一份 SDK 设置。三套机制各自独立——所以"三处版本一致"必须逐处验证,不能靠感觉。
本篇要解决的就是这个"三处对齐"问题:
| 位置 | 由谁决定 | 常见失控原因 |
|---|---|---|
① 命令行 java / javac |
PATH 里第一个 java(常是 /usr/bin/java → 符号链接) |
PATH 顺序变了、装新 JDK 后没切 alternatives |
| ② Maven / Gradle 运行用的 JDK | JAVA_HOME,缺失时回退到 PATH |
JAVA_HOME 空(能跑但要警告)、指向 JRE、指向旧版本 |
| ③ IDE 项目用的 JDK | 项目 .idea/ 里的 SDK 名 + IDE 全局 SDK 表 |
IDE 内置 JDK、SDK 名指向已删除目录 |
2. 先把概念分清:别把四个词当一件事
| 词 | 是什么 | 你要关心吗 |
|---|---|---|
| JVM | 执行字节码的虚拟机 | 不用直接管,跟着 JDK 来 |
| JRE | JVM + 核心类库,只能运行不能编译 | 不要把 JAVA_HOME 指向它(javac 会找不到) |
| JDK | JRE + 编译器(javac)+ 工具(jstack、jmap、jshell…) |
这才是要装的东西 |
| 发行版(Distribution) | 同一个 JDK 版本的不同"打包商":Temurin、Corretto、Zulu、Microsoft Build、Oracle | 装哪个都行,但别混着装;生产环境统一一个 |
版本号怎么读
openjdk version "21.0.12.1" 2026-08-18
↑ ↑ ↑ ↑
│ │ │ └── 安全补丁号(build 内的小修订)
│ │ └───── 补丁版本(含安全修复)
│ └──────── 特性版本(21 = LTS)
└───────────────── 发行版前缀;Temurin 会显示 "Temurin"
- LTS:8 / 11 / 17 / 21 / 25。团队项目锁 LTS,别用中间版本(如 22、23)做长期环境。
UnsupportedClassVersionError: class file version 65.0的含义:这个.class是 JDK 21 编译的(65),而运行它的 JVM 是 17(61)。错误在"运行端 JDK 太旧",不在代码。
3. 实测:JAVA_HOME 空着也能跑,指错了立刻死
这是本篇最值得记住的一组实验(同一台 Ubuntu 24.04.1,只改变量,不改系统):
# ① 不设 JAVA_HOME(该机器默认状态)
$ mvn -v
Apache Maven 3.8.7
Maven home: /usr/share/maven
Java version: 21.0.12.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64
能跑。 Maven 的启动脚本在 JAVA_HOME 为空时会回退到 PATH 里的 java。
# ② JAVA_HOME 指向一个不存在的目录
$ JAVA_HOME=/opt/does-not-exist mvn -v
The JAVA_HOME environment variable is not defined correctly,
this environment variable is needed to run this program.
直接失败,而且报错信息不会告诉你"你指的是个不存在的目录"。
# ③ JAVA_HOME 指向真实 JDK
$ JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 mvn -v
Apache Maven 3.8.7
Java version: 21.0.12.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64
结论
JAVA_HOME为空 ≠ 环境坏,很多工具能靠PATH兜底;但兜底是"隐式"的,一旦你装了第二个 JDK,兜底结果就不可预测。JAVA_HOME指错 = 直接崩,且报错不指向根因。- 所以实践上:要么明确设置
JAVA_HOME,要么彻底依赖alternatives/sdkman统一管理——不要半吊子。
4. 看一眼链接链:java 到底是谁
$ which java
/usr/bin/java
$ readlink -f $(which java)
/usr/lib/jvm/java-21-openjdk-amd64/bin/java
# Debian/Ubuntu 的版本登记表(只读)
$ update-alternatives --display java
java - auto mode
link best version is /usr/lib/jvm/java-21-openjdk-amd64/bin/java
link currently points to /usr/lib/jvm/java-21-openjdk-amd64/bin/java
link java is /usr/bin/java
/usr/lib/jvm/java-21-openjdk-amd64/bin/java - priority 2111
/usr/bin/java 是符号链接,指向系统登记(alternatives)里选中的那个 JDK。装第二个 JDK 后,update-alternatives 里会出现两条,切哪个就生效哪个。
这就是为什么"卸载/重装 JDK 后
java -version变了,但mvn没变":前者走alternatives的链接,后者走JAVA_HOME。两条路。
5. 装两套 JDK:17 与 21
5.1 Ubuntu / Debian(alternatives 体系)
sudo apt update
sudo apt install -y openjdk-17-jdk openjdk-21-jdk
# 查看已安装
ls /usr/lib/jvm/
# java-17-openjdk-amd64 java-21-openjdk-amd64
# 交互切换(java 与 javac 要分别切!)
sudo update-alternatives --config java
sudo update-alternatives --config javac
验证:
$ java -version && javac -version
openjdk version "17.0.x" ...
openjdk 17.0.x ...
坑:只切
java不切javac,会出现java -version是 17、javac -version是 21 —— 编译用 21、运行用 17,直接命中 §2 的UnsupportedClassVersionError。两者必须一起切。
5.2 macOS(Homebrew 或安装包)
# Homebrew(注意 Homebrew 只装 JDK,不自动改 JAVA_HOME)
brew install --cask temurin@17 temurin@21
# 找到安装位置
/usr/libexec/java_home -V
# Matching Java Virtual Machines:
# 21.0.x (arm64) "Eclipse Adoptium" - "Temurin-21" /Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home
# 17.0.x (arm64) "Eclipse Adoptium" - "Temurin-17" /Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home
# 临时切到 17(只对当前 shell 生效,最安全)
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
/usr/libexec/java_home -v <版本> 是 macOS 最省事的切换方式:不改系统状态,只影响当前会话。
5.3 Windows
- 官方
.msi安装包默认写入注册表,但不会替你改JAVA_HOME。 - 双版本共存建议:安装到不同目录(如
D:\tools\jdk17、D:\tools\jdk21),把JAVA_HOME指向"当前使用"的那个,Path里只放%JAVA_HOME%\bin(不要同时放两个 JDK 的bin,否则先命中谁全看顺序)。 - 切换靠改环境变量或写两个
.bat:
:: use-jdk17.bat
setx JAVA_HOME "D:\tools\jdk17"
:: 需要重开终端;PATH 里已引用 %JAVA_HOME%\bin
坑:
JAVA_HOME的值不要带引号,也不要带\bin后缀。正确:D:\tools\jdk17;错误:"D:\tools\jdk17"或D:\tools\jdk17\bin。
6. 三种切换方案对比:选一种,别混用
| 方案 | 平台 | 优点 | 缺点 | 适合谁 |
|---|---|---|---|---|
update-alternatives |
Debian/Ubuntu | 系统级、GUI/脚本都生效;一条命令切换 | 只覆盖系统包装的 JDK;要记得切 javac |
Linux 服务器、固定几套系统 JDK |
| sdkman | Linux/macOS/WSL | 任意发行版、任意版本;sdk use 只影响当前 shell;可装 Temurin/Corretto/Zulu |
需要 source 初始化脚本;CI 里要显式安装 |
开发者本机(推荐) |
| mise / asdf | 跨平台(含 Windows) | 与 Node/Python 等统一管理;.tool-versions 随仓库走 |
配置概念略多 | 多语言项目、要"项目级锁版本" |
| 手工环境变量 | 全平台 | 零依赖 | 容易忘、容易指错(§3 实验②) | Windows、临时验证 |
| IDE 内置下载器 | 全平台 | 一键 | 与命令行脱钩,制造"只在 IDE 能跑" | 不建议 |
sdkman 最小用法
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk list java | head # 列出可用发行版/版本
sdk install java 21.0.5-tem # 装 Temurin 21
sdk install java 17.0.13-tem # 装 Temurin 17
sdk use java 17.0.13-tem # 只对当前 shell 生效(推荐)
sdk default java 21.0.5-tem # 改默认
关键差别:sdk use 只影响当前终端会话,sdk default 才改全局。这样你可以"这个窗口跑老项目(17),那个窗口跑新项目(21)",互不干扰。
7. 在 IDEA 里配置 JDK:项目级与全局
旧文 A-1373 给的步骤是对的,这里补上"为什么"和"配完怎么验证"。
7.1 为当前项目配置
File → Project Structure…(Ctrl+Alt+Shift+S / ⌘;)
→ Project Settings → Project
SDK: 选择 JDK 17 或 21(没有就 Add SDK → JDK… 指向安装目录)
Language level: 与 SDK 主版本一致(17 / 21)
→ Project Settings → Modules → Sources
Language level: 同上
→ Apply → OK
7.2 全局默认(影响以后新建的项目)
File → New Projects Setup → Structure…
→ 同上配置 → 以后 New Project 默认使用
7.3 三者对齐:SDK / Language level / Compiler target
这是 A-1373 没讲、但最容易出错的一层。"我明明换了 JDK 21,为什么还是编译成 17 的字节码?" 因为有三处设置:
| 设置 | 作用 | 不对齐的症状 |
|---|---|---|
Project SDK |
用哪个 JDK 编译/运行 | 无法用新语法(如 record pattern) |
Language level |
IDE 允许你用哪版语言特性 | 语法报红说"不支持" |
Maven maven.compiler.release(或 source/target) |
实际产出什么字节码 | 编译成功但运行报 UnsupportedClassVersionError |
Maven 项目里,最终话语权在 pom.xml:
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
release 比 source+target 更好:它同时约束语言特性与 API(用 source/target 时你可能调用了 JDK 21 才有的 API 却声称兼容 17)。
实测提醒(S01-B03 复现):只写
<maven.compiler.release>属性不一定生效——release是maven-compiler-plugin3.6+ 才支持的参数。Maven 3.8.7 在不声明插件版本的默认插件链里用的是 3.1,会直接报:[ERROR] Source option 5 is no longer supported. Use 7 or later. [ERROR] Target option 5 is no longer supported. Use 7 or later.所以生产上要显式锁插件版本(
maven-compiler-plugin3.13.0 起)+<release>,两者缺一不可。完整实验见 S01-B03 §5。
验证方式(命令行,不靠肉眼):
$ mvn -q help:evaluate -Dexpression=maven.compiler.release -DforceStdout
21
$ javap -v target/classes/Main.class | grep 'major version'
major version: 65 # 65 = Java 21;61 = Java 17
major version对照:52=8、55=11、61=17、65=21、69=25。
7.4 IDE 与 Maven 不同步的典型现象
| 现象 | 原因 | 处置 |
|---|---|---|
项目全红,但终端 mvn -q compile 成功 |
IDE 的 Maven 模型未 reload | Maven 面板 → Reload All Maven Projects |
IDE 里 JDK 是 21,mvn 出来 17 |
IDE SDK ≠ JAVA_HOME |
统一到同一个 JDK(§3 实验③) |
换了 pom.xml 里的 release 但 IDE 仍报语法错 |
IDE 的 Language level 跟着 pom 走需要 reload | reload 后检查 Project Structure → Modules |
8. 六个高频坑(按踩坑概率排序)
| # | 坑 | 症状 | 修法 |
|---|---|---|---|
| 1 | JAVA_HOME 指向 .../jre |
javac: not found;Maven 报环境不正确 |
指到 JDK 根目录(含 bin/javac) |
| 2 | 只切 java,没切 javac |
编译/运行版本错位 | 两个都 update-alternatives --config |
| 3 | Windows Path 里并存两个 JDK bin |
版本随机 | 只留 %JAVA_HOME%\bin |
| 4 | JAVA_HOME 带引号或带 \bin |
Maven 报 "not defined correctly" | 去掉引号与后缀 |
| 5 | 用 IDE 内置下载器装 JDK | 只在 IDE 能跑;命令行找不到 | 自己装,IDE 里 Add SDK 指过去 |
| 6 | 装了非 LTS(22/23)当日常环境 | 半年后依赖/插件不支持 | 回归 17/21 |
9. 自检清单
按顺序执行,全部符合才算"三处对齐":
# ① 命令行
java -version # 期望:17.x 或 21.x
javac -version # 期望:与上面同主版本
# ② JAVA_HOME
echo $JAVA_HOME # 期望:非空,且指向 JDK 根(不是 jre,不带 /bin)
ls $JAVA_HOME/bin/javac # 期望:文件存在
# ③ Maven
mvn -v | head -3 # 期望:Java version 与 ① 一致
# ④ 字节码
javap -v target/classes/*.class | grep 'major version' # 期望:与 release 一致
IDE 侧两条:Project Structure → Project SDK 与命令行同版本;Language level 与 maven.compiler.release 同代。
10. 旧文对照
| 项 | 旧文 A-1373 | 本篇处理 |
|---|---|---|
| 项目级配置 JDK | ✅ 给了 File → Project Structure → SDKs → + → Add JDK 步骤 |
保留,补 Language level 与 pom 的 release |
| 全局配置 JDK | ✅ File → New Projects Setup → Structure |
保留,补"为什么新建项目会自动继承" |
| 前提(先装 JDK 并配环境变量) | ⚠️ 只给了外链到两篇 JDK 教程 | 升级为本篇 §5 正文,不再依赖外链 |
JAVA_HOME 的行为 |
❌ 未提及 | 新增 §3 三组实测 |
| 多版本共存 | ❌ 未提及 | 新增 §5、§6(alternatives / sdkman / mise 对比) |
| SDK / Language level / release 三者关系 | ❌ 未提及 | 新增 §7.3,附 major version 对照表 |
11. 自测
java -version是 21,mvn -v显示 17。列出至少两种可能原因与各自的修法。- 为什么建议
maven.compiler.release而不是source+target? update-alternatives --config java之后还需要做什么?不做会怎样?- macOS 上
export JAVA_HOME=$(/usr/libexec/java_home -v 17)与sdk default java 17的差别是什么? - 团队里有人 Windows 装了两套 JDK,
Path里都放了bin。他报"版本随机变",你怎么一句话定位?
12. 下一篇
S01-B03 · Maven 安装、镜像与第一次构建
JDK 对齐之后,第一个会挡路的就是依赖下载:Could not resolve dependencies 到底该查本地仓库、镜像配置还是网络?下一篇用"空 ~/.m2 首次构建"和"改镜像再构建"两组对照,把 Maven 的查找顺序讲清楚,并给出 mvnw 的可复现做法。
本篇属于系列《S01 · 开发环境与工具链》。旧文来源:A-1373;扩展点 E-S01-001/002/003/008。点位映射见 ../coverage.csv。