Java常用类库-09-Immutable集合的构建与共享边界
商品目录的冻结范围 目录导入程序把商品列表交给查询线程,查询接口承诺结果不能增删。如果返回 Collections.unmodifiableList,导入程序继续向原列表追加商品,查询结果也会变。如果改为 ImmutableList.copyOf,追加不再传播;但商品对象内部的名称若仍可修改,查询结果中的名称仍会变。 这里有两个不同的状态集合:容器保存的元素引用序列,以及这些引用指向的对象状态。不可变集合固定前者。要让一次目录查询代表某个时刻的完整业务状态,还需要约束商品对象及其可达对象。只检查 add 是否抛异常,尚未覆盖目录的版本含义。 第 02 篇建立了视图、浅复制与不可变的区别。本篇以 Guava 33.5.0-jre 继续追踪 ImmutableList、ImmutableSet、ImmutableMap 的构建、访问和 builder 复用路径。实验遵循 Java 8 API,附 完整测试、运行说明;JDK 现代工厂方法作为明确标注版本的替代方案讨论。 构建边界也是校验边界 同一批商品标签 B、A、B 进入不同集合,会得到不同结构。ImmutableList 保留三个...
Java常用类库-08-CharMatcher与Strings的字符单位
商品名称的长度按什么计算 商品名称含一个表情符号,界面显示为 A😀B。Java 的 length 返回 4,按 Unicode 码点计数得到 3。若要求“将表情替换为一个问号”,对 CharMatcher 使用表情的字符串形式,可能得到两个问号;若只删除其中一个代理项,还会留下不成对的 UTF-16 代码单元。 问题来自字符单位没有对齐。Java 的 char 是 16 位 UTF-16 代码单元;补充平面的一个码点由高、低两个代理项组成。一个用户感知的字形还可能由多个码点组合,因此 code point 也不能直接等同于界面中的一个字符。商品编码只允许 ASCII 时,这些区别可以由输入校验排除;商品名称允许多语言和表情时,就必须直接面对它们。 本篇使用 Guava 33.5.0-jre、Java 8 API,双 JDK 为 Zulu 8.0.472 与 Corretto 21.0.11。完整测试源和 运行说明覆盖 UTF-16、组合字符、前后缀及空白策略。前篇 分隔契约关注边界是否丢失,这里继续追踪一次字符操作究竟修改了哪些单位。 CharMatcher 的输入域是 cha...
Java常用类库-07-Joiner与Splitter的分隔契约
商品标签中的空项是什么 商品导入记录用逗号连接标签,输入为 ,新品,,促销,。预期结果可能是五个位置固定的字段,也可能是两个有效标签。这两种需求使用相同字符串,却不能使用相同的丢弃规则。空项一旦被删除,原来位于哪个位置也随之丢失;下游无法根据剩余值判断是缺字段、空字段还是多余分隔符。 Guava 33.5.0-jre 的 Splitter 默认保留空项,Joiner 默认拒绝 null。它们没有自动推断业务格式的职责。标签导入若决定忽略空项,可以明确配置;价格、数量等按列定位的记录若套用这项配置,则可能把下一列的值读到前一列。类库调用之前应先决定空项是否携带位置意义。 这里的验证使用 Java 8 API、Guava 33.5.0-jre,分别运行在 Zulu 8.0.472 和 Corretto 21.0.11。完整测试是 Chapter07Test.java,运行方式见 RUN.md。基础工程下载入口位于 第 00 篇。 保留规则决定输出形状 将 ,A,, 按逗号切分,手算共有三个分隔符、四个字段。默认 Splitter 的结果为 ["", "A...
Java常用类库-06-Optional迁移的值语义与类型边界
查询不到商品,还是转换程序出错 商品查询返回 Guava Optional<Product>,空结果表示目录没有这个商品。迁移到 JDK Optional<Product> 时,常见改动是替换 import,再把 transform 改成 map。若转换函数不小心返回 null,旧实现抛异常,新实现却得到 empty;调用者随后返回“商品不存在”,原本的转换错误被折叠进正常缺失。 两种 Optional 都可以表达一个非 null 值或缺失,但相似的数据模型并不意味着所有操作相同。默认值何时计算、空返回值如何处理、异常类别、公开方法的返回类型以及 Java 序列化都可能改变。迁移需要逐项固定业务约束,而不是把编译通过当作全部验收。 本章固定 Guava 33.5.0-jre,使用 Java 8 API,在 Zulu 8u472、Corretto 21.0.11 上运行。第 05 篇区分检查与消息求值,这里沿着相同的参数求值规则检查 Optional 默认值。 容器不存 null,容器引用仍可能是 null Guava Optional.fromNullab...
Java常用类库-05-Preconditions与Verify的失败契约
一次导入的三种失败 商品数量为负数,目录已经关闭,以及导入结束后两个内部索引数量不一致,都可以让程序停止,但它们暴露的问题不同。负数量来自调用参数;目录关闭说明当前对象状态不允许执行操作;索引不一致则违反程序建立并依赖的内部假设。如果统一抛出一个含糊的 RuntimeException,调用者很难判断该修正请求、调整调用时机,还是报告实现缺陷。 Guava 33.5.0-jre 的 Preconditions 和 Verify 给这些检查提供了短写法。checkArgument、checkState、checkNotNull 分别产生不同的异常,Verify 使用 VerifyException 表达假设失败。它们只在实际调用处检查表达式,不会自动证明系统其他位置已经满足不变量。Preconditions、Verify 第 04 篇把库调用放回业务契约中比较;本章继续使用商品导入,检查异常类型、消息文本与消息构造时机。实验的 Java 8 基线在 Zulu 8u472、Corretto 21.0.11 上分别运行,未使用耗时数字推导性能优势。 检查位置决定错误归属 一个入口方法若...
Java常用类库-04-规范化策略与依赖成本
同一个商户编号为什么得到不同结果 导入文件中的商户编号需要去除两端空白,再统一为大写。String.trim()、Commons Lang 的 StringUtils.strip()、Guava 的 CharMatcher.whitespace().trimFrom() 都能把普通的 " acme " 变成 "acme"。只用这一条输入验收,三个实现看起来可以自由替换。 把两端字符换成不换行空格 U+00A0,结果就不同了:Guava 删除它,另外两个实现保留它。若规范化结果作为目录索引的商户键,替换工具方法可能合并原来分开的键,也可能让同一来源在不同服务里查不到。依赖选型首先要确定这次转换允许丢掉哪些信息,再比较代码、包体和维护成本。 本文固定 Guava 33.5.0-jre、Commons Lang 3.20.0、Java 8 API,运行时对照 Zulu 8u472 与 Corretto 21.0.11。第 01 篇已区分空值与空白,第 03 篇明确身份与排序关系;这里把这些差异放进一个具体的依赖决策。 先写出规范化契约 示例的最小...
Java常用类库-03-相等哈希与排序的契约
商品去重使用哪个身份 商户 A 和商户 B 都提交了 SKU-1。若商品身份是商户编号与商品编号的组合,两条记录必须保留;若用只比较 SKU 的 TreeSet 收集,第二次插入却会返回 false。排序正常执行、集合也没有抛异常,但业务数据已经少了一条。 相等、哈希与排序分别定义不同的关系。equals 决定对象之间的相等关系,hashCode 必须与这个关系相容;比较器定义顺序,同时也给有序集合提供“比较结果为零”的等价关系。工具方法可以组合字段,却不能替业务决定哪些字段属于身份。第 02 篇讨论容器和元素的可变边界,这里继续检查元素作为键以后还允许哪些变化。 实验使用 Java 8 API,在 Zulu 8u472 和 Corretto 21.0.11 上运行。Guava 固定为 33.5.0-jre,但这一章的基础反例只依赖 JDK;先明确标准集合的约束,后续引入 Multimap、BiMap 或 builder 才有判断依据。 哈希值不能代替相等关系 Object 的契约要求相等对象具有相同哈希值,并不要求不同对象的哈希值不同。哈希冲突是集合实现必须处理的情况,因此不能...
Java常用类库-02-视图浅复制与不可变的边界
返回的列表,谁还能改 商品目录查询返回 List<Item>,调用方只允许读取。把内部列表套进 Collections.unmodifiableList 后,调用方执行 add 会抛异常,但后台导入程序仍能通过原列表追加商品,调用方下次读取也会看到新增项。这个接口提供的是随源变化的只读视图;若需求是“保留查询当时的商品集合”,这层包装没有完成任务。 判断保护范围,需要沿着三种引用分别检查:列表变量指向哪个容器,容器保存哪些元素引用,元素内部还指向哪些可变状态。列表不可写只回答其中一部分问题。第 01 篇讨论输入中缺失值的含义;这里把同一份商品数据送入不同容器,观察哪些变化仍能传播。 构造方式 源列表增删替换是否传播 返回列表能否增删替换 元素是否共享 Collections.unmodifiableList(source) 是 否 是 new ArrayList<>(source) 否 是 是 ImmutableList.copyOf(source) 否 否 是 表中“传播”均指构造完成后的操作;构造期间源集合不被其他线程修改。...
Java常用类库 01:null、empty与blank的语义边界
商品编号里的空白会改变什么 商品目录接收了一个编号 BOOK-01,随后又收到 BOOK-01 和末尾带不换行空格的 BOOK-01\u00A0。三份输入应当指向同一件商品,还是后两份必须报错?这个决定先于 trim、isBlank 和 Strings.isNullOrEmpty 的选择。若先统一清洗再讨论身份,原始输入之间的差别已经丢失。 本篇约定商品编号只允许 1~32 个 ASCII 大写字母、数字或连字符,首字符必须是字母或数字;任何不合规输入直接拒绝,合法输入保持原值。商品名称可以另有规则,例如保留内部空格。这样的区分避免把“字符串工具接受什么”误当成“商品目录允许什么”。 第00篇建立了实验环境。本篇沿用 Guava `33.5.0-jre`、Commons Lang `3.20.0` 和 JUnit Jupiter `5.13.4`,主示例使用 Java 8 语法,另在 JDK 21 对照 `String.isBlank()`。完整参数表、输出和业务断言都在 第 00 篇工程附件内的 `src/test/java/blog/libraries/Chapter01T...
Java常用类库00:从业务契约建立可复现基线
商品导入程序需要按商户分组,保留输入顺序,并拒绝同一商户下重复的商品编号。实现可以使用普通的 Map<String, List<Product>>,也可以引入 Guava 的多值集合。两个程序即使打印出相同的分组结果,也未必可以替换:返回集合是否允许修改、源集合修改是否传播、null 怎样处理,都属于调用方能观察到的行为。 类库减少代码后,这些责任仍然存在。本系列从一个很小的商品目录开始,把每次引入依赖的理由写进业务约束和实验。Guava、Apache Commons 是主要研究对象;缓存、JSON、日志和 HTTP 等类库在相应需求出现后加入。第一篇交付能运行的基线,后续章节在这份工程里累计反例和比较实现。 商品目录先确定哪些行为 假设输入是已经解析好的商品对象,每个对象包含商户编号、商品编号和价格。此次只研究内存中的校验与分组,文件解析、数据库、网络请求尚未加入。这样的边界使失败原因容易定位:重复编号的异常来自分组规则,价格精度错误来自金额构造,不会混入连接超时或 CSV 方言问题。 业务身份由商户编号和商品编号共同决定。shop-a/p-1 与 s...

