以前给别人演示一个小工具,如果代码拆成几个类,就得先javac编译一通,再java运行主类。要是旁边没有装JDK,只有JRE,还得捣鼓classpath,麻烦得很。上周末翻Java 23的发布说明,发现JEP 458给了个爽快的玩法——你可以直接java Main.java运行一个包含多个类的程序,不再需要手动编译了。今天拿一个真实的小案例来试一试。
从单文件到多文件,解决了什么别扭
Java 11开始有“单文件源码启动”,也就是java Test.java直接跑。但如果你在Test.java里用了另一个自定义类,就算这个类也放在同一个目录里,直接运行还是会报“找不到符号”。所以以往还得退回去用javac。
JEP 458直接把“单文件”扩展成了“多文件”。只要用java命令启动一个入口源文件,它会自动在当前目录下寻找这个源文件引用的其他源文件,在内存里编译并运行。也就是说,你不用看到.class文件,不需要敲编译命令,源码就是可执行的东西。
一、先看一个完整的案例
我写了一个很简单的订单结算程序,里面有两个普通的类和一个record。功能是算2个商品的总价和折扣。
先建立三个文件,放在同一个目录,比如order文件夹下。
Item.java:
public class Item {
private final String name;
private final double price;
private final int quantity;
public Item(String name, double price, int quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
public double getTotal() {
return price * quantity;
}
public String getName() {
return name;
}
}
Order.java:
import java.util.List;
public class Order {
private final List<Item> items;
public Order(List<Item> items) {
this.items = items;
}
public double calcTotal() {
double total = 0;
for (Item item : items) {
total += item.getTotal();
}
return total;
}
public double calcDiscountedTotal(double discountRate) {
return calcTotal() * (1 - discountRate);
}
}
Main.java:
import java.util.List;
public class Main {
public static void main(String[] args) {
var items = List.of(
new Item("薯片", 8.5, 2),
new Item("可乐", 3.0, 3)
);
var order = new Order(items);
System.out.println("商品明细:");
for (Item item : items) {
System.out.printf(" %s x%d,小计%.2f%n", item.getName(), item.getQuantity(), item.getTotal());
}
System.out.printf("原价合计:%.2f%n", order.calcTotal());
System.out.printf("打八折后:%.2f%n", order.calcDiscountedTotal(0.2));
}
}
运行命令,假设命令行当前目录在order下:
java Main.java
直接得到输出:
商品明细:
薯片 x2,小计17.00
可乐 x3,小计9.00
原价合计:26.00
打八折后:20.80
全程没有javac,也没有生成class文件。这比我预想的还爽,因为它连入口类里import的那些类也自动找到了。
二、它是怎么找到的吗
JEP 458做的事情本质上是“用javac的源码路径机制”自动解析源码。当你执行java Main.java,编译器会从Main.java开始,把其中引用的类型对应的源文件也加载进来,相当于在内存里做一次增量编译。只要这些类没有循环依赖或者缺失,就可以直接运行。即使你用了Record、枚举、内部类,都没问题。
如果你用的是Java 21或更早版本,这个功能是没有的。我当时用Java 17试了下,java Main.java会报错找不到Item。升级到Java 23后,一切都安静了。
三、带包的代码也支持
如果你的源文件声明了package,只要目录结构和包名一致,照样能跑。只是你需要把入口文件的路径带上去,比如:
java demo/order/Main.java
假设Main.java位于demo/order/下,并且包含package demo.order;,那么它引用的其他类会自动从包路径下搜到。实际上JEP 458规定启动的源文件不能是被模块化的代码,也就是说不要在源文件旁边放一个module-info.java。但用于普通的小项目足够了。
四、能不能用第三方JAR包
可以,通过–class-path选项。比如项目里用了Apache Commons Lang的StringUtils,启动命令就是:
java --class-path commons-lang3.jar Main.java
但要注意:这个模式下,class-path里的jar包只能提供外部依赖,不能替代你当前要编译的源码。你的源码仍然必须能被find出来。
五、编写代码时最好注意的几个点
- 所有源文件必须放在一个能被自动搜索到的目录里。最方便的就是把入口文件和其他类放在同一个顶层目录下。如果你的包结构比较复杂,只要目录按包名放好,也能自动找到。
- 不要混用不同包的默认构造。如果多个类在同一个包里,互相可见没问题。但如果你写了一个带package的类,又写了一个无package的类,很容易导致找不到。建议要么全带包,要么全不带。
- 启动的源文件不能有module-info.java。也就是不支持模块化。所以那些需要用到Java模块路径的项目还得按传统方式。
- 每次运行会有编译开销。因为它是“现场编译”,每次跑都会重新在内存里编译一次。代码量小还好,要是几千个类,启动速度会比较感人。所以适合小工具、脚本化教学、快速Demo,不适合大型应用。
六、再展示一个更贴近“脚本”的玩法
为了体现这个特性的便利,我写一个基于Record和Sealed Interface的小程序,模拟一个“形状周长计算”。这种Java新特性和多文件启动搭配起来,代码就像脚本一样快。
创建shape目录,下面是Shape.java:
public sealed interface Shape permits Circle, Rectangle {
double perimeter();
}
Circle.java:
public record Circle(double radius) implements Shape {
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}
Rectangle.java:
public record Rectangle(double width, double height) implements Shape {
@Override
public double perimeter() {
return 2 * (width + height);
}
}
Main.java:
public class Main {
public static void main(String[] args) {
Shape circle = new Circle(3);
Shape rectangle = new Rectangle(4, 5);
System.out.printf("半径3的圆周长:%.2f%n", circle.perimeter());
System.out.printf("4x5的矩形周长:%.2f%n", rectangle.perimeter());
}
}
然后在shape目录下执行:
java Main.java
结果就像运行脚本那样,打印出周长。同时,Record的构造方法和toString也自动生成,整个程序结构非常紧凑。
七、什么时候我会用它
最直接的是给同事演示某个新API或算法时,不用先开IDE,也不用复制粘贴成一个类。直接传一个目录过去,他们自己用java Main.java就能跑起来。
另外在做代码教学的时候也很方便,因为学生不用理解编译和类路径,先专注在业务逻辑上。等后来真正需要大型项目时,再讲构建工具。
如果你喜欢写一些“命令行小工具”放在本机,这个方法也特别舒服——写完直接跑,还不会在目录里留下.class垃圾。
八、与脚本语言相比,Java终于有了点“脚本感”
Java确实不是脚本语言,但这个特性让它更轻快了。你依然需要写public static void main,但你已经不用管编译这一步了。对于Java新手来说,这是一个非常友好的入门方式:先用普通文本编辑器写几个类,然后一条命令运行。没有Maven,没有Gradle,没有classpath的迷宫。
不过也别指望能完全脱离javac。多文件源码启动在背后仍然调用jdk内部的编译器,只是帮我们省略了显式的编译命令和中间产物。它更像是一个“隐式编译”的入口。
九、总结
JEP 458让Java向“方便”又迈了一步。这次不用写任何构建脚本,也没有外部依赖,直接就有一个纯JDK的方案来运行多文件程序。对快速原型、算法演练、小工具来说,几乎完美。如果你还在使用Java 17或21,不妨升级到Java 23体验一下。以后我写demo再也不想开IDE了,直接一个vscode加终端就够了。
如果你也经常要给人分享代码片段,试试这个新玩法,确实能省下不少时间。

