Java 基础体系 · 第 40/100 篇。示例统一以 Java 25 LTS 为语言和 JVM 基线;框架示例使用与其兼容的现代稳定版本。
Java 25 可见性与包:访问控制、封装、模块边界和依赖方向
在 Java 中,“能不能访问”不是一个单一问题。至少要区分四层边界:
- 语言访问控制:
private、无修饰符、protected、public。 - 包(package)边界:类型和成员是否属于同一个包。
- 模块(module)边界:包所在模块是否被读取,包是否被导出或开放。
- 运行时反射边界:反射或
MethodHandles是否被模块系统允许。
这些边界按顺序叠加。一个类型即使声明为 public,也可能因为所在包没有导出而无法被另一个模块编译使用;一个包即使被 exports,其中的 private 成员也不会因此变成普通可访问成员。
1. 先区分几个核心概念
1.1 类型、成员、访问者
考虑下面的代码:
package com.example.account;
public class Account {
private String owner;
public void rename(String newOwner) {
this.owner = newOwner;
}
}
这里有三个不同的访问对象:
Account是一个顶层类型;owner是Account的字段成员;rename是Account的方法成员。
还需要区分访问者:
Account account = new Account();
account.rename("Alice");
访问者可能是:
- 同一个类;
- 同一个顶层类中的嵌套类;
- 同一个包中的其他类型;
- 不同包中的子类;
- 不同包中的非子类;
- 另一个模块中的类型;
- 运行时反射代码。
Java 的访问检查会同时考虑“被访问成员的修饰符”和“访问发生的位置”。
1.2 import 不会授予访问权限
import com.example.account.Account;
import 只允许使用简单名称 Account,不会改变访问权限。下面两种写法在权限上完全等价:
import com.example.account.Account;
Account account;
com.example.account.Account account;
如果类型不可访问,改成全限定名也不能绕过检查。
2. Java 语言层面的四种访问控制
2.1 private:声明类内部的实现细节
private 成员只能在声明它的顶层类或接口的主体范围内访问:
package com.example.account;
public class Account {
private String owner;
private void validateOwner(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("owner is blank");
}
}
public void rename(String newOwner) {
validateOwner(newOwner);
owner = newOwner;
}
}
private 的直接效果是:
- 同一个类的方法可以访问;
- 同一个类的嵌套类可以访问;
- 同一个包中的其他顶层类不能访问;
- 子类不能访问;
- 其他模块不能访问。
例如:
package com.example.account;
public class AccountPrinter {
public static void print(Account account) {
// 编译错误:owner has private access in Account
// System.out.println(account.owner);
}
}
嵌套类与 private
Java 源代码层面,同一个顶层类内部的嵌套类型可以互相访问私有成员:
public class Outer {
private int value = 42;
static class Nested {
static int read(Outer outer) {
return outer.value;
}
}
public static void main(String[] args) {
System.out.println(Nested.read(new Outer()));
}
}
这不是因为 Nested 和 Outer 属于同一个包,而是因为访问发生在同一个顶层类型的嵌套范围内。现代 JVM 还使用 nest-based access 机制表达这类嵌套关系,但源代码判断应以 Java 语言规则为准。
private 的典型用途不是“防止任何形式的观察”,而是限制实现细节的直接依赖。类可以通过方法控制状态变化,从而维护不变量:
public final class Money {
private final long cents;
private Money(long cents) {
if (cents < 0) {
throw new IllegalArgumentException("negative money");
}
this.cents = cents;
}
public static Money ofCents(long cents) {
return new Money(cents);
}
public long cents() {
return cents;
}
}
调用方无法直接构造一个负数 Money,因为构造器和字段都不是公开的。封装在这里体现为:
如果字段直接公开,调用方可能绕过校验;如果状态只能通过受控操作建立,类就能把合法性检查集中在自身内部。
2.2 无修饰符:包访问权限
成员或顶层类型没有写访问修饰符时,通常称为包私有或默认访问权限:
package com.example.account;
class AccountValidator {
static boolean validOwner(String owner) {
return owner != null && !owner.isBlank();
}
}
它只能被同一个包中的代码访问:
package com.example.account;
public class AccountService {
public boolean accepts(String owner) {
return AccountValidator.validOwner(owner);
}
}
不同包中的代码不能访问:
package com.example.billing;
public class BillingService {
public boolean accepts(String owner) {
// 编译错误:
// AccountValidator is not public in com.example.account
// return AccountValidator.validOwner(owner);
return true;
}
}
包私有常用于把多个类型组成一个实现协作单元:
AccountService暴露业务入口;AccountValidator、缓存、解析器等实现类型只在包内协作;- 包外只能看到公开 API。
但是,包私有不是强安全边界。它主要是 Java 编译器层面的可见性规则,不等价于进程隔离,也不阻止拥有足够权限的调试器、字节码工具或某些运行时机制观察类信息。
顶层类型只有两种访问级别:
public class PublicType {
}
或者:
class PackageType {
}
顶层类型不能声明为 private 或 protected。因为顶层类型没有外层类作为访问范围,private 和 protected 在这里没有相应的语言语义。
2.3 public:语言层面允许更广泛的访问
public 表示该声明在 Java 语言访问控制上不受包范围限制,但它仍然要满足其他条件:
package com.example.account;
public class Account {
public String owner() {
return "Alice";
}
}
一个 public 类型中的 public 方法只有在以下条件都满足时,其他代码才能正常使用:
- 声明它的类型本身可访问;
- 调用方能够解析到该类型;
- 如果跨模块,调用方读取被调用方模块;
- 该类型所在的包对调用方模块导出;
- 调用表达式满足普通 Java 语言规则。
因此下面的类型并不能真正形成公开 API:
package com.example.internal;
public class InternalParser {
public static String parse(String input) {
return input.trim();
}
}
如果 com.example.internal 没有被模块导出,那么它虽然在语言层面是 public,却不能被其他模块正常编译依赖。
2.4 protected:包内访问,或受限的子类访问
protected 有两个独立入口:
- 同一个包内的代码可以访问;
- 不同包中的子类可以访问,但受到额外限制。
package com.example.base;
public class Base {
protected int value = 10;
}
同包代码可以直接访问:
package com.example.base;
public class SamePackage {
public int read(Base base) {
return base.value;
}
}
不同包的子类可以通过自身继承关系访问:
package com.example.derived;
import com.example.base.Base;
public class Child extends Base {
public int readOwnValue() {
return value; // 合法
}
public int readThisValue() {
return this.value; // 合法
}
}
但“是子类”不意味着可以通过任意 Base 对象访问:
package com.example.derived;
import com.example.base.Base;
public class Child extends Base {
public int read(Base base) {
// 编译错误:
// value has protected access in Base
// return base.value;
return this.value;
}
}
跨包子类访问实例 protected 成员时,访问必须通过当前子类类型或其子类型的对象路径,而不能通过一个仅声明为基类类型的任意对象路径完成。
可以用下面的对照理解:
public class Child extends Base {
public int readChild(Child child) {
return child.value; // 合法
}
public int readBase(Base base) {
// return base.value; // 不合法
return this.value; // 合法
}
}
protected 因而不是“对所有子类完全公开”,也不是“只有子类可见”。同包非子类也可以访问,这正是 protected 最容易被误解的地方。
3. 访问控制的完整判断
可以把一个普通 Java 访问粗略表达为:
其中:
- 表示 Java 语言层面的访问条件成立;
- 表示模块系统条件成立。
3.1 语言条件
对于成员访问:
private:访问位置满足私有成员规则;- 无修饰符:访问者与声明者属于同一个包;
protected:访问者在同包,或者是满足限制的子类访问;public:成员本身公开,并且声明它的类型也可访问。
对于顶层类型:
public:语言层面可被其他包引用;- 无修饰符:只能被同包代码引用。
例如:
package com.example.api;
class Hidden {
public static void run() {
}
}
即使 run 是 public,下面的访问仍然失败:
package com.example.client;
import com.example.api.Hidden;
public class Client {
public void call() {
// 编译错误:Hidden 本身不是 public
// Hidden.run();
}
}
访问成员时,不能只看成员修饰符,还必须先检查声明成员的类型是否可访问。
3.2 模块条件
当访问跨越命名模块时,至少要检查:
- 访问方模块是否读取目标模块;
- 目标包是否导出给访问方;
- 目标类型和成员是否满足 Java 语言访问控制。
因此跨模块访问一个公开类型的典型条件是:
其中:
reads来自requires;exports来自目标模块的module-info.java;public来自 Java 类型或成员声明。
同一个模块内部不需要通过 exports 访问自己的包;exports 的作用是向其他模块提供包的普通编译和运行时 API。
4. 包不是目录,也不是模块
4.1 包名与目录结构
下面的文件声明属于包 com.example.account:
package com.example.account;
public class Account {
}
通常文件路径写成:
com/example/account/Account.java
目录结构是编译器和构建工具常用的组织方式,但真正决定 Java 包归属的是 package 声明和编译上下文,不是目录名字本身。
同一个包中的类型可以使用包私有成员:
package com.example.account;
class AccountRules {
static boolean valid(String value) {
return value != null;
}
}
package com.example.account;
public class AccountService {
public boolean accepts(String value) {
return AccountRules.valid(value);
}
}
com.example.account 与 com.example.billing 是不同包,即使它们的前缀相同,也不会因此获得访问权限。包名中的点号没有继承关系:
com.example
com.example.account
com.example.account.internal
这三个包彼此不是父子包。com.example.account.internal 不会自动获得 com.example.account 的包私有访问权。
4.2 包与模块的关系
模块由 module-info.java 描述,模块可以包含多个包:
com.example.account module
├── com.example.account
├── com.example.account.internal
└── com.example.account.persistence
包解决的是:
- 类型命名空间;
- 包私有访问;
- 代码组织。
模块解决的是:
- 模块间依赖;
- 哪些包作为外部 API 导出;
- 反射是否可以深入访问;
- 模块图和封装边界。
一个模块可以包含不导出的内部包:
module com.example.account {
exports com.example.account;
}
这表示:
com.example.account对其他可读取该模块的模块导出;com.example.account.internal如果存在但没有导出,就是模块内部实现;- 同模块内的代码仍然可以使用内部包;
- 其他模块不能正常编译引用内部包。
包私有和模块封装是两种不同层次的限制:
同一包 :可以使用包私有声明
同一模块不同包:只能使用符合语言规则的公开成员
跨模块 :还需要 reads 和 exports
5. JPMS:requires、exports 与 opens
5.1 requires:建立模块读取关系
假设有两个模块:
src/
├── com.example.api/
│ ├── module-info.java
│ └── com/example/api/Api.java
└── com.example.app/
├── module-info.java
└── com/example/app/Main.java
com.example.api/module-info.java:
module com.example.api {
exports com.example.api;
}
Api.java:
package com.example.api;
public class Api {
public static String message() {
return "hello";
}
}
com.example.app/module-info.java:
module com.example.app {
requires com.example.api;
}
Main.java:
package com.example.app;
import com.example.api.Api;
public class Main {
public static void main(String[] args) {
System.out.println(Api.message());
}
}
使用 Java 25 编译和运行:
javac -d out \
--module-source-path src \
-m com.example.api,com.example.app
java \
--module-path out \
-m com.example.app/com.example.app.Main
预期输出:
hello
这里有两条必要关系:
com.example.app --requires--> com.example.api
com.example.api --exports--> com.example.api
如果删除 requires,编译器无法把 com.example.api 作为 com.example.app 的可读模块;如果删除 exports,即使模块被读取,com.example.app 也不能把 com.example.api.Api 作为普通外部 API 使用。
典型错误会类似于:
package com.example.api is not visible
(package com.example.api is declared in module com.example.api,
but module com.example.app does not read it)
或:
package com.example.api is not visible
(package com.example.api is declared in module com.example.api,
which does not export it to module com.example.app)
具体错误文本可能随编译器上下文略有差异,但失败原因分别对应“没有读取”和“没有导出”。
5.2 exports:导出普通 API
可以只导出特定包:
module com.example.api {
exports com.example.api;
}
也可以限定导出对象:
module com.example.api {
exports com.example.api to com.example.app;
}
限定导出称为限定导出。只有指定模块能够把该包作为普通 Java API 使用。
exports com.example.api to com.example.app;
并不表示:
- 该包中的所有成员都变成
public; - 其他模块可以访问
private成员; - 反射可以任意修改私有字段;
- 未被指定的模块自动获得访问权限。
exports 只是在模块边界上允许其他模块进行普通的、受 Java 语言访问控制约束的访问。
5.3 requires transitive:把依赖暴露给 API 使用者
普通依赖:
module com.example.service {
requires com.example.api;
}
表示 com.example.service 自己需要 com.example.api,但使用 com.example.service 的模块不会因为这个声明自动读取 com.example.api。
传递依赖:
module com.example.service {
requires transitive com.example.api;
}
表示读取 com.example.service 的模块也能通过模块图读取 com.example.api。
这通常适用于公共 API 暴露了依赖模块中的类型。例如:
package com.example.service;
import com.example.api.Account;
public class AccountService {
public Account find() {
return null;
}
}
如果 Account 出现在 AccountService 的公开方法签名中,那么 com.example.api 已经成为 com.example.service 的 API 依赖。此时使用 requires transitive 可能是合理的,因为 API 使用者需要解析 Account。
但如果依赖类型只出现在实现内部:
package com.example.service;
public class AccountService {
public void refresh() {
// 内部使用 com.example.api 的实现
}
}
通常不应仅为了方便而使用 requires transitive。否则模块会把本来属于内部实现的依赖扩散给自己的使用者。
6. exports 与 opens 解决不同问题
6.1 opens 允许深度反射
module com.example.account {
exports com.example.account;
opens com.example.account;
}
opens 主要服务于运行时反射框架,例如对象序列化、依赖注入、ORM 映射等。它允许反射代码在满足运行时权限条件时深入访问非公开成员。
也可以限定开放给某个模块:
module com.example.account {
opens com.example.account.persistence
to com.example.orm;
}
6.2 opens 不等于 exports
module com.example.account {
opens com.example.account.internal;
}
这不会让其他模块可以在 Java 源代码中正常写:
import com.example.account.internal.InternalEntity;
因为 opens 不提供普通编译访问。反过来:
module com.example.account {
exports com.example.account;
}
也不会让框架能够访问:
private String passwordHash;
因此:
| 声明 | 普通编译访问 public 类型 |
深度反射访问非公开成员 |
|---|---|---|
| 无声明 | 否 | 否 |
exports p |
是 | 不保证 |
opens p |
否 | 是 |
exports p; opens p; |
是 | 是 |
6.3 反射失败的表现
如果模块没有开放某个包,反射框架尝试执行深度访问时可能出现:
java.lang.reflect.InaccessibleObjectException
例如:
field.setAccessible(true);
在强封装模块环境下可能失败。运维上可以使用:
java \
--add-opens com.example.account/com.example.account.persistence=com.example.orm \
...
但 --add-opens 是运行时启动参数,不是源代码中的模块 API 设计。它适合迁移旧框架、诊断或临时兼容;长期使用会降低模块边界的实际强度。
如果问题是普通 public 类型无法被另一个模块访问,通常应该检查 exports 和 requires,而不是使用 --add-opens。--add-exports 与 --add-opens 也不能混用:
--add-exports:补充普通导出,主要解决公开 API 的模块可见性;--add-opens:补充深度反射开放,主要解决非公开成员反射。
7. 模块路径、类路径与特殊模块
7.1 命名模块
包含 module-info.java 的模块是命名模块。它有明确的:
- 模块名;
requires;exports;opens;uses和provides服务声明。
命名模块的依赖图由模块解析器建立。命名模块不能通过随意访问类路径上的类型来绕过其声明式依赖。
7.2 未命名模块
传统类路径上的类通常属于未命名模块。未命名模块没有显式的 module-info.java,其包不会按照命名模块的方式声明导出和开放。
这会造成一个常见错觉:代码放在类路径上时可以正常访问,改放到模块路径后却失败。原因不是 public 语义改变了,而是模块边界开始参与访问判断。
应用迁移到模块化时,需要重新检查:
类路径上的可见性
≠
模块路径上的可见性
7.3 自动模块
没有 module-info.java、但作为模块路径上的 JAR 使用时,可能形成自动模块。自动模块有模块名推导规则,并具有比显式命名模块宽松的读取和导出行为。
自动模块适合迁移阶段,但不应把它当作最终封装设计。依赖关系、导出范围和模块名都不如显式模块描述精确。
8. 封装不是“把字段改成 private”这么简单
封装是把状态表示和状态变化规则限制在类型内部,并通过受控 API 暴露行为。
下面的设计允许调用方直接破坏不变量:
public class BankAccount {
public long balance;
}
调用方可以写:
account.balance = -100;
改为:
public final class BankAccount {
private long balance;
public long balance() {
return balance;
}
public void deposit(long amount) {
if (amount <= 0) {
throw new IllegalArgumentException("amount must be positive");
}
balance += amount;
}
public void withdraw(long amount) {
if (amount <= 0 || amount > balance) {
throw new IllegalArgumentException("invalid withdrawal");
}
balance -= amount;
}
}
此时合法状态由操作前置条件决定:
private 字段只解决“谁能直接改字段”;方法中的校验、并发控制和事务边界才决定封装是否真正维护了业务不变量。
例如多线程下,即使字段是 private,下面的操作仍可能有竞态:
public void withdraw(long amount) {
if (amount <= balance) {
balance -= amount;
}
}
如果两个线程同时读取相同余额,检查和扣减不是一个原子操作。封装没有自动提供线程安全性。可以使用锁或其他并发原语:
public synchronized void withdraw(long amount) {
if (amount <= 0 || amount > balance) {
throw new IllegalArgumentException("invalid withdrawal");
}
balance -= amount;
}
这里应区分三个问题:
private:谁能访问字段;- 不变量:哪些状态合法;
- 并发安全:多个线程同时操作时状态是否仍然合法。
它们相关,但不是同一个概念。
9. 依赖方向:访问关系不等于架构方向
依赖方向描述的是一个组件是否需要另一个组件的类型、行为或实现。
可以把依赖表示为有向图:
表示 A 依赖 B。例如:
com.example.app
|
v
com.example.service
|
v
com.example.repository
在模块系统中,这通常对应:
module com.example.app {
requires com.example.service;
}
module com.example.service {
requires com.example.repository;
}
依赖方向至少涉及三种不同关系:
- 名称依赖:代码中引用了另一个包或模块的类型;
- 编译依赖:编译时需要目标模块的 API;
- 运行时依赖:运行时需要目标模块、类或服务实现。
访问权限只能回答“某个引用是否被允许”,不能自动保证依赖方向合理。
9.1 public 可能扩大依赖面
package com.example.service;
import com.example.repository.Repository;
public class Service {
public Repository repository() {
return null;
}
}
Repository 出现在公开方法返回值中,于是调用方也必须理解 Repository。即使模块图没有使用 requires transitive,这个公开签名也已经形成了 API 层面的耦合。
更稳定的边界通常是返回服务层自己的类型:
package com.example.service;
public record AccountView(String id, String owner) {
}
public class Service {
public AccountView find(String id) {
return new AccountView(id, "Alice");
}
}
此时仓储实现可以变化,而调用方不需要依赖持久化层类型。
9.2 包私有也会形成隐含耦合
同一个包中的多个类型可以互相使用包私有成员:
com.example.order
├── OrderService.java
├── OrderValidator.java
└── OrderRepository.java
这有利于隐藏包外实现,但包内类型之间可能形成紧密耦合。将所有实现类放入一个超大包,并不会自动得到良好的架构;它只是把边界从“模块外部”缩小到“包外部”。
因此依赖方向需要结合包结构设计:
com.example.order.api
^
|
com.example.order.internal
如果 API 包依赖 internal 包中的实现类型,方向就发生了反转。更稳定的结构通常是:
com.example.order.internal
|
v
com.example.order.api
或者让内部实现依赖一个更小的端口接口:
application -> port <- infrastructure
模块的 exports 可以强化这种边界,但不能替代类型设计。一个导出了内部类型的模块,仍然可能形成不必要的外部耦合。
9.3 模块图中的环
如果:
module A {
requires B;
}
同时:
module B {
requires A;
}
就形成:
A -> B -> A
命名模块的解析要求依赖图满足模块系统的约束;循环依赖通常导致模块解析失败,而不是等到某个方法调用时才失败。
循环依赖的修复不能靠把成员改成 public。常见的结构性修复是:
- 抽取较小的 API 模块;
- 将共同抽象放入第三个模块;
- 使用服务接口与
uses/provides解耦实现; - 将实现依赖反转为接口依赖。
模块声明表达的是组件级依赖方向;访问修饰符表达的是类型和成员级可见性。二者必须同时设计。
10. 访问控制的失败阶段
同一个问题可能在不同阶段表现为不同错误。
10.1 编译期:源代码不可见
例如目标包没有导出:
package com.example.internal is not visible
或者类型本身不是 public:
X is not public in com.example.internal;
cannot be accessed from outside package
此时程序尚未生成可运行的合法引用。优先检查:
package 声明
类型和成员修饰符
module-info.java
requires
exports
编译使用的 --module-source-path 或 --module-path
10.2 链接或运行期:解析到类型但访问检查失败
如果编译产物与运行时模块路径不一致,可能出现:
ModuleResolutionExceptionFindExceptionNoClassDefFoundErrorIllegalAccessError
例如编译时使用了某个版本的模块,运行时却加载了不兼容的模块版本。此时不能只检查源代码,还要检查实际加载的模块:
java --show-module-resolution \
--module-path out \
-m com.example.app/com.example.app.Main
该选项可以帮助观察模块解析结果和依赖关系。
10.3 反射期:深度访问未开放
如果普通代码可访问一个 public 类型,但框架反射访问其私有字段失败,典型错误是:
InaccessibleObjectException
诊断时应先判断框架真正需要什么:
- 只调用公开构造器和方法:检查
exports; - 需要访问私有字段或私有构造器:检查
opens; - 依赖 JDK 内部非公开 API:还可能涉及强封装和迁移问题。
不要用 --add-opens 解决所有模块错误,因为它只能解决深度反射问题,不能替代普通模块依赖或 API 导出。
11. 一个完整的边界判断示例
假设有类型:
模块:com.example.account
包:com.example.account.internal
类型:public class TokenStore
成员:private String token
从另一个模块 com.example.app 访问时,逐步判断:
情况一:包未导出
module com.example.account {
}
结果:
com.example.app 不能把 TokenStore 作为普通 Java 类型导入
即使 TokenStore 是 public,也失败。
情况二:包导出,但成员是 private
module com.example.account {
exports com.example.account.internal;
}
下面的代码仍然失败:
store.token
因为 exports 只解决模块包边界,不改变 private 的语言规则。
情况三:包开放但未导出
module com.example.account {
opens com.example.account.internal;
}
普通源码导入仍然失败,但满足条件的反射代码可能访问 token。
情况四:包限定导出给其他模块
module com.example.account {
exports com.example.account.internal to com.example.app;
}
只有 com.example.app 能够普通访问其中符合 Java 语言规则的公开类型和成员。另一个模块 com.example.test 仍然不能依赖该包的公开 API。
这四种情况说明,访问权限是逐层收敛的:
public
↓
包被导出
↓
访问方读取模块
↓
成员满足 Java 语言规则
↓
如涉及反射,再检查 opens
其中任意一层不满足,都可能导致访问失败。
12. 常见误解与边界
12.1 “public 就等于任何地方都能用”
错误。public 只解决语言层面的声明访问。跨命名模块时还要满足 requires 和 exports。
12.2 “同名包就是同一个包”
不能这样假设。包还处在编译环境、模块和类加载环境中。JPMS 也限制命名模块之间的拆分包。一个包名的字符串相同,不代表不同模块中的代码可以共享包私有权限。
12.3 “子包可以访问父包的包私有成员”
错误。包之间没有父子访问关系:
com.example.order
com.example.order.internal
是两个独立包。
12.4 “protected 只对继承者开放”
不完整。同包中的非子类也能访问 protected 成员;跨包时才需要满足子类访问规则。
12.5 “opens 等于 exports”
错误。exports 面向普通 Java API 访问,opens 面向深度反射。一个不能替代另一个。
12.6 “改成反射就能绕过所有权限”
错误。模块系统会参与反射访问检查。没有适当的开放声明或启动参数,深度反射可能失败;即使反射成功,也不代表这样的访问适合作为稳定 API。
13. 设计时如何让依赖方向可见
可以从外到内逐层设置边界:
模块:限制组件之间的依赖
↓
包:区分公开 API 和内部实现
↓
类型:限制可被创建和继承的对象
↓
成员:限制状态和操作入口
一个典型模块声明可以是:
module com.example.order {
exports com.example.order.api;
requires com.example.payment.api;
}
目录结构:
com.example.order
├── module-info.java
├── com/example/order/api
│ └── OrderService.java
└── com/example/order/internal
├── OrderValidator.java
└── DefaultOrderService.java
这里的意图是:
com.example.order.api是外部调用入口;com.example.order.internal是模块实现;- 外部模块不能直接依赖内部包;
- 模块只声明自己真正需要的支付 API;
- 包私有成员继续用于实现类之间的内部协作。
如果框架需要实例化内部实现,再针对特定框架模块开放必要包:
module com.example.order {
exports com.example.order.api;
opens com.example.order.internal to com.example.di;
requires com.example.payment.api;
requires com.example.di;
}
这种声明比整个模块无条件 open 更精确,因为它保留了普通 API 和反射入口之间的区别。
14. 最终判断模型
遇到“为什么这个类型或成员不能访问”时,可以按以下顺序推导,而不是直接尝试增加修饰符:
-
它是什么?
顶层类型、成员、嵌套类型,还是反射目标? -
访问发生在哪里?
同一个类、同一个顶层类、同一个包、子类、不同包,还是不同模块? -
语言规则是否满足?
检查private、包私有、protected、public,以及声明类型自身的可访问性。 -
是否跨模块?
如果跨模块,访问方是否requires目标模块? -
目标包是否导出?
普通源码访问需要exports;限定导出还要检查目标模块是否在允许列表中。 -
是否是深度反射?
如果访问私有成员,检查opens、限定开放或--add-opens。 -
依赖方向是否合理?
如果为了解决访问问题不断扩大public、exports或requires transitive,可能是在掩盖包结构或模块职责设计问题。
Java 25 中,访问修饰符、包和模块分别承担不同层次的边界职责。private 控制类型内部实现,包控制局部协作范围,模块控制组件之间的可见 API,opens 控制特殊的运行时反射入口。只有把这些规则和依赖方向一起设计,封装才不只是语法上的隐藏,而会成为可以被编译器、模块解析器和运行时共同验证的结构约束。
系列导航与关联阅读
- 系列入口:Java 完整学习路线:从 Java 25 语言与 JVM 到 Spring、微服务和生产交付
- 上一篇:Java 25 对象初始化:字段、初始化块、构造器、继承与发布安全
- 下一篇:Java 25 Record 完整指南:值对象、紧凑构造器、验证和序列化
官方资料
本文依据 Java、Spring 与相关项目官方文档重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论