读完这篇,你能做什么
能组织多包源码、设置输出根目录,并避免把请求状态存入共享字段。
学习理由、准备要求与知识点
为什么学它
包名、访问权限与共享成员决定项目组织和对象状态边界。
先读这些内容
知识点清单
- 完整类名与包目录
- import 与 classpath
- 四类访问权限
- protected 跨包约束
- static 共享成员
- 实例方法与 this
final的不同作用- 引用固定与对象可变
运行前先确认Java 17;源码在本机 Microsoft OpenJDK 17.0.20.1 src.zip 核验,在线链接为上游 jdk-17.0.20-ga。深入模块可在掌握基础后回读。
01先弄懂它是什么
包用来区分和组织类名。例如 package learn.notes; 中的公开类 Demo,完整名字是 learn.notes.Demo。把文件放在 src/learn/notes/Demo.java 后,可用 javac -d out src/learn/notes/Demo.java 编译,再用 java -cp out learn.notes.Demo 运行。
类路径要指向输出根目录 out。import 只是让源码中能用较短名字,不会复制类,也不会把整个包一次性加载进来。
访问权限说明哪些代码能直接使用某个成员。public 允许能访问该类型的调用者使用;成员不写访问修饰符时通常只允许同包访问;private 保护类型内部实现;protected 还提供与继承有关的访问。跨包子类使用 protected 时,对访问位置和所操作对象的类型仍有限制,不能理解为子类能随意访问任何父类对象的受保护成员。
static 成员属于对应已加载类的状态或行为,实例成员则关联具体对象。静态方法没有 this,不能直接读取某个未指定实例的字段。final 变量限制重新赋值,final 方法禁止重写,final 类禁止继承;三者针对不同边界。final List 仍可以指向可变列表,static final 也不自动意味着深度不可变或适合保存当前请求数据。
文中这几个词,先认一认
- 完整类型名
- 把包名和类名连起来的名字,例如
learn.notes.Demo,用来避免同名类混淆。 - 包访问
- 成员不写访问修饰符时通常采用的规则:同一个包里的代码可以访问。
- 静态成员
- 与已加载的类关联的成员,不是每创建一个对象就单独拥有一份。
- 编译期常量
- 例如
finalintDAYS=7:变量是基本类型、用final固定,并且初始值由编译期可确定的常量表达式给出。基本类型或String的final变量还得满足这个初始化条件;只写final、但值要运行时调用方法取得,就不算。
02看看它怎么工作
源代码的包声明决定类型命名,目录对应包结构是常见工程约定;编译器的 -d 输出参数会依据包名组织产物目录。运行时依据类路径根与完整名寻找类,所以从包目录内部随意启动容易把根位置弄错。排错时列出完整名、输出根和实际 class 路径,三者应该能够互相推导。
访问权限服务于维护边界,而不是替代身份认证。把方法改成 private 可以减少普通代码直接调用,却不能说明外部网络请求已经授权。模块系统还可能对跨模块访问增加边界,因此 Java17 中 public 并不是绕过一切封装的通行证。反射篇会讨论合法访问,不采用强行开启所有模块来掩盖设计问题。
static 字段由同一个已加载类的多个实例共享,但不能一概说“整个进程只有一份”。应用、插件或隔离测试可能使用不同类加载器,分别加载出同名类,各自保存一份静态状态。另外,有些常量会在编译时直接写进调用方的代码。因此改了库里的常量后,如果只替换库文件,旧调用方未必看到新值。需要运行时调整的配置,应和这类常量分开设计。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["package learn.notes"] --> B["完整名 learn.notes.Demo"]
B --> C["javac -d out 组织产物"]
C --> D["out/learn/notes/Demo.class"]
D --> E["classpath 指向 out"]
E --> F["java 以完整类名启动"]
B --> G["访问控制再决定成员可用性"]按顺序看,每一步做了什么
- 写明包声明与完整类型名。
- 让源码目录与包结构保持一致。
- 从输出根配置 classpath。
- 按最小需要选择成员可见性。
- 区分静态共享、引用
final与不可变值。
03对照实现代码
Microsoft OpenJDK 17.0.20.1;java.compiler/javax/lang/model/SourceVersion.java · javax.lang.model.SourceVersion#isName(CharSequence,SourceVersion)
以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。
import javax.lang.model.SourceVersion;
public final class SourceModel {
private SourceModel() {} // 工具类无需实例状态。
public static boolean packageShape(String name) {
if (name == null) return false;
// 负 limit 保留末尾空段,才能拒绝以点结尾的名字。
for (String part : name.split("\\.", -1)) {
if (!SourceVersion.isIdentifier(part)
|| SourceVersion.isKeyword(part, SourceVersion.RELEASE_17))
return false;
}
return true;
}
}真实 SourceVersion.isName 将点分名字拆为各段,对每段检查标识符规则与指定源版本的关键字限制。模型固定 RELEASE_17 并把 null 定义成 false,避免把最新运行时语言规则误当成文章目标版本。它只模拟普通名称形状校验,不证明这个包或类型实际存在。
split 的负 limit 保留尾部空段,因此 com.demo. 不会被当成 com.demo。中间连续两个点、开头点和空字符串也会形成非法段。看似小小的参数决定边界输入是否被完整检查,源码阅读时不应只注意主循环而忽略库调用的具体重载与参数。
Java 标识符不是只有 ASCII 字母、数字和下划线,真实字符规则包含 Unicode,并对起始位置与后续位置作不同限制。模型调用公开 SourceVersion 方法保持这项语义,而不自制一个过窄正则。业务命名约定可以主动收窄为小写英文,但应承认那是工程规则而非语言全部能力。
名字本身符合格式,不代表放进任何 Java 位置都合法;受限标识符、模块、类型解析和访问控制还需要编译上下文。静态检查工具要说明只查了哪一层,不能把一个返回真假值的名称检查当成 javac。这个例子也展示了工具类用静态方法和私有构造器的常见写法。
代码先调用谁,再调用谁
isName获取字符串表示- 按点拆分并保留空段
- 逐段调用
isIdentifier与版本化isKeyword - 返回词法名称结果,后续解析仍由编译上下文完成
04动手写一个例子
求多个包名的最长公共包前缀
给定一组 Java17 点分名称,返回按完整段比较的最长公共前缀。每项先做 SourceVersion 名称校验;空输入返回空字符串,非法名称拒绝。本题只处理命名结构,不解析真实项目。
- 输入 1
com.demo.api;com.demo.service- 输出 1
com.demo
- 输入 2
com.a;company.a- 输出 2
空字符串
- 输入 3
[]- 输出 3
空字符串
直接比较字符前缀会把 com.a 与 company.a 的相似字符误当成共同包,因此应以点分段为单位。先保存第一个名字的段数组,令共同段数等于其长度;以后每个名字只与仍然共同的前缀比较,一旦某一段不同就缩短共同段数。
共同段数只减不增,表示已经检查的全部名字共享哪些完整段。即使它降到零,也应继续验证后面的输入是否合法,因为接口承诺拒绝任意非法名称;为了提前返回空结果而跳过校验,会使错误处理依赖输入顺序。
最终用点连接前共同段,空输入与完全无公共段都返回空字符串。本题不把空字符串当成可输入的默认包名,因为默认包与普通点分包名的工程用途不同。需要支持默认包时应显式扩展契约,而不是让空输入、空名称和无共同前缀三种情况混在一起。
为什么这样写是对的处理完每个名字后,common 是已处理全部名字共同拥有的最长完整前缀段数。
每个名称进行词法校验与段拆分;比较只读取其前缀且不会超出该名称长度,因此累计扫描受总字符量约束。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.Arrays;
import javax.lang.model.SourceVersion;
public class Main {
static String commonPackage(String... names) {
if (names == null) throw new IllegalArgumentException("names");
String[] prefix = null;
int common = 0;
for (String name : names) {
// 即使公共前缀已为空,也继续验证剩余输入。
if (name == null || !SourceVersion.isName(name, SourceVersion.RELEASE_17))
throw new IllegalArgumentException("invalid name");
String[] parts = name.split("\\.", -1);
if (prefix == null) { prefix = parts; common = parts.length; }
else {
int i = 0;
// 只按完整段比较,防止把相似字符当成相同包。
while (i < common && i < parts.length
&& prefix[i].equals(parts[i])) i++;
common = i;
}
}
return prefix == null ? "" : String.join(".", Arrays.copyOf(prefix, common));
}
static void check(String wanted, String... names) {
if (!wanted.equals(commonPackage(names))) throw new AssertionError(wanted);
}
static void reject(String... names) {
try { commonPackage(names); } catch (IllegalArgumentException e) { return; }
throw new AssertionError("must reject");
}
public static void main(String[] args) {
// 正常、相似字符、空输入、相同名称和空段边界。
check("com.demo","com.demo.api","com.demo.service");
check("","com.a","company.a");
check("");
check("learn.notes","learn.notes","learn.notes");
reject("com..demo");
reject("com.demo.");
reject("a","b","class");
System.out.println("PASS 7");
}
}这些特殊情况也要试一试
- 空输入合法,输入中的空名称非法。
- 相同字符前缀不等于相同包段。
- 末尾点必须保留并识别为空段。
- 公共前缀归零后仍验证剩余名称。
进阶选读 · 可选深入:用前缀树建立包索引
场景大量类型需要按包层级导航和查找祖先。
做法把每个包段作为树边,节点保存直接类型与子包;明确大小写和模块归属,删除时按引用数量清理空节点。
复杂度构建与查询成本通常随总段数或路径深度增长,存储为节点与边数量。
好处与代价索引更快但需同步增删与版本变化,单次求公共前缀不必提前建树。
05项目里怎么用
默认先给成员较窄的可见性,再根据真实调用关系开放。public API 一旦被其它模块依赖,改名、删除或改变约定的成本都会增加。包可以帮助聚合同一职责,但不能只按照所有 controller、所有 service 的机械分类判断边界,还应考虑哪些实现细节应共同隐藏。
当前请求、用户身份和临时构建结果不适合放入随意共享的 static 可变字段。不同请求会互相覆盖,测试之间也可能污染。共享缓存或配置确实可以是静态或单例管理,但必须有明确生命周期、更新策略和并发保证;static 本身不提供这些机制。
固定协议值、部署配置和不可变对象要分开设计。真正固定的值可以做编译期常量,环境地址应由配置提供;集合即使作为常量,也要检查容器和元素是否仍可修改。final 只能限制对应的重新赋值,不能替你复制数据、同步线程或检查访问权限,评审时要看它具体保护了什么。
容易踩的坑
- 把包目录当成 classpath 根。
- 认为 import 会复制或加载整个包。
- 把
final引用当作深度不可变。 - 将请求私有数据保存到 static 可变字段。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 包访问实现 | 限制依赖范围且方便同包协作 | 跨包复用受限 | 模块内部细节 |
| 公开接口 | 调用契约清晰且可跨边界 | 维护兼容成本增加 | 稳定对外能力 |
| 静态工具方法 | 没有实例状态且调用直接 | 依赖替换较难,不适合隐藏可变环境 | 纯函数与稳定通用计算 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01为什么 java -cp out/learn/notes Demo 找不到预期类?
展开参考答案与推导
包声明使二进制名字为 learn.notes.Demo,查找根应是 out,并使用完整名字启动。把根移进包目录再用短类名,会与 class 文件记录的名字不一致。
02static final List 是否一定不能 add?
展开参考答案与推导
不一定。static 描述共享成员,final 限制字段重新绑定;列表能否 add 取决于具体容器。即使容器不可修改,内部元素也可能可变,需要另外说明。
03名称合法是否证明类型可访问?
展开参考答案与推导
没有。词法名称校验不解析类路径、模块导出或成员访问权限。要知道某次引用是否合法,仍需在完整编译上下文中解析目标并检查访问控制。
换个情况,还会处理吗
01改了库中的常量,旧应用仍读到旧值,可能为什么?
展开参考答案与推导
满足编译期常量条件的值可能已经内联到调用方产物。应重新编译依赖方并核对发布版本;会变化的环境参数应作为配置读取,不能依靠替换常量库实现动态更新。
02两个请求拿到了对方用户信息,先查什么共享状态?
展开参考答案与推导
检查 static 字段、单例实例字段及共享缓存中是否保存了请求专属数据。定位写入和读取的完整链路,再调整作用域;仅给变量加 final 不会修复跨请求共享。
03只想开放给同包使用,需要 public 吗?
展开参考答案与推导
通常包访问即可满足同包协作,减少对外契约。具体仍要看模块与嵌套类型关系;先按真实调用者选择最小可见性,而不是为了省编译错误把所有成员公开。
写代码试一试
01增加前缀本身也是输入的测试
- 输入 / 条件
a.b;a.b.c- 输出 / 预期
a.b
提示 比较不能越过较短名称。
展开参考答案与推导
加入 check("a.b","a.b","a.b.c")。公共段数应保持二,不能因为后一项更长就扩大;不变量规定它只减少或不变,不会在后续名字中重新增长。
02把工程包名限制为小写 ASCII
- 输入 / 条件
Com.demo- 输出 / 预期
拒绝
提示 区分工程约定与语言规则。
展开参考答案与推导
在语言名称校验后增加项目约定检查,例如每段只接受小写字母开头和允许字符集合。文档应写明这是项目规范,不要宣称大写或 Unicode 包名违反 Java 语言。
03实现不可实例化常量工具类
- 输入 / 条件
new Names()- 输出 / 预期
编译失败
提示 构造器 private。
展开参考答案与推导
声明 final 类并提供 private 构造器,把纯函数设为 static。不要因此放入可变请求数据;不可实例化只是对象创建边界,并没有自动建立线程安全或不可变性。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能组织多包源码、设置输出根目录,并避免把请求状态存入共享字段。
本篇重点加练
重点练类加载入口与 final 的具体限制,减少仅凭关键字推断运行行为。
- import 与 classpath
final的不同作用
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01排错实验
在练习目录建立两个包,各含一个同名 Note 类,分别从终端和 IDE 编译运行;记录完整类名、源文件目录、输出目录和 classpath 的对应关系。
- 提交内容
- 目录树、源代码、自写调用检查及命令记录。
- 怎样算完成
- 包名与磁盘路径职责区分
- 同名类引用可明确解析
练到的知识点完整类名与包目录 · import 与 classpath
- 作业 02代码推演
设计
p.base.Parent、同包 Peer 与跨包 Child,在每类中尝试访问四种权限成员;对 protected 用不同静态类型的接收者分别编译验证。- 提交内容
- 独立编译用例和权限矩阵。
- 怎样算完成
- 测试包含跨包继承接收者条件
- 失败样例不与其他错误混杂
练到的知识点四类访问权限 · protected 跨包约束
- 作业 03代码推演
分别对
final局部整数、final对象引用和final方法设计一项允许操作及一项受限制操作;每个失败样例单独编译。- 提交内容
- 关键字作用对象矩阵与编译证据。
- 怎样算完成
- 限制指向具体语法对象
- 不由一个样例概括全部
final
练到的知识点final 的不同作用
综合应用
- 作业 04原理问答
一个计数器类同时有实例计数和 static 总计数,创建两个对象并改变各自状态;画出成员归属并说明 this 在实例方法和 static 上下文中的使用边界。
- 提交内容
- 对象图、代码样例和推演记录。
- 怎样算完成
- 类级与实例级状态分开
- 调用形式不替代实际归属
练到的知识点static 共享成员 · 实例方法与 this
- 作业 05编程实践
实现配置对象,
final字段保存一个可变数组;先写出是否允许调用方修改内容的合同,再实现构造与读取方法,并测试输入数组和返回值被修改后的行为。- 提交内容
- Java 实现、自写别名/空输入/边界测试和运行证据。
- 怎样算完成
final的限制描述准确- 所选隔离合同得到验证
练到的知识点final 的不同作用 · 引用固定与对象可变
- 作业 06面试场景
测试类单独运行成功,整套测试运行计数却累积;同时 IDE 能启动而终端找不到主类。把两类故障分别定位到需要收集的证据,避免混合修改。
- 提交内容
- 两条排查树及最小复现草案。
- 怎样算完成
- 共享状态与类路径问题分开
- 每步检查有明确目标
练到的知识点完整类名与包目录 · import 与 classpath · static 共享成员
- 作业 07排错实验
创建同包的
final类与调用类,从终端指定输出目录和 classpath 运行;另在独立文件尝试继承该类,分别记录编译与运行阶段证据。- 提交内容
- 目录结构、自写调用检查、编译失败及成功运行记录。
- 怎样算完成
- 类路径与继承约束分开验证
- 文件副本不会混用
练到的知识点import 与 classpath · final 的不同作用
- 作业 08设计评审
工具库打成 jar 后,消费者写了 import 却未加入 classpath。列出 import 与类路径分别需要承担的责任,设计一个最小验证流程。
- 提交内容
- 依赖查找图与编译运行检查。
- 怎样算完成
- 源码名称解析和类可用性区分
- 验证不依赖 IDE 隐式配置
练到的知识点import 与 classpath
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
完成接口设计后:为可扩展的解析器库划定公开 API、子类扩展点与内部实现,说明哪些类或方法可以限制继承,制定兼容性测试。
- 提交内容
- 访问级别表、扩展样例与变更评审规则。
- 怎样算完成
- 权限依据使用方需要选择
- 不以全部 public 代替设计
练到的知识点四类访问权限 · protected 跨包约束 · final 的不同作用
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 反例构造
审查“类声明为
final后就能作为完全安全的共享库对象”的说法。限定在已学字段与引用知识内提出检查样本,列出该声明不能单独证明的合同。- 提交内容
- 对象模型和合同证据表。
- 怎样算完成
- 推理依据具体成员行为
- 编译限制与运行保证分别表述
练到的知识点import 与 classpath · final 的不同作用
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读规范英文
Java 17 语言规范:包与模块 (新标签页)↗
阅读包声明、导入和编译单元;区分目录、包名与模块概念。
- 实践规范英文
Java 17 语言规范:名称与访问控制 (新标签页)↗
阅读作用域、名称遮蔽和 public
/protected/package/private访问规则。 - 深入规范英文
Java 17 语言规范:类、方法与记录类 (新标签页)↗
按需阅读构造器、方法重载、static
/final、枚举和 record 的定义与约束。 - 深入参考文档英文
Java 17:javac 命令 (新标签页)↗
查阅 -encoding、-d、--release 和 classpath,用于解释编译器实际输入与输出。
- 深入参考文档英文
OpenJDK 17u javax.lang.model.SourceVersion (新标签页)↗
定位版本化
isName,重点阅读 split 保留空段与逐段词法校验。 - 深入参考文档英文
Java17 SourceVersion API (新标签页)↗
确认
isName、isIdentifier、isKeyword的不同约束和版本参数。