Java常用类库-19-EventBus的订阅关系与派发边界
事件发布隐藏了哪些调用关系 商品目录更新后,需要刷新界面上的商品计数。直接调用一个监听器十分明确:调用方知道接口类型,也知道异常会从哪里返回。改用 EventBus 后,发布方只需要 post 一个事件,订阅者通过注册和注解接收它。方法调用被移到运行时的订阅关系中,依赖并未消失,只是无法从发布语句直接看出全部接收者。 这会影响维护。商品计数没有更新,可能是事件类型不匹配、订阅对象没有注册、已经注销,或订阅方法执行失败。如果把库存扣减、通知、界面刷新都放在同一条事件上,发布方还可能无法判断哪些操作已经成功。使用事件总线之前,先确定业务是否接受这种调用和失败模型。 Guava 当前固定版本的 EventBus 文档明确建议避免新用法,并列出依赖注入、响应式编程等替代方向。本文研究 Guava 33.5.0-jre 的既有行为,用于理解和维护已有系统;是否引入新框架仍应由业务复杂度决定。一个明确的监听器接口常常就足够。固定版本 EventBus 类文档 本章限定在同一进程内的默认 EventBus。完整实验使用 Java 8 源码,在 Zulu 8u472 和 Corretto 21....
Java常用类库-18-Hashing与BloomFilter的编码和误判边界
未找到和可能存在是不同结果 商品目录很大时,可以先用 BloomFilter 判断某个身份是否可能存在,再决定是否查询精确索引。过滤器返回 false 时可以排除,返回 true 时仍需查询真实目录。若把 true 直接当成“商品存在”,误判就会进入业务结果;若用它判断新商品是否重复并直接拒绝,也会误拒尚未存在的商品。 Guava 33.5.0-jre 的 BloomFilter 提供单侧错误的近似包含测试。这个语义以插入与查询使用一致表示、过滤器包含所需数据等前提为基础。哈希函数、字节编码、对象拆分方式和生命周期都参与结果,不能仅看到 BloomFilter 这个类名就推断整个系统没有漏查。BloomFilter 文档 本文使用 Java 8 基线,在 Zulu 8u472 与 Corretto 21.0.11 分别执行。实验先验证字节输入,再以精确 Set 标注样本真实成员关系,最后观察配置容量与实际插入量不同的误判率。固定种子用于复现输入,不用来证明所有业务分布都满足某个概率。 先确定输入字节 Hashing 不直接理解“商品身份”。字符串必须选择编码,整数必须选择字节顺序...
Java常用类库-17-RateLimiter的许可债务与时间模型
每秒两个许可不等于最多两个请求 商品同步任务需要控制向下游发请求的速度。把 RateLimiter 配成每秒两个许可,再让每次请求申请一个许可,可以约束许可申请节奏,却不能据此证明任意一秒窗口最多出现两个请求,更不能证明同时执行的请求最多两个。空闲期间可积累许可,多许可请求还会把等待成本留给后续调用。 这一章使用 Guava 33.5.0-jre,分别检查公开 API 和固定版本 SmoothRateLimiter 的时间模型。Java 8 代码在 Zulu 8u472、Corretto 21.0.11 上运行。确定性模型使用可控时钟,真实时间另作观测;两类证据分开报告,模型中的精确小数不冒充操作系统调度精度。 第 16 篇区分结果完成与线程工作,本章把限流也拆成到达、许可申请、等待、执行、完成五个时刻。RateLimiter 只参与许可申请和必要等待,没有从任务完成处自动接收反馈。[固定版本 RateLimiter](https://github.com/google/guava/blob/8868c096cfdabbe38170b6e395369c315cfb72a1/gua...
Java常用类库-16-ListenableFuture的执行线程与取消边界
结果完成以后,线程还在做什么 商品查询完成后需要转换价格、记录结果,再通知调用方。把这些操作注册成 Future 回调,能够避免在查询入口直接阻塞等待,但并不能据此断定回调运行在另一个线程。使用 directExecutor 时,完成查询的线程可能继续执行全部回调;若回调等待慢速存储,查询线程就仍然被占用。 ListenableFuture 在普通 Future 上增加完成监听器,每个监听器关联一个 Executor。这个关联才决定执行策略。“异步结果”描述何时取得值,Executor 描述由谁执行动作,两者应分别建模。本章固定 Guava 33.5.0-jre,以 Java 8 代码在 Zulu 8u472 与 Corretto 21.0.11 运行。ListenableFuture 文档 系列基线中的商品身份仍用于示例,线程实验只传递不可变字符串,避免把结果对象可变性混入执行线程问题。完整测试使用 latch 控制进度,用真实线程名与 Future 状态判断因果,不靠任意 sleep 猜测先后。 三个时间点必须分开 查询计算完成、Future 进入终态、监听器执行完成,是三个...
Java常用类库15:加载、刷新与失效的缓存状态
商品详情缓存需要减少重复查询,同时给价格更新留下明确的生效边界。一次查询返回旧价格,可能因为条目仍在有效期,也可能因为刷新正在执行或已经失败。调用 invalidate 以后仍然出现一个先前启动的加载结果,也有不同于“删除没有成功”的原因。只记录缓存里有没有这个 key,无法解释这些行为。 Guava LoadingCache 把缺失加载、已有值刷新和条目维护安排在不同路径中。固定到 33.5.0-jre 后,可以用受控时钟和 Future 把状态分别停住,观察哪个调用等待、哪个调用返回旧值、哪些失败会传给调用方。商品缓存的陈旧窗口和更新协议需要依据这些可见行为设计。 商品详情允许多旧,先写成约束 设缓存保存 sku -> 商品详情快照,加载器查询外部详情服务。此次实验用字符串版本号代替详情对象,只观察缓存协议;联网与 JSON 解析留给相应章节。详情快照应当不可变,否则命中相同对象仍可能观察到未受缓存协议约束的字段修改。 假设条目写入十秒后允许刷新,三十秒后过期。十秒的含义是刷新资格,不能据此承诺“后台每十秒更新一次”。三十秒定义从成功写入开始计算的过期时限,也不能替代...
Java常用类库-14-Range区间边界与覆盖语义
价格等于门槛时归入哪一档 商品价格以整数分保存,活动档位定义为“满 100 分但不足 200 分”。这不是两个孤立数字,而是包含下端点、排除上端点的区间 [100,200)。如果配置接口只保存 lower=100、upper=200,而把端点规则留给各处 if 判断,那么导入校验、页面展示和价格查询可能采用不同规则。 Range 将端点和值域比较规则组合为一个对象;RangeSet 表示若干区间的并集;RangeMap 把互不重叠的区间关联到值。三种结构处理不同的问题:属于哪个范围、是否属于任意合法范围、所在范围采用哪条规则。活动价格场景还必须确定重叠配置是拒绝、覆盖还是叠加,容器的默认行为不能替代业务决策。 本篇固定 Guava 33.5.0-jre,Java 8 API,JDK 8 与 JDK 21 分别验证。完整测试源码含五组实验,RUN.md给出复跑命令。价格使用整数分是本实验的业务约束,不讨论浮点舍入。金额进入范围判断前需要完成统一单位和范围校验。 开闭端点不只影响打印形式 Range 契约基于 Comparable 排序定义边界。closedOpen(100,200)...
Java常用类库-13-惰性集合视图与Ordering求值边界
一次转换与一份转换规则 商品目录保存整数价格,导出前需要换算成另一单位。若将源 Map 交给 Maps.transformValues,得到的 Map 看起来已经保存换算后的价格;但转换函数没有在构造时执行。返回对象保存的是源 Map 与转换规则,读取某个值时才进行计算。 这种设计可以让结果随目录修改持续变化,也使同一次导出中的重复查询可能重复计算。若转换函数执行远程查询、记录日志或读取当前汇率,重复读取不只增加工作量,还可能得到不同结果。选择集合工具之前,必须确定需要“实时读取源状态”的视图,还是“在某个时间点完成计算”的结果集合。 实验固定 Guava 33.5.0-jre,源码 SHA 为 8868c096cfdabbe38170b6e395369c315cfb72a1,使用 Java 8 语法。完整测试源码包含 Map 转换、Iterable 转换、分区和集合运算、排序四组场景,RUN.md给出 JDK 8 与 JDK 21 的复跑命令。容器与元素的共享边界见 第 02 篇。 用计数器辨认真正的计算时机 源 Map 按顺序保存 sku-a=10、sku-b=20。转换函数...
Java常用类库-12-BiMap与Table的索引约束
反向查询与二维查询是两种需求 商品导入需要维护内部 SKU 与外部商品编号的对应关系。如果每个外部编号只能归属一个内部 SKU,查询方向既包括“内部编号找外部编号”,也包括“外部编号找内部编号”,则数据本身是一对一关系。另一个需求是按商家与 SKU 查询价格,同一 SKU 可以在不同商家下有不同价格;这里的标识是两个坐标构成的组合。 第一种关系适合讨论 BiMap,第二种适合讨论 Table。前者约束值唯一,并提供可反向访问的同一份映射;后者把一对键作为单元格位置,值可以重复。若只因为都需要“多一种查询方式”就把两者混为额外索引,会误解重复约束,也会错误估计列查询代价。 实验使用 Guava 33.5.0-jre,源码固定到 8868c096cfdabbe38170b6e395369c315cfb72a1,基础代码保持 Java 8。完整可编译的 Chapter12Test.java 与 复跑说明覆盖五组场景;JDK 8、JDK 21 分别运行。容器别名的前置内容见 第 02 篇。 BiMap 把值唯一变成写入约束 普通 Map 允许两个不同键映射到同一个值。若另建一个反向 Ma...
Java常用类库-11-Multiset频次与集合大小的区别
商品标签统计需要保留什么 一次商品导入包含三个标签出现记录:sale、sale、new。统计接口可能同时需要回答三个问题:收到多少条标签记录,出现过多少种标签,每种标签各出现多少次。答案分别为 3、2,以及 sale=2、new=1。用 Set 保存会丢失重复次数;用 List 保存可以保留记录,但每次查询频次都要重新统计。Multiset 将元素和出现次数作为一个整体管理,适合这里的频次模型。 频次是否具有业务意义,要先于容器选择。若同一商品重复填两次 sale 应当被视为输入错误,直接把全部标签装进 Multiset 会把错误输入变成合法频次。若要统计“带有 sale 的商品数量”,则应先对单件商品内部的标签去重,再汇总;若要统计“收到多少次 sale 事件”,则不能提前去重。容器能够保持计数,不能决定一条记录应当算几次。 本篇使用 com.google.guava:guava:33.5.0-jre,示例采用 Java 8 API,在 JDK 8 与 JDK 21 分别运行。完整实验为 Chapter11Test.java,复跑说明见 RUN.md。前置的容器共享问题见 第 ...
Java常用类库-10-Multimap的多值语义与写入视图
一家商户可以对应多个商品 目录导入把商户编号映射到商品编号,同一商户输入 B、A、B。若记录代表三条导入事件,两个 B 都应保留;若记录代表商户当前拥有的商品集合,重复 B 可以合并。Map<String, String> 无法同时保留多个值,Map<String, List> 能表达第一种需求,但空分组、计数与删除最后一个值需要额外约定。 Guava 的 Multimap 将一个键对应多个值作为公开集合契约。它并没有替所有实现选择相同的重复规则:ArrayListMultimap 的每键值集合是 List,HashMultimap 的每键值集合是 Set。选型首先由业务是否允许重复决定,其次才是 API 调用是否简短。 本篇使用 Guava 33.5.0-jre、Java 8 API,在 Zulu 8.0.472 和 Corretto 21.0.11 上验证。完整测试与 运行说明可复跑各返回视图的修改。前篇 Immutable 集合讨论冻结结果,这里检查可变多值结构如何维持各入口的一致性。 size 计数的是键值对 向 ArrayListMultimap...

