Skip to content

单文件源代码程序

以前写一个简单的 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.class

JDK 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 文件保留在内存(下次运行直接用)                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

关键点

  1. 不需要 javac 命令,JDK 自动编译
  2. 编译只在必要时进行(源文件比 class 新才编译)
  3. 编译产生的 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");

与脚本语言的对比

特性PythonRubyNode.jsJava 单文件
运行命令python app.pyruby app.rbnode app.jsjava 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

什么时候用它:

  • 快速原型验证
  • 小工具、脚本
  • 教学演示
  • 数据处理任务

什么时候不用:

  • 需要包结构
  • 依赖复杂
  • 多人协作项目

基于 VitePress 构建