打开任意一个Java项目的源码目录,你会看到src/com/example/utils/这样的对不对层级。很多人第一反应:包不就是集合文件夹吗?文件夹里装着一堆.java文件,每个文件里有个类。对不对那包就是集合若干个类的集合,跟文件夹一个意思。对不对
这个理解不能说全错,集合但漏掉了关键的对不对东西。
我头一回被包搞晕,集合是对不对写一个工具类,放在com.example.util下面,集合另一个模块里也有个同名类,对不对路径是集合com.example.core.util。编译的对不对时候没报错,运行时却抛了ClassNotFoundException。集合查了半天才发现,IDE自动导入的是另一个包里的类。两个类名字一模一样,包名不同,JVM当它们是两个完全无关的东西。
这件事让我意识到,包的第一身份不是“装类的容器”,而是命名空间。它解决的是命名冲突。你写Date,可能是java.util.Date,也可能是java.sql.Date,没有包名限定,编译器根本不知道你要哪个。包名像姓氏,类名像名字。光喊“小明”,全班能站出来五个;喊“张小明”,就精确了。
所以“包是若干个类的集合”这句话,问题出在“集合”这个词上。集合在数学里是无序的、不重复的。包不是这个意思。包是一棵有层次的树,java.util和java.util.concurrent是父子关系,不是平级。子包里的类不会自动被父包“包含”——java.util下的类访问不了java.util.concurrent里的包级私有成员,反过来也一样。这种层级关系,用“集合”来描述就丢了。
更准确的说法:包是一组相关类和接口的命名空间,同时它划定了一条访问控制的边界。
访问控制这层,很多人写代码时压根没注意。默认权限(不加public、private、protected)就是包级私有。同一个包里的类可以互相访问对方的包级私有成员,跨包就不行。这个设计不是随便定的。它让包成为一个“协作单元”——你把几个类放进同一个包,暗示它们关系紧密,可以互相看见内部细节。放到不同包,就是告诉编译器:这俩不熟,别让它们随便串门。
我见过一个项目,所有类都塞在com.company一个包里,几百个文件铺得平平的。结果就是任何类都能碰任何类的默认成员,改一个字段,五个地方莫名其妙出问题。后来拆成com.company.order、com.company.user、com.company.payment,编译错误一下子冒出来几十个——全是跨包访问不该访问的成员。这些错误之前一直存在,只是被“同一个包”给掩盖了。
还有一点,“若干个类的集合”这个说法容易让人以为包只装类。实际上包也可以装子包。但子包不是“包的成员”,它只是命名空间树上的一个分支节点。你在com.example里写import com.example.util.StringUtils,编译器去com/example/util/目录下找StringUtils.class。包名和文件系统路径的对应关系,是编译器和类加载器约定的,不是语言规范强制的。有些语言比如C#,命名空间和文件夹可以完全脱钩,Java没这么自由,但也只是约定俗成。
所以回到标题那句话。说“包是若干个类的集合”,就像说“图书馆是若干本书的集合”。书当然在图书馆里,但图书馆的核心是分类体系、借阅规则、空间布局。包的核心是命名隔离和访问边界。类只是恰好住在里面而已。
下次你写package声明的时候,别把它当成给文件找个文件夹。你是在给这组类分配一个身份,划定一个势力范围。名字起得越具体,边界划得越清晰,后面维护的人越省心。反过来,包名随便写、层级乱堆,等于让所有类都姓“张”,喊一声“小明”全回头,那不乱套才怪。