Java 后端每日学习 · Day 18 · 2026-09-11
打开今日互动学习页 :汇总五个课程章节,支持代码复制、复盘作答、浏览器草稿和本地 Markdown 保存。网页作答优先、聊天提交补充。
今日主题
Map 阶段闯关变式:多仓库存预占过期批处理索引的契约、聚合、编码与源码解释。
方向元数据
字段
值
directionId
java-data-structures
directionSession
16
curriculumItemId
challenge.map
lessonMode
checkpoint
节点进入前状态
covered_unverified
完成本课后的预期状态
covered_unverified;生成第六个变式不等于闯关通过
当前方向状态
active
查看全局 Java 后端知识地图 。固定同步助手对最近完整课程 Day 17 的拉取结果为 remote_missing,服务器与本地均没有 2026-09-10/04-复盘作答.md,当前任务也没有能明确映射到 Day 17 题号和问题源哈希的答案。因此答案来源为 none,不评分、不推断薄弱项或红线;challenge.map 保持 covered_unverified,今天继续在 Map 模块完成新的综合变式。
今天把重心从“排序字段变化后的重排”转到“有界批量状态迁移与聚合扣减”:库存预占事实不能因缓存淘汰或到期索引删除而消失;确认和过期必须同步处理到期树、每仓 SKU 预占量、状态计数与最近访问缓存,并在聚合量归零时删除键。批处理上限、严格时间边界和失败恢复要求大大把单个 Map API 与完整业务原子性分开。
可验证目标
能从仓库、订单、行号组成稳定事实身份,并区分库存事实、到期顺序、SKU 聚合、状态计数与诊断缓存的职责。
能逐步推演确认、严格上界过期批次、批量上限、聚合扣减至零删键、缓存重新进入以及旧快照保持不变。
能完成 Java 21 必做编码,编译并产生规定输出,同时守住复合键相等性、TreeMap 全序、半开范围和多索引一致性。
能沿 OpenJDK jdk-21+35 的入口、字段和主调用链解释 HashMap 条件更新与聚合、TreeMap 导航删除、LinkedHashMap 访问顺序和 EnumMap 槽位。
能说明批量循环、fail-fast、单 Map 条件 API 与覆盖全部事实及投影的事务或恢复边界之间的区别。
60~75 分钟学习顺序
昨日复盘 :Day 17 五题完整参考答案与限流策略版本切换索引完整实现(约 12~14 分钟)。
核心讲解 :用库存预占到期批次串联事实、排序、聚合、缓存和一致性边界(约 31~32 分钟)。
编码练习 :完成到期快照、确认与带上限的严格过期批处理;这是闯关通过的必交编码(约 18~20 分钟)。
复盘问题 :按四个固定维度提交 Map 阶段闯关答案(约 8~9 分钟)。
总预计用时约 69~75 分钟 。今天只验证 Map 模块,不提前进入 Set,也不把昨日参考答案算成大大的作答证据。
方向进度
课前与课程完成后的标准课覆盖均为 10/35(28.57%);今天是同一稳定闯关项的第六个变式,不重复计数。
知识点覆盖保持 9/27;已评估 0、已掌握 0、待补强 0。
源码型知识点覆盖保持 4/9,已掌握仍为 0/9。
Map 闯关保持“已出题待验证”:闯关已开始 1/6、通过 0/6;综合项目 0/1、方向总测 0/1。
无作答只表示尚未验证,不等于失败;也不能据此放行到 Set。
课程完成标准
完成四部分学习,并能说明预占事实为何不随缓存或到期投影删除,以及 SKU 聚合归零为何要清理键。
将编码练习的 3 个 TODO 补全,以 javac --release 21 编译并运行,保存完整代码与规定输出。
回答复盘页全部闯关题;答案必须映射到当天题号和问题源,不能用本文或昨日参考答案代替。
不依赖 HashMap 顺序,不破坏 equals/hashCode 或 Comparator 契约,不混淆活视图、只读包装与快照,不把 fail-fast 或单 Map 原子 API 当成跨 Map 事务。
闯关放行条件
总分至少 80/100;固定维度为:契约与选型 25、复杂度与状态推演 25、编码 30、源码讲解 20。
Java 21 编码必须成功编译、运行且产生预期结果;只写思路、缺少完整代码或输出不符均不能通过。
六类红线均未命中。达到分数但编码失败或命中红线,仍视为未通过。
今日课程生成后,challenge.map 仍只记 covered_unverified。只有后续取得完整、有效且满足全部条件的作答证据,才可记为 mastered 并进入 Set。
固定版本与官方资料
条件式下节预告
若仍没有完整有效的闯关答案:challenge.map 保持未通过,下一完整学习日继续用新数据和约束完成 Map 综合练习或闯关变式,绝不进入 Set。
若出现部分有效答案且未暴露明确错误:逐题点评,challenge.map 记为 practicing,保留验证债务并继续 Map 闯关,不虚构总分。
若完整答案低于 80、Java 21 编码不通过,或任何有效答案暴露明确错误或红线:challenge.map 与明确薄弱的 1~3 个知识点记为 needs_review,方向记为 needs_review;下一课优先更换数据、约束和场景补强,再重闯。
只有完整答案达到至少 80、编码编译运行符合预期且无红线:challenge.map 才记为 mastered,并依据充分证据更新相关 Map 知识点;下一课才可进入 ds.set.contract-algebra。
返回今日索引
昨日复盘:多租户限流策略版本切换 Map 闯关核对
复盘对象:Day 17(2026-09-10),评估项 challenge.map。建议用时:12~14 分钟。先用 1~2 分钟确认第 1 节证据与推进边界,遮住答案用 5~6 分钟口述第 2 节,再用约 6 分钟核对第 3 节三个 TODO、规定输出和复杂度。下文全部是教学参考答案,不构成大大的作答、分数、闯关通过或掌握证据 。
1. 作答证据、逐题评分与推进结论
固定同步助手拉取 Day 17 作答的结果为 remote_missing,服务器上没有 2026-09-10/04-复盘作答.md;本地也没有该文件。
Day 17 问题源哈希:sha256:3f2bd372377f603188f5b167aa98c9c33c03df8a0f4c14464794e74cde9e0476。
当前任务中没有能明确映射到 Day 17、题号和上述问题源哈希的聊天答案。
答案来源:none。
五题均为未作答、未评分 ;没有有效证据可给分,缺答也不能直接记为 0 分。
总分:不足以评分 。
红线:未知 。没有证据证明命中,也没有证据证明已避开六类红线。
薄弱知识 ID:无法从无作答证据中确定 ,因此不虚构错题或针对性薄弱项。
状态变化:challenge.map 保持 covered_unverified,Map 闯关仍为未通过 ;方向保持 active。
推进边界:今天继续 challenge.map 的第六个变式“多仓库存预占过期批处理索引”,不得进入 Set 。阅读、复制或运行下方参考答案均不会改变状态。
题号
固定维度
分值
大大的有效答案
点评与得分
1
契约与选型
25
无
无法核对版本身份、排序全序、缓存淘汰、重排投影与快照所有权,未评分
2
复杂度与状态推演
12
无
无法核对重排成本、HashMap 退化、LinkedList 对比与 fail-fast 边界,未评分
3
复杂度与状态推演
13
无
无法核对重排、严格上界、缓存重新进入、活视图与稳定快照,未评分
4
编码
30
无
未提交完整 Java 21 代码、编译证据和规定输出,编码维度未评分,闯关不可放行
5
源码讲解
20
无
无法核对固定版本入口、字段、主路径和可观察合同,未评分
合计
100
证据不足
不足以评分
无作答时可先独立重做第 1~3 题,再在不看参考实现的情况下完成三个 TODO,最后用第 5 题的源码路径反向解释运行结果。这只是通用复习路线,不表示已经发现大大的确定错误。
2. Day 17 五道闯关题完整参考答案
第 1 题:稳定版本身份、排序投影与所有权
事实主表使用 HashMap<PolicyKey, RatePolicy>;不可变 record PolicyKey(tenantId,apiCode,version) 的三个组件共同参与 equals/hashCode。键和值都拒绝 null,租户和 API 还拒绝空白、版本必须为正,因此本题 get()==null 可以无歧义地表示缺失。
activateMinute、priority、permitsPerMinute 和 state 会随调度或激活而改变,只能放在不可变 value 的新版本中;version 是策略身份的一部分,不能因重排而改变事实键。
调度索引使用 TreeMap<ActivationKey,PolicyKey>,依次按分钟升序、优先级降序、租户、API、版本升序比较;合法键上只有五个组件全相等才 compareTo==0,不同策略不会在树中互相覆盖。
状态计数使用 EnumMap<PolicyState,Integer>;封闭枚举键域紧凑且按枚举声明顺序遇见,计数降为零时删除槽位。
预览缓存使用容量为 3 的 access-order LinkedHashMap;命中、已有键更新和插入都会改变热度,超限只淘汰最老缓存项,不删除事实或调度项。
重排先保存并条件删除旧 ActivationKey,再以新不可变 value 条件替换事实、插入新排序键并更新缓存;状态未变,所以计数不变。四张普通 Map 的连续写入仍需外层共同锁、单线程事件所有者、不可变聚合交换或事务保护。
subMap/headMap 是背靠根 TreeMap 的活视图,删除会写穿;对外结果应立即投影为独立、不可修改的字符串快照。只读包装会随底层容器变化,不等于快照。
激活业务顺序只由 ActivationKey 的比较合同决定,不能借用 HashMap/HashSet 的偶然遇见顺序;WeakHashMap 会让事实寿命受键可达性和 GC 影响,IdentityHashMap 用 == 区分反序列化得到的等值键,都不适合持久策略事实。
byKey 是唯一事实来源;调度树、状态计数和预览缓存都是可核对、可从事实重建的派生投影。缓存未命中只说明当前不热,绝不表示策略事实不存在。
第 2 题:重排包含两次树修改,复杂度不是一句 O(1)
设事实表有 n 个策略版本,时间窗命中 k 个:
byKey 的精确查询、条件新增和条件替换的期望时间为 O(1),但这不是每次操作的严格保证。
提交包含 HashMap 条件新增、TreeMap 插入、EnumMap 计数和缓存写入,由树插入主导为 O(log n)。
重排包含旧排序键删除和新排序键插入,两项各为 O(log n),再加事实替换及缓存写入的期望 O(1),合并后仍为 O(log n),而不是只做一次 HashMap value 替换。
首项激活需要边界导航和 TreeMap 删除,再更新事实、计数与缓存,成功分支为 O(log n)。
半开窗口的边界定位加 k 项投影为 O(log n + k),返回稳定字符串快照额外占用 O(k) 空间。
EnumMap 键域固定,单次计数读写按 O(1) 理解;容量固定的预览缓存命中、移动、插入和一次最老项淘汰为常数或期望 O(1),空间上限为 O(1)。事实表与调度树的总空间为 O(n)。
在固定 OpenJDK jdk-21+35 中,HashMap 新增超过负载阈值时会执行 resize 并处理旧 table 中的节点;发生碰撞时还可能沿链或树继续定位。碰撞桶达到树化条件时,table 容量小于 MIN_TREEIFY_CAPACITY=64 会优先扩容而非树化,容量足够才可能转为树,因此“正常分布下期望 O(1)”不能说成“每次严格 O(1)”。
若改用 LinkedList 且事先不知道候选节点位置,必须先线性查找 O(n);拿到节点后链接删除即便是 O(1),完整的“查找并删除”仍为 O(n)。TreeMap 则利用比较树以 O(log n) 导航和删除。迭代器 fail-fast 只尽力检测部分结构修改,不提供互斥、内存可见性、复合操作原子性或一致快照,所以不能被当作线程安全机制。
第 3 题:重排会改变树位置和缓存热度,但不会改变事实身份
用 A、B、C、D 表示:
A:tenant-a/orders/v1@640#P2
B:tenant-b/orders/v1@620#P1
C:tenant-a/pay/v2@620#P4
D:tenant-c/report/v1@660#P3
预览缓存容量为 2,调度树和缓存逐步变化如下:
动作
调度树顺序
access-order 缓存(左旧右新)
提交 A
[A@640#P2]
[A]
提交 B
[B@620#P1, A@640#P2]
[A,B]
提交 C
[C@620#P4, B@620#P1, A@640#P2]
[B,C],A 被淘汰
A 重排为 630#P5
[C@620#P4, B@620#P1, A@630#P5]
[C,A],A 重新进入并淘汰 B
提交 D
[C@620#P4, B@620#P1, A@630#P5, D@660#P3]
[A,D],淘汰 C
activateNext(630)
[B@620#P1, A@630#P5, D@660#P3]
[D,C],C 重新进入并淘汰 A
同一分钟 620 内优先级降序,所以 P4 的 C 排在 P1 的 B 前。严格上界 630 只接纳分钟 <630 的候选,重排后恰在 630 的 A 被排除,因此激活的是 C。最终计数为 SCHEDULED=3、ACTIVE=1;C 的事实仍在 byKey 中,但 value 已换成 ACTIVE。
A 的事实键仍是 tenant-a/orders/v1,新 value 保留原限额并变为 activateMinute=630、priority=5、state=SCHEDULED;旧 A@640#P2 排序键已经删除。先取得的 [620,641) 活视图在激活后当前内容为 [B@620#P1, A@630#P5],因为删除 C 会写穿根树;此前固化的不可变字符串快照仍为 [C@620#P4:SCHEDULED, B@620#P1:SCHEDULED, A@630#P5:SCHEDULED],不会被后续事实替换改写。
“删旧排序键、换事实、建新排序键、更新缓存”横跨多张 Map。题设单线程只避免线程交错,不能抵御进程在中途退出;单个 replace、remove(key,value) 或 ConcurrentHashMap 的单键原子 API 也不会自动把其他 Map 纳入同一事务。
第 4 题:三个 TODO 的最小完整实现策略
重排在任何写入前校验新分钟和优先级;缺失或已激活直接返回 false。成功路径保存并条件删除旧排序键,用新 record 条件替换事实,再插入新键并让缓存项重新进入或升温,状态计数不变。
快照使用两个边界键和四参数 subMap 表达 [fromInclusive,toExclusive),按活视图顺序核对事实状态及完整排序键,最后以 Stream.toList() 固化为不可修改的字符串列表。
激活使用严格上界 headMap 选首项,核对事实与投影后以 ACTIVE record 条件替换事实,从活视图删除旧调度项,再迁移计数和更新缓存。
重复提交由事实表的 putIfAbsent 立即截断,不能修改调度、计数或缓存热度。
被缓存淘汰的策略仍在事实表中,重排或激活写入最新 value 时可以重新进入并触发另一次缓存淘汰。
所有成功步骤仍依赖题设的单线程顺序;生产环境必须给整段状态转换提供共同原子或可恢复边界。
第 3 节给出与 Day 17 起始代码一致的完整 Java 21 参考实现和规定输出。
第 5 题:固定源码路径要落回本题的可观察结果
固定版本为 OpenJDK jdk-21+35:
问题
公开入口
关键字段
最多四个箭头节点的主路径
可观察结果或扩展点
HashMap 精确查询、新键写入、同键 value 替换与删除
get、put、replace、remove
table、size、modCount
get→getNode→桶首/链/树匹配;put→putVal→替换或新增;remove→removeNode→摘链/树删除
等值键替换 value 不增加 size;新节点和结构删除影响结构;遇见顺序不属于合同
阈值、扩容拆分、树化容量条件和迭代检测
put、集合视图 iterator
threshold、loadFactor、table、modCount、expectedModCount
putVal→超阈值→resize→高低链拆分;treeifyBin→容量判断→扩容或树化;nextNode→比较 modCount
table 容量不足 64 时长桶优先扩容;fail-fast 仅尽力发现结构修改,不提供线程安全
LinkedHashMap access-order 命中、已有键更新与最老项钩子
get、put
head、tail、accessOrder
get→afterNodeAccess→节点移尾;putVal→afterNodeAccess/afterNodeInsertion→removeEldestEntry
命中和已有键更新改变热度;插入后子类可淘汰最老缓存项,但不会删除另一张事实表
TreeMap 比较定位、删除旧键、插入新键及范围视图
put、remove、subMap、headMap
root、comparator、size、modCount
put→逐节点 compare→替换或插入平衡;remove→getEntry→deleteEntry;subMap/headMap→NavigableSubMap→边界导航/写穿
compare==0 即同一排序键;旧键删除后新键按完整全序换位,视图按范围遇见并写穿根树
EnumMap 枚举槽位与计数迁移
put、get、merge
keyType、keyUniverse、vals、size
put→typeCheck→key.ordinal→写入 vals 槽位
状态按枚举声明顺序遇见;同一常量定位同一槽位,计数降零后由业务代码删键
源码实现只能解释固定版本现象,不能升级为公共业务合同:不能猜红黑树具体形状,不能把内部阈值写成 Map API 的永久保证,也不能把 modCount、fail-fast、普通 Map 或 ConcurrentHashMap 的单键能力扩张成业务排序、线程安全或跨键、跨 Map 事务。可核对 HashMap.java 、LinkedHashMap.java 、TreeMap.java 与 EnumMap.java 。
3. Day 17 主练习完整可运行参考实现
下面只补全原起始代码的三个 TODO,不改变测试数据,不添加第三方依赖、日志、无恢复意义的 try/catch 或多余公开 API。嵌套类型和方法保持最小可见性;关键注释只解释事实与派生投影的一致性边界。
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;
public final class RatePolicySwitchChallenge {
private enum PolicyState {
SCHEDULED,
ACTIVE
}
private record PolicyKey(
String tenantId,
String apiCode,
long version) {
PolicyKey {
Objects.requireNonNull(tenantId, "tenantId");
Objects.requireNonNull(apiCode, "apiCode");
if (tenantId.isBlank() || apiCode.isBlank() || version <= 0) {
throw new IllegalArgumentException("invalid policy key");
}
}
String label() {
return tenantId + "/" + apiCode + "/v" + version;
}
}
private record RatePolicy(
PolicyKey key,
long activateMinute,
int priority,
int permitsPerMinute,
PolicyState state) {
RatePolicy {
Objects.requireNonNull(key, "key");
Objects.requireNonNull(state, "state");
if (activateMinute < 0
|| priority < 1
|| priority > 9
|| permitsPerMinute <= 0) {
throw new IllegalArgumentException("invalid rate policy");
}
}
RatePolicy rescheduled(long newMinute, int newPriority) {
return new RatePolicy(
key,
newMinute,
newPriority,
permitsPerMinute,
state);
}
RatePolicy activated() {
return new RatePolicy(
key,
activateMinute,
priority,
permitsPerMinute,
PolicyState.ACTIVE);
}
String snapshotLine() {
return key.label() + "@" + activateMinute
+ "#P" + priority + ":" + state
+ ":L" + permitsPerMinute;
}
}
private record ActivationKey(
long activateMinute,
int priority,
String tenantId,
String apiCode,
long version) implements Comparable<ActivationKey> {
static ActivationKey from(RatePolicy policy) {
return new ActivationKey(
policy.activateMinute(),
policy.priority(),
policy.key().tenantId(),
policy.key().apiCode(),
policy.key().version());
}
static ActivationKey boundary(long minute) {
return new ActivationKey(
minute,
Integer.MAX_VALUE,
"",
"",
Long.MIN_VALUE);
}
@Override
public int compareTo(ActivationKey other) {
int byMinute = Long.compare(activateMinute, other.activateMinute);
if (byMinute != 0) {
return byMinute;
}
int byPriority = Integer.compare(other.priority, priority);
if (byPriority != 0) {
return byPriority;
}
int byTenant = tenantId.compareTo(other.tenantId);
if (byTenant != 0) {
return byTenant;
}
int byApi = apiCode.compareTo(other.apiCode);
return byApi != 0
? byApi
: Long.compare(version, other.version);
}
}
private static final class PolicyIndex {
private static final int PREVIEW_LIMIT = 3;
private final Map<PolicyKey, RatePolicy> byKey = new HashMap<>();
private final NavigableMap<ActivationKey, PolicyKey>
activationSchedule = new TreeMap<>();
private final EnumMap<PolicyState, Integer> stateCounts =
new EnumMap<>(PolicyState.class);
private final LinkedHashMap<PolicyKey, RatePolicy> previewCache =
new LinkedHashMap<>(4, 0.75f, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<PolicyKey, RatePolicy> eldest) {
return size() > PREVIEW_LIMIT;
}
};
private boolean submit(RatePolicy policy) {
Objects.requireNonNull(policy, "policy");
if (policy.state() != PolicyState.SCHEDULED) {
throw new IllegalArgumentException(
"new policy must be scheduled");
}
if (byKey.putIfAbsent(policy.key(), policy) != null) {
return false;
}
// 事实新增后才建立派生投影;生产环境仍需外层一致性边界。
activationSchedule.put(ActivationKey.from(policy), policy.key());
stateCounts.merge(PolicyState.SCHEDULED, 1, Integer::sum);
previewCache.put(policy.key(), policy);
return true;
}
private boolean reschedule(
PolicyKey key,
long newMinute,
int newPriority) {
Objects.requireNonNull(key, "key");
if (newMinute < 0 || newPriority < 1 || newPriority > 9) {
throw new IllegalArgumentException("invalid new schedule");
}
RatePolicy current = byKey.get(key);
if (current == null || current.state() != PolicyState.SCHEDULED) {
return false;
}
ActivationKey oldKey = ActivationKey.from(current);
if (!activationSchedule.remove(oldKey, key)) {
throw new IllegalStateException(
"activation schedule is inconsistent with fact map");
}
RatePolicy updated = current.rescheduled(newMinute, newPriority);
if (!byKey.replace(key, current, updated)) {
throw new IllegalStateException(
"fact changed while rescheduling policy");
}
// 旧投影已清理;新事实、新排序键和缓存须由同一外层边界保护。
activationSchedule.put(ActivationKey.from(updated), key);
previewCache.put(key, updated);
return true;
}
private List<String> activationSnapshot(
long fromInclusive,
long toExclusive) {
if (fromInclusive > toExclusive) {
throw new IllegalArgumentException(
"fromInclusive must be <= toExclusive");
}
NavigableMap<ActivationKey, PolicyKey> window =
activationSchedule.subMap(
ActivationKey.boundary(fromInclusive), true,
ActivationKey.boundary(toExclusive), false);
return window.entrySet().stream()
.map(entry -> {
RatePolicy policy = byKey.get(entry.getValue());
if (policy == null
|| policy.state() != PolicyState.SCHEDULED
|| !ActivationKey.from(policy)
.equals(entry.getKey())) {
throw new IllegalStateException(
"activation schedule is inconsistent "
+ "with fact map");
}
return policy.snapshotLine();
})
.toList();
}
private String activateNext(long beforeExclusive) {
if (beforeExclusive < 0) {
throw new IllegalArgumentException(
"beforeExclusive must be >= 0");
}
NavigableMap<ActivationKey, PolicyKey> candidates =
activationSchedule.headMap(
ActivationKey.boundary(beforeExclusive), false);
Map.Entry<ActivationKey, PolicyKey> first =
candidates.firstEntry();
if (first == null) {
return "NONE";
}
PolicyKey key = first.getValue();
RatePolicy current = byKey.get(key);
Integer scheduledCount = stateCounts.get(PolicyState.SCHEDULED);
if (current == null
|| current.state() != PolicyState.SCHEDULED
|| !ActivationKey.from(current).equals(first.getKey())
|| scheduledCount == null
|| scheduledCount <= 0) {
throw new IllegalStateException(
"activation schedule is inconsistent with fact map");
}
RatePolicy activated = current.activated();
if (!byKey.replace(key, current, activated)) {
throw new IllegalStateException(
"fact changed while activating policy");
}
// 子视图删除写穿根树,随后同步计数与最新缓存 value。
if (!candidates.remove(first.getKey(), key)) {
throw new IllegalStateException(
"activation schedule changed while activating policy");
}
int remaining = stateCounts.merge(
PolicyState.SCHEDULED, -1, Integer::sum);
if (remaining == 0) {
stateCounts.remove(PolicyState.SCHEDULED);
}
stateCounts.merge(PolicyState.ACTIVE, 1, Integer::sum);
previewCache.put(key, activated);
return key.label();
}
private String policyLine(PolicyKey key) {
RatePolicy policy = byKey.get(Objects.requireNonNull(key, "key"));
return policy == null ? "MISSING" : policy.snapshotLine();
}
private String previewState(PolicyKey key) {
RatePolicy policy = previewCache.get(
Objects.requireNonNull(key, "key"));
return policy == null ? "MISS" : policy.state().name();
}
private List<String> scheduleOrder() {
return activationSchedule.entrySet().stream()
.map(entry -> entry.getValue().label()
+ "@" + entry.getKey().activateMinute()
+ "#P" + entry.getKey().priority())
.toList();
}
private List<String> previewOrder() {
return previewCache.keySet().stream()
.map(PolicyKey::label)
.toList();
}
private List<String> countSummary() {
return stateCounts.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.toList();
}
}
public static void main(String[] args) {
PolicyKey aKey = new PolicyKey("tenant-a", "search", 1);
PolicyKey bKey = new PolicyKey("tenant-b", "search", 1);
PolicyKey cKey = new PolicyKey("tenant-a", "pay", 2);
PolicyKey dKey = new PolicyKey("tenant-c", "report", 1);
RatePolicy a = new RatePolicy(
aKey, 520, 2, 100, PolicyState.SCHEDULED);
RatePolicy b = new RatePolicy(
bKey, 500, 1, 80, PolicyState.SCHEDULED);
RatePolicy c = new RatePolicy(
cKey, 500, 5, 40, PolicyState.SCHEDULED);
RatePolicy d = new RatePolicy(
dKey, 540, 3, 200, PolicyState.SCHEDULED);
PolicyIndex index = new PolicyIndex();
System.out.println("SUBMIT A=" + index.submit(a));
System.out.println("SUBMIT B=" + index.submit(b));
System.out.println("SUBMIT C=" + index.submit(c));
System.out.println("SUBMIT D=" + index.submit(d));
System.out.println("SUBMIT duplicate-A=" + index.submit(a));
System.out.println("PREVIEW before-reschedule="
+ index.previewOrder());
System.out.println("RESCHEDULE A="
+ index.reschedule(aKey, 510, 4));
System.out.println("POLICY A=" + index.policyLine(aKey));
System.out.println("PREVIEW after-reschedule="
+ index.previewOrder());
List<String> beforeActivation = index.activationSnapshot(500, 521);
System.out.println("WINDOW before=" + beforeActivation);
System.out.println("COUNTS before=" + index.countSummary());
System.out.println("ACTIVATE before-500="
+ index.activateNext(500));
System.out.println("ACTIVATE before-510="
+ index.activateNext(510));
System.out.println("COUNTS after=" + index.countSummary());
System.out.println("SCHEDULE after=" + index.scheduleOrder());
System.out.println("PREVIEW after=" + index.previewOrder());
System.out.println("SNAPSHOT unchanged=" + beforeActivation);
}
}
三个待补位置的关键步骤
reschedule 先校验全部外部输入和当前状态,再从旧事实生成旧 ActivationKey;只有“旧排序键 + 事实键”条件删除成功才构造新 record、条件替换事实并建立新投影。重排没有状态迁移,不能改 SCHEDULED 计数。
activationSnapshot 用排在同分钟所有合法业务键之前的边界键表达半开区间。下界包含边界会收进下界整分钟,上界排除边界会排除上界整分钟;逐项核对事实和排序键后生成字符串,toList() 返回不可修改列表。
activateNext 用严格上界活视图取得第一项,先核对事实状态、完整排序键和计数,再以新 ACTIVE record 条件替换事实。对子视图的删除写穿根树,随后把计数从 SCHEDULED 迁移到 ACTIVE,并用新 value 更新缓存。
精确编译与规定输出
javac --release 21 RatePolicySwitchChallenge.java
java RatePolicySwitchChallenge
运行输出必须为:
SUBMIT A=true
SUBMIT B=true
SUBMIT C=true
SUBMIT D=true
SUBMIT duplicate-A=false
PREVIEW before-reschedule=[tenant-b/search/v1, tenant-a/pay/v2, tenant-c/report/v1]
RESCHEDULE A=true
POLICY A=tenant-a/search/v1@510#P4:SCHEDULED:L100
PREVIEW after-reschedule=[tenant-a/pay/v2, tenant-c/report/v1, tenant-a/search/v1]
WINDOW before=[tenant-a/pay/v2@500#P5:SCHEDULED:L40, tenant-b/search/v1@500#P1:SCHEDULED:L80, tenant-a/search/v1@510#P4:SCHEDULED:L100]
COUNTS before=[SCHEDULED=4]
ACTIVATE before-500=NONE
ACTIVATE before-510=tenant-a/pay/v2
COUNTS after=[SCHEDULED=3, ACTIVE=1]
SCHEDULE after=[tenant-b/search/v1@500#P1, tenant-a/search/v1@510#P4, tenant-c/report/v1@540#P3]
PREVIEW after=[tenant-c/report/v1, tenant-a/search/v1, tenant-a/pay/v2]
SNAPSHOT unchanged=[tenant-a/pay/v2@500#P5:SCHEDULED:L40, tenant-b/search/v1@500#P1:SCHEDULED:L80, tenant-a/search/v1@510#P4:SCHEDULED:L100]
第四次提交只让容量为 3 的缓存淘汰 A,事实表和调度树仍保留 A;重复提交在 putIfAbsent 失败后立即返回,不能让 A 回到缓存。重排把 A 的旧 520#P2 投影删除并建立 510#P4 新投影,状态没变所以计数仍为 4。同为 500 时 P5 的 C 排在 P1 的 B 前;严格上界 510 排除分钟恰为 510 的 A,因而激活 C。旧快照已经复制为字符串,即使 C 的事实变为 ACTIVE,仍保留复制时的 SCHEDULED。
时间与空间复杂度
submit 的事实表条件写入期望 O(1),调度树插入 O(log n),计数与缓存更新为常数或期望 O(1),整次为 O(log n)。
reschedule 对调度树删除一次、插入一次,事实条件替换和缓存更新为期望 O(1),整次为 O(log n);它不会因两次树操作变成 O(2 log n) 之外的新渐进阶。
activationSnapshot 的边界定位与 k 项核对、投影为 O(log n + k),稳定字符串快照额外空间为 O(k)。
activateNext 的边界导航与树删除为 O(log n),事实替换、计数迁移及缓存更新为常数或期望 O(1),整次为 O(log n)。
policyLine 和预览命中的期望时间为 O(1);scheduleOrder 遍历剩余候选为 O(n);固定状态域和固定容量缓存按 O(1) 空间理解。
事实表与调度树总空间为 O(n),状态表和有界缓存为 O(1)。这些复杂度结论不代表最坏碰撞、并发安全或跨 Map 事务保证。
边界复核
同租户同 API 的不同版本、不同租户的同 API/版本可以并存;重排只产生新 value,不改变事实键。
完全相同复合键重复提交返回 false,且调度、计数和缓存热度都不改变。
key==null、负分钟、优先级越界必须在任何 Map 修改前失败;缺失或 ACTIVE 策略重排返回 false 且无副作用。
重排成功后旧排序键不存在,新排序键按完整五字段全序定位,SCHEDULED 计数不变。
[510,510) 返回空快照;下界大于上界在读取业务状态前失败;快照不可增删,后续事实替换不改旧字符串。
activateNext(500) 返回 NONE;严格上界 510 排除重排后的 A,只允许更早的 C/B 参与比较。
缓存淘汰不删除事实或候选;重排或激活已淘汰项会让它重新进入并可能淘汰当前最老项。
生产环境必须让共同锁、单线程事件所有权、不可变聚合交换或事务覆盖完整多索引转换,并准备从事实重建派生投影。
4. 今日变式前的最短自检
能否说明版本为何属于稳定事实身份,而分钟、优先级、限额和状态为何属于可替换 value?
能否从分钟、优先级、租户、API、版本推出全序,不依赖 HashMap 或 HashSet 的遇见顺序?
能否逐步推演容量为 2 的 access-order 缓存,包括 A 和 C 被淘汰后重新进入的两次变化?
能否区分严格上界 630、[620,641) 活视图与固化字符串快照在激活后的内容?
能否解释重排为何要删旧 TreeMap 键、插新键,却仍保持 O(log n) 且不迁移状态计数?
能否明确承认没有有效作答,所以 challenge.map 仍是 covered_unverified,今天必须继续 Map 而不能进入 Set?
如有一项含糊,先换一组仓库、订单行、预占到期时间与批处理上限重新推演,再开始今天的库存预占变式。这里的完整参考答案始终只用于教学核对,不会被记录为大大的答案证据。
返回今日索引
返回今日索引
核心讲解:多仓库存预占过期批处理中的 Map 合同与聚合
Day 18|directionSession 16|challenge.map|checkpoint。建议用时 31~32 分钟 。Day 17 的远端拉取结果为 remote_missing,本地与当前聊天也没有有效作答,因此 challenge.map 保持 covered_unverified,方向保持 active;本课继续取证,阅读参考过程本身不构成闯关通过。
1. 为什么需要:一笔预占结束,会同时影响事实、日历和汇总量
电商库存服务通常先“预占”再确认扣减:订单在支付窗口内占住某仓某 SKU 的数量,超时则释放。平台需要按预占身份精确查询,按到期时间批量释放,快速回答“某仓某 SKU 当前共被占多少”,维护最近访问缓存和有限状态计数。单条释放已经跨越多张 Map;批量释放还增加两个不同边界:只处理 expireMinute < beforeExclusive 的严格时间上界,同时最多处理 limit 条,避免一次任务独占线程。
昨日主轴是可变排序字段重排;今天不修改到期时间,而是研究多个事实共同贡献一个聚合键时怎样成批退出。若只把事实改为 EXPIRED,却忘了删 TreeMap 候选或扣减聚合量,查询会互相矛盾;若数量减到零仍保留 sku -> 0,那么“缺键表示没有预占”的合同也被破坏。真正目标是从业务不变量推出每一步,而不是背三行 Map 调用。
Map 闯关仍没有满足“总分至少 80、Java 21 编码通过、无红线”的有效证据,所以本课不能越过门禁进入 Set。
2. 前置知识:九项 Map 能力各自解决哪一段问题
Map 定义键到值的映射。事实表禁止 null 键和值,facts.get(key)==null 才能唯一表示缺失;汇总表只存正数并在零时删键,所以缺键可解释为该仓 SKU 当前预占量为零。
ReservationKey(warehouseId, orderId, lineNo) 的三个不可变分量共同参与 equals/hashCode。不同仓的同订单行可以并存,数量、SKU、到期时间和状态属于 value,不能改写身份。
HashMap 在散列分布正常时精确访问期望 O(1),但碰撞、扩容、链与树桶使单次并非严格 O(1);它的遇见顺序从来不是到期顺序。
TreeMap 的 Comparator 同时决定排序和树键同一性。到期键按分钟升序,再按仓、订单、行号补足全序;若比较为 0,TreeMap 就把两项视为同一键。
putIfAbsent 适合事实去重;merge 可累加正向预占;compute 返回 null 会删除映射,适合“扣到零删键”,但映射函数不能顺手修改同一 Map,且必须先防止缺项、负数和溢出。
headMap/subMap、entrySet/keySet/values 是背靠根容器的活视图。只读包装仍跟随根 Map;对外历史结果必须复制为稳定快照。
access-order LinkedHashMap 的成功命中和已有键更新会改变热度,容量 3 的淘汰只删除缓存投影,不能释放库存。
EnumMap 用枚举序号定位封闭状态槽,适合 HELD/CONFIRMED/EXPIRED;WeakHashMap 受 GC 影响,IdentityHashMap 用 ==,都不适合作为稳定订单事实表。
随堂检查 1:warehouseId 相同、orderId 相同但 lineNo 不同的两笔预占,能否共用一个事实键,靠 SKU 再区分?
**即时答案:**不能。订单行号属于业务身份;若事实表先覆盖,任何调度或 SKU 聚合都无法恢复丢失的那一行。SKU 是被预占的商品属性,不替代预占身份。
3. 定义:五张 Map 和四条可重算不变量
今天的结构职责是:
HashMap<ReservationKey, Reservation> facts 是唯一事实来源,value 为不可变 record。
TreeMap<ExpiryKey, ReservationKey> expirySchedule 只保存状态为 HELD 的候选;ExpiryKey 按到期分钟、仓、订单、行号全序排列。
HashMap<StockKey, Integer> heldQuantityBySku 汇总每个“仓 + SKU”的 HELD 数量;StockKey(warehouseId, sku) 不跨仓混算,只允许正 value,零值不保留。
容量为 3 的 access-order LinkedHashMap<ReservationKey, Reservation> recentCache 保存最近成功读取或更新的事实副本,允许缺项。
EnumMap<ReservationState, Integer> stateCounts 统计 HELD、CONFIRMED、EXPIRED,零计数项删除。
四条不变量可从 facts 重算:每个 HELD 事实恰好有一条匹配的到期项;每条到期项都能回查同一 HELD 事实;每个仓 SKU 汇总值等于所有匹配 HELD 事实数量之和;状态计数等于事实状态分组计数。缓存不参与等式,它只是可丢弃投影。confirm 与过期处理都会让一条事实退出 HELD 集合,因此都必须删除到期项、扣减汇总量、迁移状态计数并更新缓存;区别只是目标状态和候选选择方式。
4. 心智模型:主账、到期日历、分仓小计账和限额传送带
把 facts 想成逐行主账,身份写在封面,状态写在可替换的不可变页上。TreeMap 是到期日历,只把仍占库存的主账行挂到正确时间。heldQuantityBySku 是分仓小计账:多笔预占会共同加在同一格,任意一笔离开 HELD 就扣自己的数量,格子归零时整格擦除。recentCache 是只有三个座位的观察窗,淘汰不影响仓库。EnumMap 是状态牌。
批处理器像限额传送带:时间闸门先形成所有 < beforeExclusive 的候选前缀,maxItems 再只取其中最前的若干条。时间上界回答“有资格吗”,条数上限回答“本批最多做几个”,二者不能互换。maxItems=0 合法并返回空结果、所有 Map 不变;负数必须在读取业务 Map 前失败。处理每项前要用 ReservationKey 回查事实并核对状态、到期键、SKU 与数量;然后让事实、日历、小计账、状态牌和缓存一起完成一次转换。
随堂检查 2:候选有 3 条,分钟分别为 90、90、95;调用参数为 beforeExclusive=96, limit=2。第三条是否因为也已到期而必须本批处理?
**即时答案:**不处理。三条都通过严格时间上界,但批次上限只允许按 TreeMap 全序取前两条;第三条保留 HELD 和到期项,等待下一批。
5. 完整数值与状态推演:两条过期怎样改变共享聚合
设四笔 HELD 预占如下:
A:wh-east/o-701/1,sku-red,数量 3,到期 100。
B:wh-west/o-702/1,sku-red,数量 4,到期 90。
C:wh-east/o-703/2,sku-red,数量 2,到期 90。
D:wh-east/o-704/1,sku-blue,数量 5,到期 95。
登记完成后 facts 有 4 项,stateCounts={HELD=4}。到期全序为 [C@90,B@90,D@95,A@100]:C、B 同分钟时,wh-east 先于 wh-west;仓与订单也相同时再用行号区分,绝不借 HashMap 顺序。聚合为 {wh-east/sku-blue=5, wh-east/sku-red=5, wh-west/sku-red=4},其中 east/red 的 5 来自 A 的 3 加 C 的 2。
缓存容量为 3。依次写入 A、B、C 后为 [A,B,C];成功读取 A 得 [B,C,A];写入 D 淘汰 B,变成 [C,A,D]。B 缓存缺失,但 facts、到期日历和 west/red=4 都仍存在。
现在先取得严格上界 96 的导航活视图和字符串快照,二者都是 [C@90,B@90,D@95]。执行限额为 2 的过期批次:
首项 C 回查为 HELD,数量 2、键与日历一致。以新 record 把事实换成 EXPIRED,删除 C@90;east/red 从 5 减到 3,仍为正所以保留;计数暂为 HELD=3, EXPIRED=1。缓存更新已有 C,顺序从 [C,A,D] 变为 [A,D,C]。
次项 B 同样通过核对。事实换成 EXPIRED 并删 B@90;west/red 从 4 减 4 得 0,因此删除聚合键而不是保存 0;计数变为 HELD=2, EXPIRED=2。B 原先不在缓存,插入尾部后超容量,淘汰 A,最终缓存为 [D,C,B]。
已处理数量达到 2,停止。D@95 虽满足 <96,仍保持 HELD;A@100 因严格上界本来就不合格。
批后到期树为 [D@95,A@100],聚合为 {wh-east/sku-blue=5, wh-east/sku-red=3}。旧活视图现在只含 [D@95],因为删除 C、B 写穿根树;旧字符串快照仍保留 [C@90,B@90,D@95]。facts 中 C、B 是 EXPIRED,A、D 是 HELD。下一批用同一上界可继续处理 D,不会重复处理已退出日历的 C、B。
复杂度要按完整批次回答。设 facts 有 n 项,本批实际处理 b 项,b≤limit:TreeMap 边界定位 O(log n),每项回查 facts 期望 O(1)、树删除 O(log n)、两个 HashMap 和 EnumMap/缓存更新为常数或期望 O(1),合计 O(log n+b log n),常写作 O((b+1)log n)。生成含 k 项的快照为 O(log n+k) 时间、O(k) 空间。聚合表大小由实际“仓 + SKU”组合数决定,最坏仍为 O(n)。
注意错误中断语义:若第 2 项才发现聚合不足,前一项是否已经生效必须由接口合同定义。要求整批全有或全无时,可先验证本批、构造新的不可变聚合状态再一次交换,或使用数据库事务;允许逐项提交时,应返回明确的成功项和失败点并保证每项自身一致。练习中的单线程顺序只消除并发穿插,不会自动提供崩溃原子性。
6. 源码映射:从批量现象回到 OpenJDK 21 路径
固定基线为 Java 21、OpenJDK jdk-21+35。每组都按“问题 → 入口类/方法 → 关键字段 → 主调用链 → 扩展点 → 调试练习”阅读;私有结构只解释固定版本现象,不升级为公共业务合同。
6.1 HashMap:事实去重、聚合累加与零值删除
**问题:**等值 ReservationKey 为什么命中同一事实,compute 返回 null 又为何删除汇总项?
入口类/方法: HashMap.get/put/putIfAbsent/compute/merge/remove;关键字段: table、size、threshold、loadFactor、modCount。
主调用链: get→getNode→桶首/链/树匹配;写入进入 putVal,相等键替换、新键增加 size;重映射先定位旧节点,再根据新结果替换、添加或删除。
**扩展点:**容量 4、阈值 3、size 2 时,新增第 3 项不扩容;第 4 项使 size 超阈值后扩到 8。旧表同在桶 1、散列值相差旧容量位 4 的节点,在新表可分到桶 1 与 5。碰撞桶申请树化时,table 容量小于 MIN_TREEIFY_CAPACITY=64 会优先扩容。
**调试练习:**观察重复事实写入的返回值、size 与 modCount;再让某聚合 compute 得到 null,确认节点和键一起消失,而不是留下 value=0。
6.2 TreeMap:严格前缀、批次上限与安全删除
**问题:**怎样确定 <96 的有序候选,并只移除前两项而不破坏遍历?
入口类/方法: TreeMap.headMap/firstEntry/pollFirstEntry/remove 及视图迭代器;关键字段: root、size、comparator、modCount 和子图边界。
**主调用链:**范围入口保存边界→导航定位首个合法节点→按后继迭代;删除按 comparator 找节点→红黑树删除与再平衡→修改结构计数。活视图删除会写穿根树。
**扩展点:**边界键必须排在该分钟所有合法业务键之前,排除它才能排除整个上界分钟;limit 在时间过滤之后计数。增强 for 中直接调用根 Map.remove 可能触发 fail-fast,应使用迭代器自身删除、反复取首项,或先复制有限键后再处理,并说明所有权与一致性。
**调试练习:**在 90、90、95、100 四项上观察严格上界 96 的视图;删前两项后比较活视图与先前复制列表,并检查第三项仍存在。
6.3 LinkedHashMap、EnumMap 与迭代检测
**问题:**为什么缓存命中会重排、枚举计数却按声明顺序输出,异常又为何不代表线程安全?
入口类/方法: LinkedHashMap.get/put、EnumMap.get/put/merge、集合视图的 iterator;**关键字段:**前者 head、tail、accessOrder,后者 keyType、keyUniverse、vals、size,迭代器持有 expectedModCount。
**主调用链:**访问顺序表 get→getNode→afterNodeAccess→移到尾部;插入完成后 afterNodeInsertion→removeEldestEntry;EnumMap 校验 key 类型后以 ordinal 定位数组槽;迭代器比较期望与实际结构版本。
**扩展点:**缓存读取在 access-order 模式可能是结构修改;EnumMap 的 null key 被拒绝,内部哨兵可区分 null value,但本题仍禁止 null;fail-fast 只尽力暴露误用,不提供锁、可见性或一致快照。
**调试练习:**持有缓存迭代器后命中中间键,观察顺序与 modCount,再解释异常为什么不能替代共同并发边界。
固定源码直链:HashMap.java 、LinkedHashMap.java 、TreeMap.java 、EnumMap.java 。公开合同见 Java SE 21 Map 、HashMap 与 NavigableMap 。
7. 真实后端应用:库存真相、幂等任务和故障恢复
生产系统通常让数据库库存明细与预占记录承担事实,内存 Map 只是单进程投影。定时任务按分片读取“到期且仍 HELD”的有限批次,通过状态条件更新实现幂等:只有成功把 HELD 改为 EXPIRED 的行才能释放对应库存。多仓不能只靠本地锁,因为同一任务可能在多个实例重试;常见做法是数据库事务、版本字段或带状态条件的更新,再用事务消息重建到期索引和聚合缓存。
如果使用纯内存模型,至少应由单线程事件所有者或覆盖五张 Map 的共同锁保护读写;读多写少也可先复制 facts,并从新 facts 重算 schedule、聚合和计数,再原子交换整个不可变状态。单独把 facts 换成 ConcurrentHashMap 不会让 TreeMap、聚合 Map、EnumMap 与缓存一起原子,也不会把两个不同 ReservationKey 的批量转换变成一个事务。
聚合量尤其适合做校验而不是第二真相:定期从所有 HELD facts 重算并与 heldQuantityBySku 对比,发现负数、零值残留或缺项就报警并重建。外部请求中的仓、订单、行号、SKU、数量与 limit 都应先校验;数量加法还应考虑整数溢出。日志不能泄露用户信息,错误应由真正能隔离批项或回滚事务的边界处理,不能捕获异常后假装整批成功。
随堂检查 3:把 facts 和 heldQuantityBySku 都换成 ConcurrentHashMap,并分别使用 compute,能否保证批量中两条事实、两条到期项、汇总量和状态计数一起提交?
**即时答案:**不能。每次 compute 的原子边界仍是单张 Map 的单个键;跨键、跨 Map、跨进程与崩溃恢复都需要更外层的事务、共同锁、单线程所有者或不可变聚合交换。
8. 错误示例:把缓存、零值和 fail-fast 当作业务保证
factKey = orderId // 漏 warehouseId/lineNo,事实覆盖
expiryCompare = expireMinuteOnly // 同分钟预占 compare==0,日历丢项
for (entry : expiredView) schedule.remove(key) // 迭代时绕过迭代器修改根树
facts.put(key, EXPIRED) // 只改事实,不删日历与扣聚合
heldQuantity.merge(skuKey, -qty, sum) // 缺键时反而创建负数
heldQuantity.put(skuKey, 0) // 破坏“缺键即零”的合同
cache eviction -> facts.remove(key) // 缓存淘汰误删库存事实
catch ConcurrentModificationException // 把 fail-fast 当线程安全和重试
也不要把未知位置的 LinkedList 删除说成 O(1):没有节点引用时先找候选仍需 O(n)。TreeMap 解决的是有序导航,HashMap 解决的是映射定位,两者的单项优势都不能证明五张表的一致性。
9. 正确示例:只验证集合合同,不交付三个 TODO
下面是独立的合同演示。它用不同数据手工处理已知的 C、B 两项,展示严格前缀、limit(2) 选择、共享聚合一项保留一项归零删除、缓存重新进入和活视图变化;它没有 expirySnapshot、confirm、expireBatch 方法,没有缺失/状态/索引失配分支,也没有整批原子性方案,因此不能替代今日编码题答案。
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;
public final class ReservationBatchContractsDemo {
private enum State {
HELD,
CONFIRMED,
EXPIRED
}
private record ReservationKey(
String warehouseId,
String orderId,
int lineNo) {
ReservationKey {
Objects.requireNonNull(warehouseId, "warehouseId");
Objects.requireNonNull(orderId, "orderId");
if (warehouseId.isBlank() || orderId.isBlank() || lineNo <= 0) {
throw new IllegalArgumentException("invalid reservation key");
}
}
String label() {
return warehouseId + "/" + orderId + "/" + lineNo;
}
}
private record StockKey(
String warehouseId,
String sku) implements Comparable<StockKey> {
StockKey {
Objects.requireNonNull(warehouseId, "warehouseId");
Objects.requireNonNull(sku, "sku");
}
@Override
public int compareTo(StockKey other) {
int byWarehouse = warehouseId.compareTo(other.warehouseId);
return byWarehouse != 0
? byWarehouse
: sku.compareTo(other.sku);
}
String label() {
return warehouseId + "/" + sku;
}
}
private record Reservation(
ReservationKey key,
String sku,
int quantity,
long expireMinute,
State state) {
Reservation {
Objects.requireNonNull(key, "key");
Objects.requireNonNull(sku, "sku");
Objects.requireNonNull(state, "state");
if (sku.isBlank() || quantity <= 0 || expireMinute < 0) {
throw new IllegalArgumentException("invalid reservation");
}
}
}
private record ExpiryKey(
long expireMinute,
String warehouseId,
String orderId,
int lineNo) implements Comparable<ExpiryKey> {
static ExpiryKey from(Reservation reservation) {
return new ExpiryKey(
reservation.expireMinute(),
reservation.key().warehouseId(),
reservation.key().orderId(),
reservation.key().lineNo());
}
@Override
public int compareTo(ExpiryKey other) {
int byMinute = Long.compare(expireMinute, other.expireMinute);
if (byMinute != 0) {
return byMinute;
}
int byWarehouse = warehouseId.compareTo(other.warehouseId);
if (byWarehouse != 0) {
return byWarehouse;
}
int byOrder = orderId.compareTo(other.orderId);
return byOrder != 0
? byOrder
: Integer.compare(lineNo, other.lineNo);
}
String label() {
return warehouseId + "/" + orderId + "/" + lineNo
+ "@" + expireMinute;
}
}
public static void main(String[] args) {
Reservation a = reservation("wh-east", "o-701", 1,
"sku-red", 3, 100);
Reservation b = reservation("wh-west", "o-702", 1,
"sku-red", 4, 90);
Reservation c = reservation("wh-east", "o-703", 2,
"sku-red", 2, 90);
Reservation d = reservation("wh-east", "o-704", 1,
"sku-blue", 5, 95);
Map<ReservationKey, Reservation> facts = new HashMap<>();
NavigableMap<ExpiryKey, ReservationKey> schedule = new TreeMap<>();
Map<StockKey, Integer> held = new HashMap<>();
for (Reservation reservation : List.of(a, b, c, d)) {
facts.put(reservation.key(), reservation);
schedule.put(ExpiryKey.from(reservation), reservation.key());
held.merge(skuKey(reservation), reservation.quantity(), Integer::sum);
}
LinkedHashMap<ReservationKey, Reservation> recent =
new LinkedHashMap<>(4, 0.75f, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<ReservationKey, Reservation> eldest) {
return size() > 3;
}
};
recent.put(a.key(), a);
recent.put(b.key(), b);
recent.put(c.key(), c);
recent.get(a.key());
recent.put(d.key(), d);
EnumMap<State, Integer> counts = new EnumMap<>(State.class);
counts.put(State.HELD, 4);
ExpiryKey upper = new ExpiryKey(96, "", "", Integer.MIN_VALUE);
NavigableMap<ExpiryKey, ReservationKey> liveEligible =
schedule.headMap(upper, false);
List<String> snapshot = liveEligible.keySet().stream()
.map(ExpiryKey::label)
.toList();
List<ReservationKey> selected = liveEligible.values().stream()
.limit(2)
.toList();
System.out.println("schedule-before=" + expiryLabels(schedule));
System.out.println("held-before=" + heldLines(held));
System.out.println("cache-before=" + reservationLabels(recent));
System.out.println("eligible-before=" + expiryLabels(liveEligible));
System.out.println("selected=" + selected.stream()
.map(ReservationKey::label).toList());
Reservation expiredC = new Reservation(
c.key(), c.sku(), c.quantity(), c.expireMinute(), State.EXPIRED);
Reservation expiredB = new Reservation(
b.key(), b.sku(), b.quantity(), b.expireMinute(), State.EXPIRED);
facts.put(c.key(), expiredC);
facts.put(b.key(), expiredB);
schedule.remove(ExpiryKey.from(c));
schedule.remove(ExpiryKey.from(b));
held.compute(skuKey(c), (ignored, current) ->
current - c.quantity());
held.compute(skuKey(b), (ignored, current) -> {
int next = current - b.quantity();
return next == 0 ? null : next;
});
counts.merge(State.HELD, -2, Integer::sum);
counts.merge(State.EXPIRED, 2, Integer::sum);
recent.put(c.key(), expiredC);
recent.put(b.key(), expiredB);
System.out.println("schedule-after=" + expiryLabels(schedule));
System.out.println("eligible-after=" + expiryLabels(liveEligible));
System.out.println("snapshot-stable=" + snapshot);
System.out.println("held-after=" + heldLines(held));
System.out.println("counts-after=" + counts);
System.out.println("cache-after=" + reservationLabels(recent));
System.out.println("states=" + facts.get(c.key()).state()
+ "," + facts.get(b.key()).state());
}
private static Reservation reservation(
String warehouseId,
String orderId,
int lineNo,
String sku,
int quantity,
long expireMinute) {
ReservationKey key = new ReservationKey(warehouseId, orderId, lineNo);
return new Reservation(
key, sku, quantity, expireMinute, State.HELD);
}
private static StockKey skuKey(Reservation reservation) {
return new StockKey(
reservation.key().warehouseId(), reservation.sku());
}
private static List<String> expiryLabels(
Map<ExpiryKey, ReservationKey> schedule) {
return schedule.keySet().stream().map(ExpiryKey::label).toList();
}
private static List<String> heldLines(
Map<StockKey, Integer> held) {
return held.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> entry.getKey().label() + "=" + entry.getValue())
.toList();
}
private static List<String> reservationLabels(
Map<ReservationKey, Reservation> reservations) {
return reservations.keySet().stream()
.map(ReservationKey::label)
.toList();
}
}
预期输出:
schedule-before=[wh-east/o-703/2@90, wh-west/o-702/1@90, wh-east/o-704/1@95, wh-east/o-701/1@100]
held-before=[wh-east/sku-blue=5, wh-east/sku-red=5, wh-west/sku-red=4]
cache-before=[wh-east/o-703/2, wh-east/o-701/1, wh-east/o-704/1]
eligible-before=[wh-east/o-703/2@90, wh-west/o-702/1@90, wh-east/o-704/1@95]
selected=[wh-east/o-703/2, wh-west/o-702/1]
schedule-after=[wh-east/o-704/1@95, wh-east/o-701/1@100]
eligible-after=[wh-east/o-704/1@95]
snapshot-stable=[wh-east/o-703/2@90, wh-west/o-702/1@90, wh-east/o-704/1@95]
held-after=[wh-east/sku-blue=5, wh-east/sku-red=3]
counts-after={HELD=2, EXPIRED=2}
cache-after=[wh-east/o-704/1, wh-east/o-703/2, wh-west/o-702/1]
states=EXPIRED,EXPIRED
10. 边界总结:六类红线与本题放行条件
HashMap、HashSet 的遇见顺序不稳定;批量顺序只能来自完整的 ExpiryKey 全序,缓存热度来自明确的 access-order。
ReservationKey 的 equals/hashCode 必须覆盖同一组不可变身份字段;Comparator 为 0 必须只代表同一到期投影,不能漏仓、单、行。
导航视图和集合视图背靠根 Map,只读包装不等于不可变快照;跨边界报告必须复制并说明对象所有权。
未知节点位置时 LinkedList 的查找和删除整体仍是 O(n),不能把已持有节点后的局部改链冒充完整复杂度。
fail-fast 只是尽力发现结构修改误用,不提供互斥、可见性、线程安全或批次回滚;正确迭代删除也不等于事务。
ConcurrentHashMap 的单键 compute 不会让多 ReservationKey、TreeMap、聚合 HashMap、EnumMap 与缓存自动原子;应使用共同锁、单线程所有者、不可变聚合交换或数据库事务。
还要守住三个领域边界:缓存淘汰不删除事实;聚合缺项和数量不足是索引失配,不能扣成负数;计数或聚合降到零时删除键,保持“缺键即零”的合同。maxItems=0 必须空返回且无副作用,负数参数先失败。putIfAbsent/compute/merge/remove(key,value) 的返回值应参与状态校验,WeakHashMap 与 IdentityHashMap 不承担稳定业务身份。
一句话收束:**事实回答“是哪笔预占”,日历回答“谁先过期”,仓 SKU 汇总回答“还占多少”,limit 控制本批工作量,外层一致性边界保证这些答案同时成立。**只有大大提交完整闯关答案,Java 21 编译运行符合预期,总分至少 80 且六类红线均未命中,challenge.map 才能从 covered_unverified 变为 mastered 并进入 Set。
返回今日索引
返回今日索引
编码闯关:多仓库库存预占过期批处理索引
评估项:challenge.map。建议用时:18~20 分钟。本题必须提交完整 Java 21 代码、成功编译证据与逐行一致的运行输出;只写思路不能通过 Map 阶段闯关。
你要为仓储服务完成一个单线程库存预占索引。一条预占既要按“仓库 + 订单 + 行号”精确查询,又要按过期分钟批量释放;确认或过期时还要扣减“仓库 + SKU”的已预占量、迁移状态计数并更新最近缓存。今天的主轴是有界批处理与聚合归零删键 ,不再重复昨日的排序字段重排。
1. 业务背景
同一订单号可以出现在多个仓库,一张订单也可以有多个行项。因此事实键必须是不可变 ReservationKey(warehouseId, orderId, lineNo);sku、过期分钟、数量和状态都属于 value,不能缩减或改写事实身份。
五张 Map 的职责分开:
facts:HashMap<ReservationKey, StockReservation>,唯一事实来源。确认和过期都保留事实,只用新的不可变 value 迁移状态。
expirySchedule:TreeMap<ExpiryKey, ReservationKey>,只保存 HELD 预占的过期投影,按过期分钟、仓库、订单、行号升序补足全序。
heldQuantityBySku:HashMap<StockKey, Integer>,StockKey(warehouseId, sku) 不跨仓混合库存;确认或过期扣到 0 时删除聚合键。
recentCache:容量为 3 的 access-order LinkedHashMap;首次预占、缓存命中、确认和过期会改变热度,淘汰仅作用于缓存。
stateCounts:EnumMap<ReservationState, Integer>,按 HELD、CONFIRMED、EXPIRED 封闭状态域统计。
已实现的 reserve 负责写入种子数据。你只需补全时间窗快照、确认、有上限的过期批次三个位置。五张普通 Map 连续写入不是事务:生产环境要用共同锁、单线程事件所有者、不可变聚合交换或数据库事务覆盖完整转换,并能由事实表重建派生索引。
2. 约束与验收边界
基线为 Java 21,仅使用 JDK;不添加依赖、日志、调试输出或无法恢复问题的 try/catch。
ReservationKey 与 StockKey 均为不可变 record;仓库、订单、SKU 不能为空白,行号和预占数量为正数,过期分钟非负。
新预占必须是 HELD。已实现的 reserve 对完整事实键去重;重复预占返回 false,不重复聚合、不覆盖过期键、不刷新缓存热度。
ExpiryKey 按 expiresMinute、warehouseId、orderId、lineNo 升序比较;合法业务键上 compareTo == 0 必须恰好表示四个分量全部相等。
expirySnapshot(fromInclusive,toExclusive) 必须从 TreeMap 的 [fromInclusive,toExclusive) 活视图投影出稳定、不可修改的字符串快照,不得扫描 HashMap 后再排序。
confirm(key) 仅能转换存在且仍为 HELD 的预占;缺失或已终态返回 false 且无副作用。成功时删除过期投影、用新 value 改为 CONFIRMED、扣减对应仓库 SKU 的 HELD 聚合、迁移计数并更新缓存。
expireBatch(beforeExclusive,maxItems) 只处理过期分钟严格小于 beforeExclusive 的前 maxItems 项;结果顺序来自 TreeMap,maxItems == 0 合法且无副作用,负数参数在任何业务读写前失败。
确认和过期都要先核对事实状态与完整排序键;聚合量不得为负,减至 0 删键,不得留下“数值为 0 却仍存在”的假预占。
禁止依赖 HashMap/HashSet 顺序、破坏 equals/hashCode 或 Comparator 契约、混淆活视图/只读包装/快照、把 fail-fast 当线程安全,或把 ConcurrentHashMap 的单键能力当成跨键、跨 Map 事务。
3. 目标拆解与实施顺序
阅读已实现的 reserve,核对只有事实键首次出现时才建立过期投影、聚合、计数和缓存投影。
完成快照:用两个边界键得到半开导航视图,逐项回查 HELD 事实并固化字符串。
完成确认:在更改任何 Map 前核对完整过期键,再依次删除过期投影、替换事实、扣减聚合、迁移计数并更新缓存。
完成批量过期:先建立严格上界视图,每轮只取当前首项,核对后迁移事实及四个派生投影,到达上限即停止。
以 Java 21 编译、运行并逐行核对规定输出;提交完整代码、编译证据和输出。
4. 完整可编译的 Java 21 起始代码
保存为 WarehouseReservationExpiryChallenge.java。未补代码时仍能通过编译;直接运行会完成种子预占和缓存命中,然后在第一个待补位置以 TODO 1: expirySnapshot 明确失败。起始代码恰好只有 3 个待补位置,当天不提供完整实现。
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;
public final class WarehouseReservationExpiryChallenge {
private enum ReservationState {
HELD,
CONFIRMED,
EXPIRED
}
private record ReservationKey(
String warehouseId,
String orderId,
int lineNo) {
ReservationKey {
Objects.requireNonNull(warehouseId, "warehouseId");
Objects.requireNonNull(orderId, "orderId");
if (warehouseId.isBlank() || orderId.isBlank() || lineNo <= 0) {
throw new IllegalArgumentException("invalid reservation key");
}
}
String label() {
return warehouseId + "/" + orderId + "/" + lineNo;
}
}
private record StockKey(String warehouseId, String sku) {
StockKey {
Objects.requireNonNull(warehouseId, "warehouseId");
Objects.requireNonNull(sku, "sku");
if (warehouseId.isBlank() || sku.isBlank()) {
throw new IllegalArgumentException("invalid stock key");
}
}
}
private record StockReservation(
ReservationKey key,
String sku,
long expiresMinute,
int quantity,
ReservationState state) {
StockReservation {
Objects.requireNonNull(key, "key");
Objects.requireNonNull(sku, "sku");
Objects.requireNonNull(state, "state");
if (sku.isBlank() || expiresMinute < 0 || quantity <= 0) {
throw new IllegalArgumentException("invalid reservation");
}
}
StockKey stockKey() {
return new StockKey(key.warehouseId(), sku);
}
StockReservation confirmed() {
return new StockReservation(
key, sku, expiresMinute, quantity,
ReservationState.CONFIRMED);
}
StockReservation expired() {
return new StockReservation(
key, sku, expiresMinute, quantity,
ReservationState.EXPIRED);
}
String snapshotLine() {
return key.label() + ":" + sku + "@" + expiresMinute
+ "x" + quantity + ":" + state;
}
}
private record ExpiryKey(
long expiresMinute,
String warehouseId,
String orderId,
int lineNo) implements Comparable<ExpiryKey> {
static ExpiryKey from(StockReservation reservation) {
return new ExpiryKey(
reservation.expiresMinute(),
reservation.key().warehouseId(),
reservation.key().orderId(),
reservation.key().lineNo());
}
static ExpiryKey boundary(long minute) {
return new ExpiryKey(minute, "", "", Integer.MIN_VALUE);
}
@Override
public int compareTo(ExpiryKey other) {
int byMinute = Long.compare(expiresMinute, other.expiresMinute);
if (byMinute != 0) {
return byMinute;
}
int byWarehouse = warehouseId.compareTo(other.warehouseId);
if (byWarehouse != 0) {
return byWarehouse;
}
int byOrder = orderId.compareTo(other.orderId);
return byOrder != 0
? byOrder
: Integer.compare(lineNo, other.lineNo);
}
}
private static final class ReservationIndex {
private static final int RECENT_LIMIT = 3;
private final Map<ReservationKey, StockReservation> facts =
new HashMap<>();
private final NavigableMap<ExpiryKey, ReservationKey> expirySchedule =
new TreeMap<>();
private final Map<StockKey, Integer> heldQuantityBySku =
new HashMap<>();
private final LinkedHashMap<ReservationKey, StockReservation>
recentCache = new LinkedHashMap<>(4, 0.75f, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<
ReservationKey,
StockReservation> eldest) {
return size() > RECENT_LIMIT;
}
};
private final EnumMap<ReservationState, Integer> stateCounts =
new EnumMap<>(ReservationState.class);
private boolean reserve(StockReservation reservation) {
Objects.requireNonNull(reservation, "reservation");
if (reservation.state() != ReservationState.HELD) {
throw new IllegalArgumentException(
"new reservation must be held");
}
if (facts.putIfAbsent(reservation.key(), reservation) != null) {
return false;
}
// 事实新增成功后才建立派生投影;生产中需共同原子边界。
ReservationKey previous = expirySchedule.put(
ExpiryKey.from(reservation), reservation.key());
if (previous != null) {
throw new IllegalStateException("duplicate expiry key");
}
heldQuantityBySku.merge(
reservation.stockKey(),
reservation.quantity(),
Math::addExact);
stateCounts.merge(ReservationState.HELD, 1, Math::addExact);
recentCache.put(reservation.key(), reservation);
return true;
}
private List<String> expirySnapshot(
long fromInclusive,
long toExclusive) {
if (fromInclusive < 0 || toExclusive < 0
|| fromInclusive > toExclusive) {
throw new IllegalArgumentException("invalid expiry window");
}
// 从半开导航活视图产生拥有独立数据的字符串快照。
throw new UnsupportedOperationException(
"TODO 1: expirySnapshot");
}
private boolean confirm(ReservationKey key) {
// 只转换 HELD 事实,并同步删导航、扣聚合、迁计数和更新缓存。
throw new UnsupportedOperationException("TODO 2: confirm");
}
private List<String> expireBatch(
long beforeExclusive,
int maxItems) {
if (beforeExclusive < 0 || maxItems < 0) {
throw new IllegalArgumentException(
"expiry boundary and batch size must not be negative");
}
// 只处理严格上界内的前 maxItems 项,每项都要维护完整不变量。
throw new UnsupportedOperationException("TODO 3: expireBatch");
}
private void decreaseHeldQuantity(StockReservation reservation) {
StockKey stockKey = reservation.stockKey();
Integer current = heldQuantityBySku.get(stockKey);
if (current == null || current < reservation.quantity()) {
throw new IllegalStateException(
"held quantity is inconsistent with facts");
}
int remaining = current - reservation.quantity();
if (remaining == 0) {
heldQuantityBySku.remove(stockKey);
} else {
heldQuantityBySku.put(stockKey, remaining);
}
}
private void moveState(
ReservationState from,
ReservationState to) {
Integer current = stateCounts.get(from);
if (current == null || current <= 0) {
throw new IllegalStateException(
"state count is inconsistent with facts");
}
if (current == 1) {
stateCounts.remove(from);
} else {
stateCounts.put(from, current - 1);
}
stateCounts.merge(to, 1, Math::addExact);
}
private int heldQuantity(StockKey stockKey) {
return heldQuantityBySku.getOrDefault(
Objects.requireNonNull(stockKey, "stockKey"), 0);
}
private String recentState(ReservationKey key) {
StockReservation reservation = recentCache.get(
Objects.requireNonNull(key, "key"));
return reservation == null ? "MISS" : reservation.state().name();
}
private List<String> scheduleOrder() {
return expirySchedule.entrySet().stream()
.map(entry -> entry.getValue().label()
+ "@" + entry.getKey().expiresMinute())
.toList();
}
private List<String> recentOrder() {
return recentCache.keySet().stream()
.map(ReservationKey::label)
.toList();
}
private List<String> countSummary() {
return stateCounts.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.toList();
}
}
public static void main(String[] args) {
ReservationKey aKey = new ReservationKey("w-a", "o-7", 1);
ReservationKey bKey = new ReservationKey("w-b", "o-7", 1);
ReservationKey cKey = new ReservationKey("w-a", "o-8", 2);
ReservationKey dKey = new ReservationKey("w-a", "o-9", 1);
StockReservation a = new StockReservation(
aKey, "book", 600, 3, ReservationState.HELD);
StockReservation b = new StockReservation(
bKey, "book", 580, 4, ReservationState.HELD);
StockReservation c = new StockReservation(
cKey, "pen", 580, 2, ReservationState.HELD);
StockReservation d = new StockReservation(
dKey, "book", 620, 5, ReservationState.HELD);
ReservationIndex index = new ReservationIndex();
System.out.println("RESERVE A=" + index.reserve(a));
System.out.println("RESERVE B=" + index.reserve(b));
System.out.println("RESERVE C=" + index.reserve(c));
System.out.println("RESERVE D=" + index.reserve(d));
System.out.println("RESERVE duplicate-A=" + index.reserve(a));
System.out.println("CACHE initial=" + index.recentOrder());
System.out.println("CACHE access-B=" + index.recentState(bKey));
System.out.println("CACHE after-access=" + index.recentOrder());
List<String> beforeChanges = index.expirySnapshot(580, 601);
System.out.println("WINDOW before=" + beforeChanges);
System.out.println("CONFIRM C=" + index.confirm(cKey));
System.out.println("CACHE after-confirm=" + index.recentOrder());
System.out.println("HELD w-a/pen="
+ index.heldQuantity(new StockKey("w-a", "pen")));
System.out.println("COUNTS after-confirm=" + index.countSummary());
System.out.println("EXPIRE before-580 max-2="
+ index.expireBatch(580, 2));
System.out.println("EXPIRE before-601 max-1="
+ index.expireBatch(601, 1));
System.out.println("CACHE after-first-expire="
+ index.recentOrder());
System.out.println("EXPIRE before-601 max-3="
+ index.expireBatch(601, 3));
System.out.println("CACHE after-second-expire="
+ index.recentOrder());
System.out.println("COUNTS final=" + index.countSummary());
System.out.println("SCHEDULE final=" + index.scheduleOrder());
System.out.println("HELD w-a/book="
+ index.heldQuantity(new StockKey("w-a", "book")));
System.out.println("HELD w-b/book="
+ index.heldQuantity(new StockKey("w-b", "book")));
System.out.println("HELD w-a/pen="
+ index.heldQuantity(new StockKey("w-a", "pen")));
System.out.println("SNAPSHOT unchanged=" + beforeChanges);
}
}
直接编译运行:
javac --release 21 WarehouseReservationExpiryChallenge.java
java WarehouseReservationExpiryChallenge
5. 三个待补位置的验收条件
位置 1:半开过期窗口转稳定快照
用 ExpiryKey.boundary 与四参数 subMap 表达 [fromInclusive,toExclusive),不遍历 facts 再排序。
按视图顺序回查事实,核对状态仍为 HELD、ExpiryKey.from(reservation) 等于当前树键;失配必须暴露索引损坏。
投影为 snapshotLine() 并返回不可修改列表;后续确认、过期或树删除均不得改变旧字符串。
[x,x) 合法且为空;负数边界或下界大于上界时,在读取业务 Map 前失败。
位置 2:确认一条预占
key 为 null 时先失败;事实缺失、CONFIRMED 或 EXPIRED 时返回 false,五张 Map 均不变。
用当前事实构造完整 ExpiryKey,确认树中恰好映射到该事实键;删除失败不能伪装成确认成功。
使用新 CONFIRMED value 替换事实,调用已给的辅助方法扣减该仓 SKU 聚合、从 HELD 迁移计数,并以新 value 写入缓存。
聚合减至 0 必须删键;缓存中已存在的 C 会移到最热端,但不影响事实寿命。
位置 3:严格上界与批次上限
用 headMap(ExpiryKey.boundary(beforeExclusive), false) 建立严格上界视图;边界分钟的全部业务键都必须被排除。
最多处理 maxItems 项,每轮取视图当前首项;无候选或上限为 0 时返回空且无副作用。
每项都要核对 HELD 事实与完整排序键,然后删树项、以新 EXPIRED value 替换事实、扣减聚合、迁移计数、更新缓存,并按树序返回完整事实标签。
批次循环只提供数量上限,不提供跨 Map 事务;生产中还需共同原子边界、幂等重试或可重建投影。
6. 三级提示
一级提示:边界键要排在同分钟所有业务键之前
ExpiryKey.boundary(minute) 使用空仓库和空订单。合法身份都是非空白字符串,所以边界键排在该分钟业务键之前:包含下界边界会收进下界整分钟,排除上界边界会排除上界整分钟。
二级提示:聚合键不是事实键
扣减聚合时用 (warehouseId, sku),而删除过期项和替换事实时用 (warehouseId, orderId, lineNo)。聚合为 0 删键只表示当前该仓 SKU 没有 HELD 数量,不能删除已确认或已过期的事实。
三级提示:每轮处理当前首项
批处理不要在遍历器上一边迭代一边用根 Map 删除。可以在每轮重新取严格上界视图的 firstEntry,核对完整不变量后删除当前项。返回列表则另行复制,不暴露活视图。
7. 精确预期输出
补全三个待补位置后,实际输出必须逐行一致:
RESERVE A=true
RESERVE B=true
RESERVE C=true
RESERVE D=true
RESERVE duplicate-A=false
CACHE initial=[w-b/o-7/1, w-a/o-8/2, w-a/o-9/1]
CACHE access-B=HELD
CACHE after-access=[w-a/o-8/2, w-a/o-9/1, w-b/o-7/1]
WINDOW before=[w-a/o-8/2:pen@580x2:HELD, w-b/o-7/1:book@580x4:HELD, w-a/o-7/1:book@600x3:HELD]
CONFIRM C=true
CACHE after-confirm=[w-a/o-9/1, w-b/o-7/1, w-a/o-8/2]
HELD w-a/pen=0
COUNTS after-confirm=[HELD=3, CONFIRMED=1]
EXPIRE before-580 max-2=[]
EXPIRE before-601 max-1=[w-b/o-7/1]
CACHE after-first-expire=[w-a/o-9/1, w-a/o-8/2, w-b/o-7/1]
EXPIRE before-601 max-3=[w-a/o-7/1]
CACHE after-second-expire=[w-a/o-8/2, w-b/o-7/1, w-a/o-7/1]
COUNTS final=[HELD=1, CONFIRMED=1, EXPIRED=2]
SCHEDULE final=[w-a/o-9/1@620]
HELD w-a/book=5
HELD w-b/book=0
HELD w-a/pen=0
SNAPSHOT unchanged=[w-a/o-8/2:pen@580x2:HELD, w-b/o-7/1:book@580x4:HELD, w-a/o-7/1:book@600x3:HELD]
验收时还要解释:第四次预占为何只把 A 挤出缓存;重复 A 为何不能升温;命中 B 为何得到 [C,D,B];同为 580 时 C 为何先于 B;确认 C 为何删除 w-a/pen 聚合键;严格上界 580 为何一项也不处理;两次过期为何分别只处理 B 和 A;旧快照为何仍全部显示 HELD。这些顺序不得来自 HashMap 的遇见次序。
8. 复杂度要求
reserve 的事实表和聚合表写入期望 O(1),过期树插入 O(log n) 主导,整次为 O(log n)。HashMap 碰撞、扩容和树化使单次严格 O(1) 不成立。
窗口边界定位加 k 项固化为 O(log n + k) 时间,快照额外空间为 O(k)。
confirm 含一次 TreeMap 删除,整次为 O(log n);事实替换、聚合扣减、计数与缓存更新为常数或期望 O(1)。
批次实际处理 m <= maxItems 项时,时间为 O(m log n),返回标签的额外空间为 O(m);上限只控制单批工作量,不改变总数据规模。
heldQuantityBySku 最多按有 HELD 量的仓库 SKU 组合增长,上界 O(n);事实表与过期树为 O(n),固定状态表和容量 3 缓存为 O(1)。
9. 边界用例
补全后至少自行验证:
不同仓库的同订单同行号可并存,同订单的不同行也可并存;事实键不含可变状态。
重复预占返回 false,事实、日程、聚合和计数不重复,已被淘汰项也不借此返回缓存。
确认缺失或已终态事实返回 false 且无副作用;事实与过期键失配时明确失败。
聚合扣减不得为负;减到 0 删除键,但 facts 中的 CONFIRMED/EXPIRED 事实仍存在。
[580,580) 是空快照;负数边界或下界大于上界时不读写业务状态。
expireBatch(580,2) 不包含恰在 580 分钟的 B;maxItems == 0 不处理,负数上限先失败。
同分钟依仓库、订单、行号全序不覆盖;不依赖 HashMap/HashSet 顺序。
确认和过期会让活导航视图变化,旧字符串快照不变,只读包装也不等于快照。
批量循环、fail-fast 或 ConcurrentHashMap 单键方法均不提供跨五张 Map 事务。
10. 闯关提交与自检
11. Java 21 与固定源码资料
返回今日索引
返回今日索引
Map 阶段闯关题:多仓库库存预占过期批处理索引
建议用时:约 8~9 分钟。请优先在 互动页面 作答,聊天提交仅作补充。当天不提供答案。 第 1~3、5 题提交结论与最短必要推理;第 4 题必须提交完整 Java 21 代码、编译证据和规定输出。
题号
固定维度
知识点 ID
分值
可能触发的红线
1
契约与选型
ds.map.contract、ds.map.equality、ds.map.linkedhashmap-source、ds.map.treemap-source、ds.map.specialized
25
假设 HashMap/HashSet 顺序稳定;破坏 equals/hashCode 或 Comparator;把聚合键、过期键当成事实身份
2
复杂度与状态推演
ds.map.hashmap-structure、ds.map.hashmap-put-get-remove-source、ds.map.hashmap-resize-treeify-iterator-source、ds.map.treemap-source
12
把期望复杂度写成严格保证;认为未知节点位置时 LinkedList 中间操作天然 O(1);把 fail-fast 当线程安全
3
复杂度与状态推演
ds.map.contract、ds.map.compute-merge-views、ds.map.linkedhashmap-source、ds.map.treemap-source、ds.map.specialized
13
混淆活视图、只读包装与快照;聚合归零不删键;把批次或多 Map 更新当事务
4
编码
ds.map.contract、ds.map.equality、ds.map.compute-merge-views、ds.map.linkedhashmap-source、ds.map.treemap-source、ds.map.specialized
30
半开/严格上界错误、批次越限、事实或聚合失配;把 ConcurrentHashMap 误认为跨键或跨集合自动原子
5
源码讲解
ds.map.hashmap-structure、ds.map.hashmap-put-get-remove-source、ds.map.hashmap-resize-treeify-iterator-source、ds.map.compute-merge-views、ds.map.linkedhashmap-source、ds.map.treemap-source、ds.map.specialized
20
猜测源码或红黑树形状;把实现细节当公共契约;把 fail-fast、缓存或单 Map 方法当并发事务
合计
契约与选型 25;复杂度与状态推演 25;编码 30;源码讲解 20
覆盖 Map 9 个知识项
100
闯关放行必须同时满足:总分至少 80/100、第 4 题 Java 21 完整代码成功编译运行且产生规定输出、六类红线均未命中。只达到分数、只交片段、编译失败或命中任一红线,都不能进入 Set;应根据有效证据补强并重闯。
1. 契约与方案设计:事实、过期、聚合与缓存(25 分)
多仓库预占服务要支持事实精确查询、按分钟过期、每仓 SKU 的 HELD 数量、封闭状态计数和容量 3 的最近访问缓存。用最多 9 条定义五张 Map 的选型、事实/派生关系和不变量,并说明:ReservationKey 与 StockKey 各自的身份边界,null 键/value 策略,ExpiryKey 如何补足全序,聚合归零为何删键,access-order 命中与淘汰的语义,导航活视图与对外快照的所有权。解释为何 HashMap/HashSet 顺序不能表示过期次序,以及 WeakHashMap、IdentityHashMap 为何不适合持久业务事实。
2. 复杂度与结构推演:批处理上限不改变单项成本(12 分)
设事实数为 n,时间窗命中 k 项,一次批处理实际过期 m <= maxItems 项。给出事实精确查询、首次预占、窗口转快照、确认、批量过期、聚合扣减、枚举计数和有界缓存的时间/空间复杂度。说明 HashMap 碰撞、扩容与 OpenJDK 21 树化容量门槛为何否定“每次严格 O(1)”;再比较不知候选节点位置时 LinkedList 查找/删除与 TreeMap 边界导航的完整成本,并指出 fail-fast 没有提供的互斥、可见性、原子性与一致快照性质。
3. 状态推演与代码分析:确认、严格过期与归零删键(13 分)
最近缓存容量为 2。依次预占 X=w-x/o-1/1:book@700x2、Y=w-y/o-1/1:book@680x3、Z=w-x/o-2/2:pen@680x4,再命中 Y;随后取得 [680,701) 的导航活视图并投影为不可变字符串快照,确认 Z,执行 expireBatch(700,1),最后执行 expireBatch(701,2)。
逐步写出过期树、最近缓存、HELD/CONFIRMED/EXPIRED 计数和三个仓库 SKU 聚合的变化;写出两次批处理的返回项、最终事实状态、过期树、旧活视图当前内容和旧快照内容。说明同分钟为何 Z 先于 Y,严格上界 700 为何排除 X,聚合为 0 为何删键,以及已被淘汰的 X 过期后如何重新进入缓存并触发新淘汰。不得依赖 HashMap 顺序,不得把批处理或多 Map 写入称为自动事务。
4. 必交编码:完成快照、确认与过期批次(30 分)
补全 编码练习 的 3 个待补位置,提交完整 WarehouseReservationExpiryChallenge.java、javac --release 21 WarehouseReservationExpiryChallenge.java 的成功结果,以及 java WarehouseReservationExpiryChallenge 的完整且逐行一致输出。再用不超过 7 句话说明重复预占、半开快照、确认、严格过期上界、批次上限、聚合归零删键、缓存重新进入和跨 Map 原子性边界。
评分拆分:半开导航视图转稳定快照 7 分;确认、聚合与状态迁移 8 分;严格上界批处理、上限与缓存 9 分;Java 21 编译、规定输出和边界说明 6 分。缺少完整代码、编译失败或输出不符时,编码维度不能视为通过,整个 Map 闯关也不得放行。
5. 源码追踪:用固定入口解释批处理结果(20 分)
固定版本为 OpenJDK jdk-21+35。用 5 行表格作答,每行包含“问题、公开入口、关键字段、最多 4 个箭头节点的主路径、可观察结果或扩展点”,分别覆盖:HashMap 复合键查询、首次写入、事实替换、聚合 merge/get/put/remove;阈值、扩容高低链拆分、树化容量条件与迭代器结构修改检测;LinkedHashMap access-order 的命中、已有键更新与最老项钩子;TreeMap 的比较定位、subMap/headMap、当前首项及视图删除;EnumMap 的枚举槽位与三状态迁移。
每行 4 分。必须从源码路径回到本题的返回值、size、日程顺序、范围、聚合、缓存和状态结果;不能把方法名清单当答案,不能猜红黑树具体形状,也不能把内部阈值、fail-fast、普通 Map 或 ConcurrentHashMap 单键能力扩张成业务顺序、线程安全或跨键/跨 Map 事务。
返回今日索引
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;
public final class WarehouseReservationExpiryChallenge {
private enum ReservationState {
HELD,
CONFIRMED,
EXPIRED
}
private record ReservationKey(
String warehouseId,
String orderId,
int lineNo) {
ReservationKey {
Objects.requireNonNull(warehouseId, "warehouseId");
Objects.requireNonNull(orderId, "orderId");
if (warehouseId.isBlank() || orderId.isBlank() || lineNo <= 0) {
throw new IllegalArgumentException("invalid reservation key");
}
}
String label() {
return warehouseId + "/" + orderId + "/" + lineNo;
}
}
private record StockKey(String warehouseId, String sku) {
StockKey {
Objects.requireNonNull(warehouseId, "warehouseId");
Objects.requireNonNull(sku, "sku");
if (warehouseId.isBlank() || sku.isBlank()) {
throw new IllegalArgumentException("invalid stock key");
}
}
}
private record StockReservation(
ReservationKey key,
String sku,
long expiresMinute,
int quantity,
ReservationState state) {
StockReservation {
Objects.requireNonNull(key, "key");
Objects.requireNonNull(sku, "sku");
Objects.requireNonNull(state, "state");
if (sku.isBlank() || expiresMinute < 0 || quantity <= 0) {
throw new IllegalArgumentException("invalid reservation");
}
}
StockKey stockKey() {
return new StockKey(key.warehouseId(), sku);
}
StockReservation confirmed() {
return new StockReservation(
key, sku, expiresMinute, quantity,
ReservationState.CONFIRMED);
}
StockReservation expired() {
return new StockReservation(
key, sku, expiresMinute, quantity,
ReservationState.EXPIRED);
}
String snapshotLine() {
return key.label() + ":" + sku + "@" + expiresMinute
+ "x" + quantity + ":" + state;
}
}
private record ExpiryKey(
long expiresMinute,
String warehouseId,
String orderId,
int lineNo) implements Comparable<ExpiryKey> {
static ExpiryKey from(StockReservation reservation) {
return new ExpiryKey(
reservation.expiresMinute(),
reservation.key().warehouseId(),
reservation.key().orderId(),
reservation.key().lineNo());
}
static ExpiryKey boundary(long minute) {
return new ExpiryKey(minute, "", "", Integer.MIN_VALUE);
}
@Override
public int compareTo(ExpiryKey other) {
int byMinute = Long.compare(expiresMinute, other.expiresMinute);
if (byMinute != 0) {
return byMinute;
}
int byWarehouse = warehouseId.compareTo(other.warehouseId);
if (byWarehouse != 0) {
return byWarehouse;
}
int byOrder = orderId.compareTo(other.orderId);
return byOrder != 0
? byOrder
: Integer.compare(lineNo, other.lineNo);
}
}
private static final class ReservationIndex {
private static final int RECENT_LIMIT = 3;
private final Map<ReservationKey, StockReservation> facts =
new HashMap<>();
private final NavigableMap<ExpiryKey, ReservationKey> expirySchedule =
new TreeMap<>();
private final Map<StockKey, Integer> heldQuantityBySku =
new HashMap<>();
private final LinkedHashMap<ReservationKey, StockReservation>
recentCache = new LinkedHashMap<>(4, 0.75f, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<
ReservationKey,
StockReservation> eldest) {
return size() > RECENT_LIMIT;
}
};
private final EnumMap<ReservationState, Integer> stateCounts =
new EnumMap<>(ReservationState.class);
private boolean reserve(StockReservation reservation) {
Objects.requireNonNull(reservation, "reservation");
if (reservation.state() != ReservationState.HELD) {
throw new IllegalArgumentException(
"new reservation must be held");
}
if (facts.putIfAbsent(reservation.key(), reservation) != null) {
return false;
}
// 事实新增成功后才建立派生投影;生产中需共同原子边界。
ReservationKey previous = expirySchedule.put(
ExpiryKey.from(reservation), reservation.key());
if (previous != null) {
throw new IllegalStateException("duplicate expiry key");
}
heldQuantityBySku.merge(
reservation.stockKey(),
reservation.quantity(),
Math::addExact);
stateCounts.merge(ReservationState.HELD, 1, Math::addExact);
recentCache.put(reservation.key(), reservation);
return true;
}
private List<String> expirySnapshot(
long fromInclusive,
long toExclusive) {
if (fromInclusive < 0 || toExclusive < 0
|| fromInclusive > toExclusive) {
throw new IllegalArgumentException("invalid expiry window");
}
// 从半开导航活视图产生拥有独立数据的字符串快照。
throw new UnsupportedOperationException(
"TODO 1: expirySnapshot");
}
private boolean confirm(ReservationKey key) {
// 只转换 HELD 事实,并同步删导航、扣聚合、迁计数和更新缓存。
throw new UnsupportedOperationException("TODO 2: confirm");
}
private List<String> expireBatch(
long beforeExclusive,
int maxItems) {
if (beforeExclusive < 0 || maxItems < 0) {
throw new IllegalArgumentException(
"expiry boundary and batch size must not be negative");
}
// 只处理严格上界内的前 maxItems 项,每项都要维护完整不变量。
throw new UnsupportedOperationException("TODO 3: expireBatch");
}
private void decreaseHeldQuantity(StockReservation reservation) {
StockKey stockKey = reservation.stockKey();
Integer current = heldQuantityBySku.get(stockKey);
if (current == null || current < reservation.quantity()) {
throw new IllegalStateException(
"held quantity is inconsistent with facts");
}
int remaining = current - reservation.quantity();
if (remaining == 0) {
heldQuantityBySku.remove(stockKey);
} else {
heldQuantityBySku.put(stockKey, remaining);
}
}
private void moveState(
ReservationState from,
ReservationState to) {
Integer current = stateCounts.get(from);
if (current == null || current <= 0) {
throw new IllegalStateException(
"state count is inconsistent with facts");
}
if (current == 1) {
stateCounts.remove(from);
} else {
stateCounts.put(from, current - 1);
}
stateCounts.merge(to, 1, Math::addExact);
}
private int heldQuantity(StockKey stockKey) {
return heldQuantityBySku.getOrDefault(
Objects.requireNonNull(stockKey, "stockKey"), 0);
}
private String recentState(ReservationKey key) {
StockReservation reservation = recentCache.get(
Objects.requireNonNull(key, "key"));
return reservation == null ? "MISS" : reservation.state().name();
}
private List<String> scheduleOrder() {
return expirySchedule.entrySet().stream()
.map(entry -> entry.getValue().label()
+ "@" + entry.getKey().expiresMinute())
.toList();
}
private List<String> recentOrder() {
return recentCache.keySet().stream()
.map(ReservationKey::label)
.toList();
}
private List<String> countSummary() {
return stateCounts.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.toList();
}
}
public static void main(String[] args) {
ReservationKey aKey = new ReservationKey("w-a", "o-7", 1);
ReservationKey bKey = new ReservationKey("w-b", "o-7", 1);
ReservationKey cKey = new ReservationKey("w-a", "o-8", 2);
ReservationKey dKey = new ReservationKey("w-a", "o-9", 1);
StockReservation a = new StockReservation(
aKey, "book", 600, 3, ReservationState.HELD);
StockReservation b = new StockReservation(
bKey, "book", 580, 4, ReservationState.HELD);
StockReservation c = new StockReservation(
cKey, "pen", 580, 2, ReservationState.HELD);
StockReservation d = new StockReservation(
dKey, "book", 620, 5, ReservationState.HELD);
ReservationIndex index = new ReservationIndex();
System.out.println("RESERVE A=" + index.reserve(a));
System.out.println("RESERVE B=" + index.reserve(b));
System.out.println("RESERVE C=" + index.reserve(c));
System.out.println("RESERVE D=" + index.reserve(d));
System.out.println("RESERVE duplicate-A=" + index.reserve(a));
System.out.println("CACHE initial=" + index.recentOrder());
System.out.println("CACHE access-B=" + index.recentState(bKey));
System.out.println("CACHE after-access=" + index.recentOrder());
List<String> beforeChanges = index.expirySnapshot(580, 601);
System.out.println("WINDOW before=" + beforeChanges);
System.out.println("CONFIRM C=" + index.confirm(cKey));
System.out.println("CACHE after-confirm=" + index.recentOrder());
System.out.println("HELD w-a/pen="
+ index.heldQuantity(new StockKey("w-a", "pen")));
System.out.println("COUNTS after-confirm=" + index.countSummary());
System.out.println("EXPIRE before-580 max-2="
+ index.expireBatch(580, 2));
System.out.println("EXPIRE before-601 max-1="
+ index.expireBatch(601, 1));
System.out.println("CACHE after-first-expire="
+ index.recentOrder());
System.out.println("EXPIRE before-601 max-3="
+ index.expireBatch(601, 3));
System.out.println("CACHE after-second-expire="
+ index.recentOrder());
System.out.println("COUNTS final=" + index.countSummary());
System.out.println("SCHEDULE final=" + index.scheduleOrder());
System.out.println("HELD w-a/book="
+ index.heldQuantity(new StockKey("w-a", "book")));
System.out.println("HELD w-b/book="
+ index.heldQuantity(new StockKey("w-b", "book")));
System.out.println("HELD w-a/pen="
+ index.heldQuantity(new StockKey("w-a", "pen")));
System.out.println("SNAPSHOT unchanged=" + beforeChanges);
}
}
互动保存需要 JavaScript。请启用 JavaScript,并通过本地启动器或已部署的学习地址访问此页面。