你编写了几个 Java 类,一切都很顺利。然后你又编写了几个。接着你的项目就有 200 个文件,你再也找不到任何东西了。两个类拥有相同的名称,编译器开始报错。这正是包所解决的问题。

Java 中的是一个命名空间。它将相关的类、接口和枚举分组到类似文件夹的结构中。Java 中的每个类都存在于某个包中。即使是简单的 HelloWorld 类也有一个包,即使你没有显式地编写它。它会被放入臭名昭著的“默认包”中,这对于学校项目来说没问题,但对于任何实际项目来说都非常糟糕。

Oracle 官方教程解释道:“包是对相关类型的分组,提供访问保护和命名空间管理。”这是一种教科书式的说法,意思是“包防止你的代码自相冲突。”

为什么包存在

三个实际原因。

首先,名称冲突。你编写了一个 User 类。你导入的某个库也拥有一个 User 类。如果没有包,Java 无法区分它们。使用包后,一个是 com.yourcompany.model.User,另一个是 org.somelibrary.model.User。它们可以和平共存。

其次,访问控制。Java 有四种访问级别:private、package-private(默认)、protected 和 public。初学者最容易忽略 package-private。它表示“对同一包中的任何类可访问,但对包外不可访问”。这不是 bug,而是一种设计工具。你可以对外部世界隐藏实现细节,同时让它们对包内的相关类可见。

第三,可发现性。当所有与账单相关的类都位于 com.yourcompany.billing 中时,新开发者就知道去哪里查找。这与在计算机上将文件组织到文件夹中的逻辑相同。

命名约定

Java 包名采用反向域名约定。如果你拥有 jamilur.dev,你的包名以 dev.jamilur 开头。如果你在拥有 example.com 的公司工作,则以 com.example 开头。

官方 Java 文档推荐:“包名使用全小写以避免与类或接口名称冲突。”

以下是真实项目的示例。

  • java.lang -- 核心 Java 语言类
  • java.util -- 工具类,如集合
  • org.springframework.boot -- Spring Boot
  • com.mysql.cj.jdbc -- MySQL JDBC 驱动程序
  • dev.jamilur.ecommerce.model -- 假设的电商模型

每个点代表一个目录。dev.jamilur.ecommerce.model 在磁盘上映射为 dev/jamilur/ecommerce/model/

声明包

将其放在 Java 文件的最顶部,任何 import 之前。

package dev.jamilur.bkash.app;

Enter fullscreen mode Exit fullscreen mode

就是这样。文件必须位于与包名匹配的文件夹结构中:dev/jamilur/bkash/app/

如果你忘记了包声明,Java 会将你的类放入默认包。不要在真实应用程序中这样做。默认包无法被命名包中的类导入,这意味着你无法在项目其他部分使用自己的代码。Baeldung 对为什么默认包在生产环境中很危险有很好的解释。

真实示例

让我们为孟加拉电商应用构建一个小型项目结构。

src/
  dev/
    jamilur/
      ecommerce/
        model/
          Product.java
          Customer.java
          Order.java
        service/
          PaymentService.java
          ShippingService.java
        controller/
          OrderController.java

Enter fullscreen mode Exit fullscreen mode

Product.java 文件以如下开头:

package dev.jamilur.ecommerce.model;

public class Product {
    private String name;
    private double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }
}

Enter fullscreen mode Exit fullscreen mode

PaymentService.java 文件以如下开头:

package dev.jamilur.ecommerce.service;

import dev.jamilur.ecommerce.model.Order;

public class PaymentService {
    public boolean processPayment(Order order) {
        // payment logic here
        return true;
    }
}

Enter fullscreen mode Exit fullscreen mode

注意 import 语句。当你想使用另一个包中的类时,需要导入它。Order 类位于 model 包中。PaymentService 类位于 service 包中。import 语句桥接了这个差距。

导入与通配符

你有三种导入方式。

显式导入(大多数时候使用这种)。

import dev.jamilur.ecommerce.model.Order;
import dev.jamilur.ecommerce.model.Customer;

Enter fullscreen mode Exit fullscreen mode

通配符导入(方便但有争议)。

import dev.jamilur.ecommerce.model.*;

Enter fullscreen mode Exit fullscreen mode

这会导入 model 包中的所有类。它节省了输入。但缺点是会失去清晰度。查看你文件的读者无法知道你具体使用了哪些类。包括Google 的 Java 风格指南在内的许多风格指南都建议不要使用通配符导入。你的 IDE 很可能会自动展开它们。

静态导入(很少使用,需谨慎)。

import static java.lang.Math.PI;
import static java.lang.Math.sqrt;

Enter fullscreen mode Exit fullscreen mode

现在你可以写 PI 而不是 Math.PI,写 sqrt(16) 而不是 Math.sqrt(16)。这对常量和工具方法很方便,但如果过度使用可能会使代码更难阅读。

包私有访问

以下是大多数教程跳过的访问修饰符。

当你声明类、方法或字段时不使用任何访问修饰符(没有 public、没有 private、没有 protected),它就会获得包私有访问权限。同一包中的任何内容都可以看到它,包外的内容则无法看到。

package dev.jamilur.ecommerce.service;

class PaymentGatewayHelper {
    // no 'public' keyword
    // only classes in the same package can use this
    String encryptData(String raw) {
        return "encrypted_" + raw;
    }
}

Enter fullscreen mode Exit fullscreen mode

这对于不应成为公共 API 一部分的内部辅助类很有用。Spring 大量使用这种模式。你可以用 @Transactional 注解一个服务方法并将其设为包私有,Spring 的代理仍然可以拦截它,因为它位于同一包中。但外部代码无法直接调用它。

包与模块

Java 9 引入了模块,模块就像增强版的包。一个模块捆绑多个包,并声明它导出哪些包以及需要哪些其他模块。

日常项目不需要模块。包对于大多数应用程序来说已经足够。模块增加了复杂度,只有在大型系统中才值得。Java 平台模块系统(JPMS)在官方 JMod 文档中有介绍,如果你想稍后探索的话。

初学者常见困惑

包名会影响性能吗?不会。包纯粹是组织性的。它们没有运行时成本。

不同包中的两个文件可以拥有相同的类名吗?可以。这正是包存在的主要原因之一。com.example.Usercom.other.User 是两个完全不同的类。

我必须匹配包和文件夹结构吗?是的,对于标准 Java 编译。编译器期望如此。Maven 和 Gradle 等现代构建工具会强制执行这一点。

我可以稍后更改包名吗?可以,但这会破坏所有 import 和所有引用。你的 IDE 可以自动重构它。但在共享团队中,更改包名会产生合并冲突和混乱。请尽早选择你的包结构。

快速总结

  • 是一个将相关 Java 类型分组的命名空间。
  • 包名遵循反向域名约定:com.example.project.module
  • 包声明是 Java 文件中的第一行。
  • 你使用 import 关键字从其他包导入类。
  • 为了清晰,优先使用显式导入而非通配符导入。
  • 包私有访问(无修饰符)仅允许在同一包内访问。
  • 包映射到文件系统上的目录。
  • 模块(Java 9+)是包之上的额外层,用于大型系统。

包并不令人兴奋。但它们是可扩展项目与一堆无人能导航的文件之间的区别。尽早正确设置包结构可以避免日后头疼。

基于 dev.java/learn:Packages