《Java 编程入门》2.3 IDE、JShell 与命令行

把 IDE、JShell 与命令行放在同一套工作流里:用 JShell 快速验证表达式和 library 的字符串格式,用 javac/java 理解编译与运行的分工,再说明 IDE 导入 Maven 工程、指定 JDK 25 和避免提交 .idea 的具体做法,最后给出三套工具之间切换的检查清单。

本节目标:知道什么场景该用 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 的步骤是:

  1. File → Open,选择 library/pom.xml,而不是选择目录。
  2. 选择「Open as Project」,让 IDEA 按 Maven 模型导入。
  3. 打开 File → Project Structure → Project,把 SDK 设为 JDK 25,Language level 设为 21(与 maven.compiler.release 对齐)。
  4. 在 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。程序会停在这行,此时可以:

  1. 把鼠标悬停在 args 上,看到数组长度和每个元素。
  2. 在 Variables 面板展开 args[0],确认它是 "list" 而不是带空格的字符串。
  3. 在 Console 面板执行 args.length,临时求值。
  4. 按 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 的完整流程 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Netty 高性能网络编程》15.3 linkd-v2 验收与复盘
  2. 《Netty 高性能网络编程》15.2 限流、超时与降级
  3. 《Netty 高性能网络编程》15.1 网关路由与转发