模块路径与类路径是java 9+中两套互斥分层的加载策略:模块路径启用模块模式并主导语义,类路径退化为匿名模块仅作兼容;前者加载具名/自动模块并强制依赖与封装,后者无模块约束且不可被requires。

类路径(-cp 或 CLASSPATH)和模块路径(--module-path)不是并列的“两个搜索目录”,而是代表两套互斥又分层的加载策略。理解它们的优先级,关键不在于“谁先查”,而在于“谁被允许查”以及“查到后怎么用”。
模块路径具有语义主导权,类路径退居兼容层
从 JDK 9 起,JVM 启动时若指定了 --module-path(哪怕为空),就进入“模块模式”;此时类路径上的所有内容自动归入一个特殊模块——匿名模块(Unnamed Module)。它能读取所有具名模块导出的包,但自身不提供模块化封装,也无法被其他具名模块直接 requires。
- 模块路径上的 JAR 若含
module-info.class,即为具名模块,由平台类加载器或系统类加载器按模块图解析加载 - 类路径上的类即使同名、同包、同版本,也无法覆盖模块路径中已导出的类——双亲委派机制下,模块系统会优先委托给模块类加载器完成解析
- 如果同时用了
--module-path和-cp,JVM 不会合并两者,而是把类路径“降级”为匿名模块的来源,不再参与模块依赖解析
加载顺序不等于覆盖顺序:双亲委派仍起决定作用
即便你把自定义的 java.util.List 放进 -cp,它也不会生效。因为 java.util.* 属于 java.base 模块,由启动类加载器(Bootstrap)在模块路径中加载;应用类加载器收到加载请求时,必须先委托给父加载器,而 Bootstrap 已能成功返回标准实现。
- 启动类加载器(Bootstrap)负责
java.base等核心模块,路径由 JVM 内置锁定,-Xbootclasspath可干预但极危险 - 平台类加载器(Platform)加载
java.logging、java.xml等扩展模块,来自模块路径 - 系统类加载器(AppClassLoader)只在父加载器无法加载时,才尝试从类路径(匿名模块)中找类
冲突场景的真实表现:不是“谁在前谁生效”,而是“谁有资格被选”
假设你有:
-
libs/my-utils.jar(含module-info.class,声明为my.utils模块,导出com.example.StringUtils) -
classes/com/example/StringUtils.class(在-cp classes中,无模块信息)
当你写 import com.example.StringUtils; 并编译运行时:
- 如果模块
my.utils被requires,且该包被正确导出,则加载的是模块内的版本 - 如果没声明依赖,或该模块未导出此包,则编译失败(模块封装阻止访问)
- 即使你删掉模块 JAR,仅留
-cp classes,加载的仍是匿名模块里的StringUtils,但它无法被任何具名模块直接引用
实用判断口诀
遇到类找不到或版本不对,先问三个问题:
- 目标类是否属于某个 JDK 内置模块?→ 查
java.base等是否已导出 - 你的代码是否在具名模块中?→ 看有没有
module-info.java,有没有requires - 类是从
--module-path还是-cp加载的?→ 运行时用MyClass.class.getModule()打印看是否为null(null表示来自匿名模块)
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











