CodeGym /课程 /JAVA 25 SELF /通过模块实现封装:导出与导入

通过模块实现封装:导出与导入

JAVA 25 SELF
第 60 级 , 课程 2
可用

1. 包的导出:谁能看到你的代码?

在 Java 9 之前的世界,一切都很简单,但并不太安全。只要类是 public,任何人只要能访问 JAR 或 classpath 就能看到它。这会导致内部类,甚至整个包,可能被项目外部意外或有意地使用。是的,可以用 privatepackage-private 隐藏一些东西,但只要类是 public,它就对所有人可见。

有了模块之后,情况发生了变化。现在即使类声明为 public,如果其所在的包没有在 module-info.java 中显式通过 exports 导出,也不能从其他模块使用它。

实践中是什么样

假设你的项目结构如下:

my-app/
  src/
    core/
      com/example/core/
        CoreService.java
        InternalHelper.java
      module-info.java
    app/
      com/example/app/
        Main.java
      module-info.java

在文件 core/module-info.java 中写:

module core {
    exports com.example.core;
}

这样,只有com.example.core 中的类对其它模块可见。位于其他包中的任何内容(例如 com.example.core.internal)都将不可见——即便那里有 public 类。这就是模块化封装。

迷你演示:“可见/不可见”

在 core 内部:

// com/example/core/CoreService.java
package com.example.core;

public class CoreService {
    public void doWork() {
        System.out.println("Work done!");
    }
}

在 app 内部:

// com/example/app/Main.java
package com.example.app;

import com.example.core.CoreService;

public class Main {
    public static void main(String[] args) {
        CoreService service = new CoreService();
        service.doWork();
    }
}

如果你从 core/module-info.java 中移除 exports com.example.core; 这一行,编译时会得到错误:

error: package com.example.core is not visible

即便 CoreServicepublic

2. 依赖导入:requires 与严格隔离

在 JPMS 中,不能随意使用来自其他模块的类。需要通过 requires 显式声明依赖。

示例

在文件 app/module-info.java 中:

module app {
    requires core;
}

现在模块 app 可以使用模块 core 导出的所有内容。

如果你忘了写 requires core;,会得到编译错误:

error: package com.example.core is not visible

或者

error: cannot access CoreService

重要提示

  • requires 在模块级别生效,而不是包级别。
  • 你不能只“导入”某一个包——只能导入整个被导出的模块。

3. 对比:模块 vs public/private

模块引入了新的封装层级,位于类与包之“上”。

级别 控制什么? 如何生效?
private 类内部访问 仅在同一个文件内
package-private(默认) 包内访问 同一包内的所有类
public 对所有可见 任意位置的代码
module 模块之间的访问 仅导出的包

关键理念:

  • 一个类可以是 public,但如果其包未导出,它只在模块内部可见。
  • 通过 exports 导出包,好比为其他模块打开一扇“窗口”。

示例:隐藏的实现

// com/example/core/internal/SecretSauce.java
package com.example.core.internal;

public class SecretSauce {
    public void addMagic() {}
}

如果你没有在 module-info.java 中导出包 com.example.core.internal,任何外部模块都无法使用这个类,即便它是 public

4. 示例: core 与 app —— API 与实现

来看一个常见场景:模块 core 提供 API,模块 app 使用它。

结构:

core/
  com/example/core/
    CoreAPI.java
  com/example/core/impl/
    CoreImpl.java
  module-info.java

app/
  com/example/app/
    Main.java
  module-info.java

core/module-info.java:

module core {
    exports com.example.core; // 仅限 API!
    // 不导出 com.example.core.impl
}

app/module-info.java:

module app {
    requires core;
}

com/example/core/CoreAPI.java:

package com.example.core;

public interface CoreAPI {
    void doSomething();
}

com/example/core/impl/CoreImpl.java:

package com.example.core.impl;

import com.example.core.CoreAPI;

public class CoreImpl implements CoreAPI {
    @Override
    public void doSomething() {
        System.out.println("Doing something!");
    }
}

com/example/app/Main.java:

package com.example.app;

import com.example.core.CoreAPI;
// import com.example.core.impl.CoreImpl; // 这将导致编译错误!

public class Main {
    public static void main(String[] args) {
        // CoreImpl impl = new CoreImpl(); // 错误!该包未导出。
        // 只能使用通过 API 可见的内容。
    }
}

结果:

  • 模块 app 只能看到 core 导出的内容。
  • 实现(impl)被隐藏,即便其中的类是 public

5. 实践:玩转导出与可见性

步骤 1:删除 export

core/module-info.java 中把这一行注释掉:

// exports com.example.core;

现在尝试编译项目。预期在模块 app 中出现编译错误——它将看不到 com.example.core 的类。

步骤 2:只导出 API

exports com.example.core; 这一行加回去,但不要导出 com.example.core.impl。试着在模块 app 中导入来自 impl 的类。你会再次得到编译错误——一切都很合理!

步骤 3:非导出包中的 public 类

在一个未导出的包中创建 public 类。尝试从其他模块使用它——行不通。这就是模块化封装的实际效果。

6. 在 IDE 中以及“形象比喻”的理解

  • 尝试导入未导出包中的类时,IDE 会标红提示错误。
  • 提示会说明该包未由模块导出。
  • 为所需包添加 exports 后——错误就会消失。

示意图:访问级别

+---------------------+
|        模块         |
|  (module-info.java) |
+---------------------+
       |
       v
+---------------------+
|         包          |
|   (package, export) |
+---------------------+
       |
       v
+---------------------+
|         类          |
|   (public/private)  |
+---------------------+
       |
       v
+---------------------+
|      方法/字段      |
|   (public/private)  |
+---------------------+

7. 重要细节与特性

仅对“好友”导出:exports ... to

有时你只想把某个包导出给特定模块(例如测试或特殊扩展):

exports com.example.core.internal to my.special.module, my.test.module;

现在只有这些指定模块能看到该包。

可以导出多个包吗?

可以!只需添加更多 exports 行:

exports com.example.core;
exports com.example.core.api;

可以导出“一切”吗?

不可以。在 Java 的模块系统中,你必须始终显式声明要导出什么。这正是它的强大之处。

8. 使用模块化封装时的常见错误

错误 №1:以为 public 类总是可见。
如果包没有通过 module-info.java 导出,即便类是 public,对其他模块仍然不可见。这是新手最常见的“坑”。

错误 №2:忘了声明 requires。
如果你的模块使用了另一个模块的类,却没有写 requires,就会得到编译错误。务必显式描述依赖。

错误 №3:试图从两个模块导出同一个包。
在 JPMS 中,每个包只能由一个模块导出。如果违反此规则,编译器会阻止你。

错误 №4:结构不正确——module-info.java 放错了位置。
文件 module-info.java 必须放在模块源码的根目录,否则模块无法被识别。

错误 №5:模块之间的循环依赖。
如果模块 A requires B,而 B 又 requires A——就会报错。避免在依赖图中出现环。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION