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
结合场景再看三个关注点
- 启动名是全限定类名
shop.app.CheckoutApp,不是文件路径。 - 包目录必须对齐,否则编译/运行在“找类”阶段失败。
- 入口类宜瘦:业务规则放到可测试类型,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。更系统的内容见 类路径与模块系统。
边界情况与反直觉行为
- 文件名与 public 类不一致 → 编译失败。
- 包声明与目录不一致 → 编译或运行找不到类型。
- 在错误工作目录启动 →
Could not find or load main class。 - 多个 main → 可以有多个类各有 main;启动时你指定哪一个,哪一个才是入口。
常见误区
[!warning] 常见误区:把 Java 当脚本从上到下执行 错误理解:没有类也可以从第一行跑。
正确模型:可执行应用由 JVM 找类并调用 main(或特定启动协议)。
为何易错:从 Python/Shell 迁移时的惯性。
[!warning] 常见误区:main 里堆全部业务 错误理解:所有逻辑写在 main 最省事。
正确模型:main 做启动;业务进可测试类型。
为何易错:教程示例过短,被误认为生产结构。
工程实践
- 入口类命名:
*App/*Application/*Main,与领域类型分离。 - 尽早引入包结构,避免默认包。
- 使用构建工具管理多模块后,仍要会手写
javac/java排障。 - 测试不要依赖 main;直接测领域方法。
本节总结
- Java 以类型与包组织程序
main是 JVM 启动应用的经典约定- 文件名、类名、包名、工作目录必须一致
自测题
概念题
- 为什么
main通常是static?
代码推理题
- 若 public 类
A写在B.java会怎样?
工程思考题
- 微服务里为什么常单独放
Application启动类?
参考答案
- 启动时还没有应用自己构造的实例,启动器需要可直接调用的方法。
- 编译错误:public 顶层类名必须与文件名对应。
- 把进程生命周期、配置加载与框架启动和领域模型隔离,便于测试与替换入口。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| JLS - Compilation Units | 规范 | 包、编译单元 |
| java 启动器文档 | 工具文档 | 启动与类查找 |