← [ BACK_TO_NOTES ]

Java 程序结构与 main

Java 编译单元、类、包声明与 main 入口约定:程序如何被组织成可被 JVM 启动的类型。

#concept #growing #dev #java

[!info] related notes

Java 程序结构与 main

这个概念为什么出现

Java 程序不是“从第一行脚本顺序执行到底”的默认心智,而是:

  • 源码组织为编译单元(通常一个 .java 文件)
  • 类型(class/interface 等)是基本结构
  • 本篇采用 Java 21 非预览语法的经典入口:public static void main(String[] args);它不是所有 Java 版本唯一合法的入口形态。

不先钉死结构,包名、文件名、类名与启动命令会反复打架。

[!abstract] 一句话理解 Java 源码以类为中心组织;本篇的经典应用通过公开的静态 main(String[]) 启动,包声明决定类型的全限定名与目录约定。

最小可运行示例

先把示例放进业务场景,再看代码:

场景:结算服务用包名区分“启动入口”和“领域模型”

小服务也要避免把所有东西塞进默认包。入口类负责启动日志,领域类描述订单号。
教学点:package、public 类与文件名、main 签名、全限定启动。

// 文件路径:shop/app/CheckoutApp.java
package shop.app;

// CheckoutApp:进程入口,不属于长期领域模型。
//
// 业务意图:启动时打印服务名,并把命令行订单号交给领域类型校验格式。
// 教学点:
// - 编译产物的包路径需相对 classpath 根目录正确布局;
// - public 顶层类名与文件名一致;
// - 此例使用 Java 21 非预览模式的经典 main 入口。

public class CheckoutApp {
    public static void main(String[] args) {
        String orderId = args.length > 0 ? args[0] : "ORD-0";
        // 领域类型:展示“入口类调用其它类”,而非只有 main 文件。
        OrderId id = new OrderId(orderId);
        System.out.println("checkout-ready " + id.value());
    }
}

// 同一文件可有非 public 顶层类;但一个文件最多一个 public 顶层类。
class OrderId {
    private final String value;

    OrderId(String value) {
        // 最小校验:空单号直接失败,避免带病启动。
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("orderId blank");
        }
        this.value = value;
    }

    String value() {
        return value;
    }
}

建议运行(在 shop/app 的上两级目录,即包含 shop 的目录):

javac shop/app/CheckoutApp.java
java shop.app.CheckoutApp ORD-42

期望输出:

checkout-ready ORD-42

结合场景再看三个关注点

  1. 启动名是全限定类名 shop.app.CheckoutApp,不是文件路径。
  2. 启动时 class 文件的包路径必须对齐 classpath 根目录;源码目录是组织约定,javac 显式接收源文件时不强制源码位于对应包目录。
  3. 入口类宜瘦:业务规则放到可测试类型,main 只做装配与启动。

核心概念与准确模型

1. 编译单元与类型

  • 一个 .java 文件是一个编译单元
  • 可包含:包声明、import、类型声明
  • 至多一个 public 顶层类型,且 public 类型名与文件名一致(经典规则)

2. 包(package)

package shop.app;
  • 构成类型全限定名:shop.app.CheckoutApp
  • 约定目录:shop/app/CheckoutApp.java
  • 默认包(不写 package)只适合一次性练习,不适合项目

3. main 入口约定

常见可启动签名:

public static void main(String[] args)

含义拆解:

部分作用
public启动器可访问
static无需先构造实例即可调用
void不通过返回值表达退出码(可用 System.exit,但要克制)
String[] args命令行参数

String... args 也可作为经典 main 参数。Java 11 的源码文件启动模式仍会编译源码再启动,不等同于省略类声明。Java 25 的 compact source files 与实例 main(JEP 512)另允许省略显式类声明,以及符合条件的无参数/非 static main;本篇继续使用经典形式。

4. import

  • import 是源码级名称简写,不“把代码拷进来”
  • 通配符 import 不影响运行时性能(编译期处理),也不自动导入子包;同名类型可能导致歧义。
  • 静态 import 要克制,避免可读性崩溃

5. 从结构到 classpath

运行 java -cp out shop.app.CheckoutApp 时,应用类加载器可从 out/shop/app/CheckoutApp.class 加载该类。命名模块启动则使用 --module-path 配合 -m 模块名/主类,并非把类放上模块路径就自动采用模块启动。更系统的内容见 类路径与模块系统。

边界情况与反直觉行为

  1. 文件名与 public 类不一致 → 编译失败。
  2. class 输出目录未按包名布局 → 启动可能找不到类型;javac -d out 源文件.java 可按包结构组织输出。
  3. 在错误工作目录启动 → Could not find or load main class。
  4. 多个 main → 可以有多个类各有 main;启动时你指定哪一个,哪一个才是入口。

常见误区

[!warning] 常见误区:把 Java 当脚本从上到下执行 错误理解:源码文件启动会像脚本一样执行任意顶层语句。 正确模型:启动器仍按规则选择 main;Java 25 的紧凑源文件有隐式声明的类,并未变成任意顶层语句脚本。 为何易错:从 Python/Shell 迁移时的惯性。

[!warning] 常见误区:main 里堆全部业务 错误理解:所有逻辑写在 main 最省事。
正确模型:main 做启动;业务进可测试类型。
为何易错:教程示例过短,被误认为生产结构。

工程实践

  1. 入口类命名:*App / *Application / *Main,与领域类型分离。
  2. 尽早引入包结构,避免默认包。
  3. 使用构建工具管理多模块后,仍要会手写 javac/java 排障。
  4. 测试不要依赖 main;直接测领域方法。

本节总结

  • Java 以类型与包组织程序
  • main 是 JVM 启动应用的经典约定
  • 分清源文件命名、class 输出包路径与 classpath;用显式路径可避免依赖当前工作目录

自测题

概念题

  1. 为什么 main 通常是 static?

代码推理题

  1. 若 public 类 A 写在 B.java 会怎样?

工程思考题

  1. 微服务里为什么常单独放 Application 启动类?
参考答案
  1. 启动时还没有应用自己构造的实例,启动器需要可直接调用的方法。
  2. 编译错误:public 顶层类名必须与文件名对应。
  3. 把进程生命周期、配置加载与框架启动和领域模型隔离,便于测试与替换入口。

延伸阅读与资料来源

资料类型支撑内容
JLS - Compilation Units规范包、编译单元
java 启动器文档工具文档启动与类查找
JEP 512Java 25 语言变更紧凑源文件与实例 main
创建于 2026/7/15 更新于 2026/10/6
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本