Java 程序结构与 main

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

#type / concept #status / growing #tech / dev #resource / java

[!info] 关联笔记

Java 程序结构与 main

这个概念为什么出现

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

  • 源码组织为编译单元(通常一个 .java 文件)
  • 类型(class/interface 等)是基本结构
  • JVM 启动时需要一个约定入口public static void main(String[] args)

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

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

最小可运行示例

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

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

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

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

// CheckoutApp:进程入口,不属于长期领域模型。
//
// 业务意图:启动时打印服务名,并把命令行订单号交给领域类型校验格式。
// 教学点:
// - package 声明必须匹配目录;
// - public 顶层类名与文件名一致;
// - main 必须是 public static void main(String[]) 这一形态(常见可启动约定)。

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. 包目录必须对齐,否则编译/运行在“找类”阶段失败。
  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命令行参数

注:语言与启动器在演进中可能支持变体(如单文件源码程序);本系列以经典应用入口为主。

4. import

  • import 是源码级名称简写,不“把代码拷进来”
  • 通配符 import 不影响运行时性能(编译期处理)
  • 静态 import 要克制,避免可读性崩溃

5. 从结构到 classpath

运行 java shop.app.CheckoutApp 时,JVM 在 classpath(或模块路径)上按包路径查找 CheckoutApp.class。更系统的内容见 类路径与模块系统

边界情况与反直觉行为

  1. 文件名与 public 类不一致 → 编译失败。
  2. 包声明与目录不一致 → 编译或运行找不到类型。
  3. 在错误工作目录启动Could not find or load main class
  4. 多个 main → 可以有多个类各有 main;启动时你指定哪一个,哪一个才是入口。

常见误区

[!warning] 常见误区:把 Java 当脚本从上到下执行 错误理解:没有类也可以从第一行跑。
正确模型:可执行应用由 JVM 找类并调用 main(或特定启动协议)。
为何易错:从 Python/Shell 迁移时的惯性。

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

工程实践

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

本节总结

  • Java 以类型与包组织程序
  • main 是 JVM 启动应用的经典约定
  • 文件名、类名、包名、工作目录必须一致

自测题

概念题

  1. 为什么 main 通常是 static

代码推理题

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

工程思考题

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

延伸阅读与资料来源

资料类型支撑内容
JLS - Compilation Units规范包、编译单元
java 启动器文档工具文档启动与类查找
创建于 2026/7/15 更新于 2026/7/15