单文件源代码程序
以前写一个简单的 Java 程序,需要:
bash
# 1. 创建文件
$ cat > Hello.java << 'EOF'
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
EOF
# 2. 编译
$ javac Hello.java
# 3. 运行
$ java Hello
Hello, World!
# 4. 清理
$ rm Hello.java Hello.classJDK 11 让这个过程变成一步:
bash
$ java Hello.java
Hello, World!快速体验
java
// Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, World!");
System.out.println("JDK 版本: " + System.getProperty("java.version"));
}
}bash
$ java Hello.java
Hello, World!
JDK 版本: 21工作原理
┌─────────────────────────────────────────────────────────────────┐
│ java <source-file>.java 执行流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ java Hello.java │
│ ↓ │
│ 1. 检测是否有对应的 .class 文件 │
│ ↓ │
│ 2. 如果没有或源码更新 → 自动编译 │
│ ↓ │
│ 3. 运行编译后的 class │
│ ↓ │
│ 4. .class 文件保留在内存(下次运行直接用) │
│ │
└─────────────────────────────────────────────────────────────────┘关键点:
- 不需要
javac命令,JDK 自动编译 - 编译只在必要时进行(源文件比 class 新才编译)
- 编译产生的 class 文件会缓存,重复运行更快
适用场景
场景一:快速脚本
java
// fetch-url.java
import java.net.URI;
import java.net.http.*;
public class fetch-url {
public static void main(String[] args) throws Exception {
var url = args.length > 0 ? args[0] : "https://example.com";
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}bash
$ java fetch-url.java https://api.github.com
{"id": 1, "name": "example"}场景二:数据处理
java
// word-count.java
import java.nio.file.*;
import java.util.*;
public class word-count {
public static void main(String[] args) throws Exception {
var text = Files.readString(Path.of(args[0]));
var words = text.toLowerCase().split("\\W+");
var count = Arrays.stream(words)
.filter(w -> w.length() > 3)
.collect(java.util.stream.Collectors.groupingBy(
w -> w,
java.util.stream.Collectors.counting()
))
.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(20)
.toList();
count.forEach(e -> System.out.println(e.getValue() + "\t" + e.getKey()));
}
}bash
$ java word-count.java article.txt | head -10场景三:教学演示
java
// stream-demo.java
import java.util.*;
public class stream-demo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
System.out.println("原始数据: " + numbers);
System.out.println("\n过滤偶数:");
numbers.stream()
.filter(n -> n % 2 == 0)
.forEach(System.out::println);
System.out.println("\n平方后求和:");
var sum = numbers.stream()
.map(n -> n * n)
.reduce(0, Integer::sum);
System.out.println(sum);
}
}语法规则
类名不必与文件名匹配
java
// myscript.java(文件名)
// 类名可以是任意合法的标识符
public class MyScript {
public static void main(String[] args) {
System.out.println("Hello!");
}
}bash
$ java myscript.java
Hello!可以包含多个类
java
// tools.java
class Utils {
static int add(int a, int b) {
return a + b;
}
}
class Helper {
static void print(Object o) {
System.out.println(o);
}
}
public class tools {
public static void main(String[] args) {
int result = Utils.add(1, 2);
Helper.print("Result: " + result);
}
}不能包含包声明
java
// ❌ 不允许
package com.example;
public class Hello {
public static void main(String[] args) {
System.out.println("Hello!");
}
}不能包含 import 语句(部分场景)
java
// ❌ 在 JDK 11 中,单文件程序不能有 import
// ✅ 使用完全限定类名
var list = new java.util.ArrayList<String>();
// ✅ 或者用 static import
import static java.util.Arrays.asList;
var list = asList("a", "b", "c");与脚本语言的对比
| 特性 | Python | Ruby | Node.js | Java 单文件 |
|---|---|---|---|---|
| 运行命令 | python app.py | ruby app.rb | node app.js | java app.java |
| 类型系统 | 动态 | 动态 | 动态 | 静态 |
| 生态 | 丰富 | 丰富 | 丰富 | 丰富 |
| 编译 | 无 | 无 | 无 | 自动(幕后) |
| 启动速度 | 快 | 快 | 快 | 较慢(JIT 编译) |
Java 单文件程序比脚本多了自动编译步骤,首次运行需要一点编译时间。
局限性
没有包结构
java
// ❌ 不支持
package com.example;
// ✅ 只能这样
public class Example {
public static void main(String[] args) {
System.out.println("Simple");
}
}只能访问 classpath 上的依赖
java
// ❌ 不能引用项目中的其他类
// ✅ 只能引用 JDK 内置类和 -cp 指定 JAR 中的类
# 需要第三方库时
java -cp ".:lib/*" my-script.java不适合复杂项目
# ❌ 不适合
java BigProject.java # 项目有几十个类
# ✅ 适合
java SingleFileScript.java与 JShell 的区别
| 特性 | JShell | 单文件程序 |
|---|---|---|
| 适用场景 | 交互式探索、API 测试 | 简单脚本 |
| 文件 | 不需要 | 需要 .java 文件 |
| 多个类 | 不支持 | 支持 |
| 重复运行 | 慢(每次重新解析) | 快(有缓存) |
| 调试 | 有限 | 标准调试器 |
| 类定义 | 临时 | 持久 |
小结
单文件源代码程序让 Java 也能像脚本一样运行:
bash
# 写一个文件
cat > hello.java << 'EOF'
public class hello {
public static void main(String[] args) {
System.out.println("Hello, Java!");
}
}
EOF
# 直接运行
java hello.java什么时候用它:
- 快速原型验证
- 小工具、脚本
- 教学演示
- 数据处理任务
什么时候不用:
- 需要包结构
- 依赖复杂
- 多人协作项目
