本节目标:知道什么场景该用 JShell、什么场景该用命令行、什么场景该用 IDE,并能让三者使用同一个 JDK 和同一份 Maven 配置。
适用版本:JDK 25 LTS(Temurin 25.0.5);Maven 3.9.12。
三种工具,三种反馈速度
学 Java 时最容易走两个极端:要么只用 IDE,离开鼠标就不会编译;要么坚持只用命令行,调试全靠 System.out.println。这两种都不是工程里的常态。真实项目里,三种工具各有一段最舒服的区间:
| 工具 | 反馈方式 | 适合做什么 | 不适合做什么 |
|---|---|---|---|
| JShell | 一次输入即得结果 | 试表达式、验证 API、确认类型推断 | 多文件工程、包结构、模块 |
| 命令行 | 一条命令一次构建 | 复现构建、排查环境、CI 脚本 | 大规模重构、图形化调试 |
| IDE | 即时补全与跳转 | 读代码、跳转、重构、断点调试 | 作为唯一的构建入口 |
判断标准不是「哪个更专业」,而是这件事需要多完整的上下文。只想确认 String.format 的输出宽度,用 JShell;想确认 Maven 能不能在干净环境构建,用命令行;想追一个跨十个类的调用链,用 IDE。
一个可执行的工作流是:先写一个 JShell 片段验证 API,确认行为后把它落成 .java 文件;用 mvn compile 在命令行确认项目能构建;再用 IDE 打开 pom.xml 做重构和调试。三步都过,才把改动提交。这个顺序看起来慢,实际比「在 IDE 里改完发现命令行编译不过」要快。
JShell:把表达式当草稿纸
JShell 是 JDK 9 引入的交互式工具。它不需要类、不需要 main、不需要编译命令,输入表达式就返回结果。启动:
jshell
本机 JDK 25 的会话(输入与结果分行展示):
jshell> var title = "Java 编程入门";
title ==> "Java 编程入门"
jshell> title.length();
$2 ==> 9
jshell> String.format("%-24s %s", title, "available");
$3 ==> "Java 编程入门 available"
jshell> int copies = 3;
copies ==> 3
jshell> copies * 8900;
$5 ==> 26700
几个细节值得注意:
var在 JShell 里可以直接用,类型由右侧推断。title的静态类型是String,不是「动态类型」。- 每个表达式的结果会被自动命名:
$2、$3、$5。这些名字可以在后续表达式里引用。 - 末尾的分号可写可不写;写了更像 Java 源码,不写更像计算器。
title.length()返回 9,因为Java四个字符、一个空格、四个汉字,共 9 个 UTF-16 单元。
JShell 的常用命令都以 / 开头:
| 命令 | 作用 |
|---|---|
/vars | 列出当前所有变量 |
/methods | 列出当前定义的方法 |
/types | 列出当前定义的类 |
/open file.jsh | 执行一个脚本文件 |
/save file.jsh | 把当前会话保存成脚本 |
/reset | 清空会话 |
/exit | 退出 |
/vars 的实际输出:
jshell> /vars
| int copies = 3
| String title = "Java 编程入门"
JShell 也有边界。它默认在无名包里执行片段,不能直接声明 package;它不适合验证模块路径、资源加载、Maven 生命周期这些「工程级」行为;它每次启动都是一个新会话,变量不会跨会话保留。JShell 是草稿纸,不是项目结构。 验证完的代码要落回 .java 文件,走正常编译流程。
把 JShell 会话保存成脚本
如果一段验证要反复做,可以把片段存成 .jsh 文件,用 jshell 直接执行。文件内容就是会话里的输入:
import com.plume.library.cli.ShelfFormatter;
String line = ShelfFormatter.line("Java 编程入门", "Leeting Yan", true);
System.out.println(line);
/exit
执行:
jshell --class-path target/classes shelf.jsh
本机输出:
Java 编程入门 Leeting Yan available
--class-path 让 JShell 能加载 Maven 编好的类;.jsh 文件里的 /exit 让脚本执行完自动退出,适合放进临时排查脚本。注意 .jsh 是 JShell 的脚本格式,不是普通 Java 源码,不能直接用 javac 编译。验证稳定后,把逻辑搬进 src/main/java,用 Maven 管起来。
命令行:最小可复现路径
命令行的价值在于「任何人照着敲都能得到同样结果」。library 目前只有 LibraryApp 和几个辅助类,用 javac 加 java 就能跑:
javac -d out $(find src/main/java -name '*.java')
java -cp out com.plume.library.LibraryApp list
本机输出:
Hello, Library
command = list
$(find ...) 在 macOS 和 Linux 的 shell 里都能用,但它把「找哪些文件」交给了 shell,不适合写进正式构建脚本。Windows 的 cmd 不支持这种写法,PowerShell 的语法也不同。跨平台项目要么用 Maven,要么用脚本语言包一层,不要指望一条 shell 命令走遍所有系统。
命令行里最常调整的三个参数:
-d out:指定输出目录。JVM 会按包名在out下建子目录。-cp out:指定类路径根目录。运行和编译都可能用到。--release 21:指定目标版本,同时切换 API 快照。想兼容 JDK 21 时用它,不要用-source/-target拼。
想确认产物版本:
javac --release 21 -d out21 src/main/java/com/plume/library/LibraryApp.java
javap -verbose out21/com/plume/library/LibraryApp.class | grep 'major version'
major version: 65
65 对应 JDK 21,69 对应 JDK 25。同一份源码,加不加 --release 21,产出的 class 文件版本不同。这个检查在排查 UnsupportedClassVersionError 时很有用。
命令行也能直接调用 Maven:
mvn -B compile
java -cp target/classes com.plume.library.LibraryApp
Maven 负责「找文件、拼参数」,java 负责「启动 JVM、加载主类」。把这两步分开理解,就不会把 mvn 当成黑盒。
IDE:把编辑器接到同一套工具上
IDE 提供补全、跳转、重构、断点调试,这些能力在大型项目里不可替代。但它应该建立在「命令行已经能构建」的基础上,而不是反过来。
以 IntelliJ IDEA Community 为例,导入 library 的步骤是:
File → Open,选择library/pom.xml,而不是选择目录。- 选择「Open as Project」,让 IDEA 按 Maven 模型导入。
- 打开
File → Project Structure → Project,把 SDK 设为 JDK 25,Language level 设为 21(与maven.compiler.release对齐)。 - 在 Maven 工具窗口执行一次
clean和compile,确认 IDE 使用的 Maven 与命令行一致。
第 2 步容易被忽略。直接打开目录时,IDEA 可能按「普通 Java 项目」处理,不识别 pom.xml 里的 release 和编码配置。打开 pom.xml 才会触发 Maven 导入。
第 3 步的 Language level 也要和 release 对齐。IDE 的 Language level 只影响编辑器检查和补全,不决定最终字节码;最终版本由 Maven 的 --release 决定。两者不一致时,IDE 可能对一段代码报错,但命令行能编译,或者反过来。统一成 21,能减少这类噪音。
VS Code 的路径类似:安装「Extension Pack for Java」,用「Java: Import Java Projects」导入 pom.xml,再在设置里把 java.configuration.runtimes 指向 JDK 25。Eclipse 则用 File → Import → Existing Maven Projects。工具不同,原则相同:导入 POM,不要只导入目录;显式指定 JDK,不要依赖默认值。
IDE 生成的 .idea/、*.iml、.classpath、.project 通常不提交。它们包含本机路径和个人偏好,提交后会让每次打开项目都产生 diff。团队需要共享的是 pom.xml、.gitignore 和代码风格配置,不是每个人的工作区状态。
用 IDE 断点调试一次
断点调试是 IDE 相对命令行的核心优势。以 LibraryApp 为例,在 System.out.println("command = " + args[0]); 这一行左侧点一下,出现红点,然后以 Debug 模式运行,传入参数 list。程序会停在这行,此时可以:
- 把鼠标悬停在
args上,看到数组长度和每个元素。 - 在 Variables 面板展开
args[0],确认它是"list"而不是带空格的字符串。 - 在 Console 面板执行
args.length,临时求值。 - 按 Step Over 执行下一行,观察输出。
这套动作在排查「参数解析为什么不对」时非常快。但它有两个前提:项目已经能被 Maven 编译,IDE 的 SDK 和 Language level 已经对齐。如果项目本身编译不过,断点根本不会命中,先把构建修好再调试。
IDE 的 Debug 本质上也是启动一个 JVM,加上 JDWP 调试代理。它不改变程序行为,只是让 IDE 能暂停线程、读写变量。所以「IDE 里能跑、命令行不能跑」通常不是调试器的问题,而是两边的类路径、工作目录或环境变量不同。
在 library 里落地
给 library 加一个简单的书架行格式化器,用来比较三种工具的反馈:
package com.plume.library.cli;
public final class ShelfFormatter {
private ShelfFormatter() {
}
public static String line(String title, String author, boolean available) {
return String.format("%-24s %-16s %s",
title, author, available ? "available" : "borrowed");
}
}
先用 Maven 编译,让类进入 target/classes:
mvn -B compile
然后用 JShell 加载这个类路径,直接试格式:
jshell --class-path target/classes
jshell> import com.plume.library.cli.ShelfFormatter;
jshell> ShelfFormatter.line("Java 编程入门", "Leeting Yan", true)
$2 ==> "Java 编程入门 Leeting Yan available"
jshell> ShelfFormatter.line("Effective Java", "Joshua Bloch", false)
$3 ==> "Effective Java Joshua Bloch borrowed"
%-24s 表示左对齐、最小宽度 24。宽度按字符数计算,不按显示宽度计算,所以中文和英文混排时视觉上不一定对齐。真实 CLI 里如果要严格对齐,需要按显示宽度计算(中文通常占两个英文字符宽),或者干脆用制表符和固定列。JShell 适合先确认「格式化字符串本身对不对」,显示宽度问题要放到真实终端里看。
主程序入口仍然是最小版本:
package com.plume.library;
public final class LibraryApp {
public static void main(String[] args) {
System.out.println("Hello, Library");
if (args.length == 0) {
System.out.println("usage: library <add|list|borrow|return|search|quit>");
return;
}
System.out.println("command = " + args[0]);
}
}
再回到命令行跑主程序:
java -cp target/classes com.plume.library.LibraryApp borrow
Hello, Library
command = borrow
同一个 target/classes,JShell 和 java 都能用。这说明类路径是它们共同的契约:Maven 把类编译到这个目录,JShell 和 java 都从这里加载。IDE 的「Run」按钮本质上也是拼一条类似的 java 命令,只是把类路径换成了它自己计算的模块/依赖列表。
常见错误与边界
「jshell 命令找不到。」 说明当前 PATH 里的 JDK 不完整,或者装的是 JRE。jshell 从 JDK 9 起随 JDK 分发,JRE 里没有。用 which jshell 和 javac -version 一起确认,别只检查 java。
「JShell 里能跑,项目里编译失败。」 JShell 的片段在无名包里执行,不检查包声明、模块依赖和访问控制。比如包私有类在 JShell 里能直接引用,放到真实包里就会被编译器拒绝。验证语法可以,验证工程结构不行。
「IDE 能跑,mvn 报错。」 九成是 JDK 不一致。IDE 的项目 SDK 是 25,命令行 JAVA_HOME 是 17;或者 IDE 用的是内置 Maven,命令行用的是另一份。先对齐 mvn -version 里的 Java version,再看依赖和插件。
「IDE 的 Language level 设成了 25,但 maven.compiler.release 是 21。」 编辑器会允许你用 25 的语法,Maven 编译时却报「不支持」。把 Language level 设成 21,或者把 release 提到 25,两者必须一致。本书选 21,所以 IDE 也设 21。
「IDE 里点 Run 没带参数。」 IDE 的 Run configuration 不会自动读取你在命令行敲的 list。要在配置的 Program arguments 里填参数,或者直接在 IDE 的 Terminal 里执行 java -cp target/classes com.plume.library.LibraryApp list。两种方式等价,区别只是谁拼命令。
小结
- JShell 适合秒级验证表达式和 API,不适合验证包、模块、Maven 生命周期;它的片段在无名包里执行。
- 命令行是最小可复现路径,
javac -d、java -cp、--release三个参数要能独立解释。 - IDE 应该导入
pom.xml,显式指定 JDK 25 和 Language level 21,不要只导入目录。 - 三种工具的共同契约是类路径:Maven 编译到
target/classes,JShell 和java都从这里加载。 - 可重复的验证可以存成
.jsh脚本,但脚本不是项目源码,验证稳定后要搬回src/main/java。 .idea/、*.iml等个人工作区文件不提交;pom.xml、.gitignore才进版本库。- 环境不一致时,先对齐
java -version、mvn -version和 IDE 的项目 SDK,再排查代码。 - IDE 的 Run configuration 要显式填 Program arguments,否则
args是空数组。 - 断点调试只是 JDWP 代理,不改变程序行为;两边结果不同时先查类路径和工作目录。
.jsh脚本用jshell --class-path执行,不是普通 Java 源码,不能直接交给javac。
工具链已经能稳定地编译和运行了。下一章从最小的 Hello, Library 开始,把「源码 → class 文件 → JVM 执行」这条链路完整走一遍,并用 javap 看每一步留下了什么。
阅读导航:上一节:2.2 Maven 与标准目录结构 · 下一节:3.1 Hello, Library:javac 与 java 的完整流程 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。