← 返回 Java 后端知识路线
阶段 01Java 基础

String、正则、日期、金额与 ArrayList:把常用 API 串成工具

用一份文本账单报告把字符串处理、正则校验、日期时间、金额和动态列表连成可复用的最小工具。

第 08 / 35 篇
String正则日期BigDecimalArrayList

先看这一课值不值得学

学完后,你手里多了哪些代码积木

把常用 API 组合成可复用的文本和数据处理工具。

本课正式新增

语法 / API / 命令你必须会到什么程度
String / StringBuilder不可变字符串与可变拼接器
matches / Pattern / Matcher校验或查找符合规则的文本
LocalDate / DateTimeFormatter表达日期并按格式解析展示
BigDecimal用十进制语义计算金额
ArrayList<E>保存数量可变的同类型元素

本课只借用,先别硬背

  • Stream 留到第 12 课,本课先用普通循环

学完必须能独立写

  • 解析一份文本账单并输出报告
  • 正确处理金额、日期、正则和动态列表
本课目录
  1. 1. 现实问题:常用工具混在一起就会制造隐性错误
  2. 2. 最小可运行示例:明确每一步的类型
  3. 3. 调用链与对象变化
  4. 4. 为什么这样设计
  5. 5. 项目落点:把清洗和展示分开
  6. 6. 易错排查
  7. 7. 一页复习

1. 现实问题:常用工具混在一起就会制造隐性错误

后端经常要处理标题空格、手机号格式、发布时间、金额和一批标签。每件事单独看都简单,但把 String、正则、日期、金额和集合混写时,容易出现字符串不可变却以为已修改、正则过度匹配、日期时区丢失、金额四舍五入不一致,以及把固定数组硬塞进可变列表的问题。

下面做一个文本报告:清洗标题,找出两位小数金额,解析 ISO 日期,去重标签,再输出报告。它不是完整业务库,却包含了工具类选择时应该问的边界。

2. 最小可运行示例:明确每一步的类型

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class TextReportDemo {
    public static void main(String[] args) {
        String rawTitle = "  Java  后端  入门  ";
        String title = rawTitle.trim().replaceAll("\\s+", " ");
        Matcher moneyMatcher = Pattern.compile("\\b\\d+(?:\\.\\d{1,2})?\\b").matcher("总价 19.90,数量 2");
        BigDecimal amount = null;
        if (moneyMatcher.find()) amount = new BigDecimal(moneyMatcher.group());

        LocalDate date = LocalDate.parse("2026-08-03");
        Set<String> tags = new LinkedHashSet<>(new ArrayList<>(java.util.List.of("Java", "后端", "Java")));

        StringBuilder report = new StringBuilder()
                .append(title).append(" | ").append(date).append(" | ")
                .append(amount).append(" | ").append(tags);
        System.out.println(report);
    }
}

示例中的 String 方法返回新字符串,StringBuilder 原地追加;正则分组返回匹配到的文本,仍然需要交给 BigDecimal 解析;LocalDate 没有时区,适合仅表示日期的业务值;LinkedHashSet 去重但保留插入顺序。工具类不是越多越好,关键是让类型携带正确语义。

3. 调用链与对象变化

trimreplaceAll 每次创建新的字符串结果,rawTitle 不会改变;Pattern 把正则编译成匹配器所需的结构,Matcher.find 移动内部游标并把捕获结果暴露给 groupBigDecimal 从匹配文本创建十进制对象,后续计算应继续使用返回的新对象。

LocalDate.parse 根据 ISO-8601 文本创建日期值,若输入包含时间或时区,就应换成 InstantOffsetDateTimeZonedDateTimeArrayList 保存可变顺序数据,LinkedHashSet 创建另一种集合对象;最后 StringBuilder.append 修改自身,toString 再生成报告字符串。

4. 为什么这样设计

字符串是不可变对象,这让它可以安全共享、作为 Map key 并缓存哈希值;大量拼接时使用 StringBuilder 减少中间对象。正则适合结构简单、边界明确的格式,不适合承担完整的业务解析器;复杂表达式要写测试,说明允许和拒绝的样例。

日期时间要先问业务表示的是“日期”“本地时间”还是“绝对时刻”。金额要先确定 scale 和 rounding mode,展示格式则放在边界层。ArrayList 提供按索引访问和尾部追加,Set 提供唯一性,Map 提供键到值的查找;数据结构选择应该来自访问方式,而不是来自“大家都用它”。

5. 项目落点:把清洗和展示分开

文章入库前可以把标题清洗放在输入边界,标签规范化放在领域服务,日期转换在 DTO 映射层完成,数据库存储使用明确的类型。不要在 SQL 拼接里临时用正则修数据,也不要让模板层决定金额的舍入。对批量标签,先定义大小上限和是否大小写不敏感,再决定 Set 实现。

练习:为一篇文章实现 normalizeTags(List<String>),要求去空、去重、保留第一次出现的顺序,并在 20 个标签后拒绝。再写 parsePublishedAt(String),分别测试只有日期、带偏移时间、非法日期和空字符串。

6. 易错排查

  • replace 后原字符串没变:String 不可变,接住返回值;大量拼接才用 StringBuilder。
  • 正则 \\d+ 把金额小数截断:根据协议写完整表达式,并测试边界,不要先 parse 再猜格式。
  • LocalDateTime 被当成全球时间:它没有时区,跨服务传输时优先使用带 offset 的值。
  • new BigDecimal(0.1):从浮点近似值开始,结果已经不精确;使用字符串或 valueOf
  • 需要保持顺序却使用 HashSet:改成 LinkedHashSet,并把顺序行为写进测试。

7. 一页复习

String 负责不可变文本,StringBuilder 负责连续构造;正则负责有限模式,不能代替业务解析;java.time 先选语义再选类型;BigDecimal 负责可控金额;ArrayList、Set、Map 按访问需求选择。每个 API 都要追问“输入类型、返回对象、是否修改原对象、失败如何表现”。

把工具的输入输出写成样例比背 API 更可靠:" Java 后端 " 变成 "Java 后端""19.9" 变成 scale 为 2 的 BigDecimal 19.90"2026-08-03" 变成 LocalDate;标签 Java,java, 后端 变成按第一次出现顺序的两个值。每个转换都应说明空输入、非法格式和超长文本的结果。

正则适合验证局部结构,不适合从一串自由文本中猜出完整订单。手机号、slug 和金额可以先限制字符长度,再用 Pattern 匹配,最后交给专门类型解析。日期如果从浏览器带来 offset,就先保存原始 OffsetDateTime,不要为了显示方便直接截成 LocalDate 丢掉时刻。金额的 scale、舍入和展示 Locale 也应作为方法参数或配置写清。

在项目中可以把 TextNormalizer 放在输入层,把 MoneyPublishedAt 等值对象放在 domain,把 DateTimeFormatter 和 NumberFormat 放在 presentation。ArrayList 只承载清洗后的顺序数据,Set 负责去重,报告 StringBuilder 只在输出边界使用。这样工具方法不会被模板、SQL 和数据库格式互相污染。

错误排查按转换阶段进行:标题仍有多余空格时查 trim/replace 的返回值;正则漏掉 19.999 时查小数位边界而非强行舍入;日期跨时区错一天时查输入是否带 offset;标签顺序变化时查 Set 实现;金额尾数异常时查构造器和 rounding。练习是为 20 条真实样本建立输入—输出—拒绝原因表,再把表转成 JUnit 参数化测试。

String 工具的对象状态也值得观察:原始标题引用不变,清洗后得到新 String;Pattern 可以预编译并复用,Matcher 每次绑定一个输入;BigDecimal 的 multiply/setScale 都返回新对象;ArrayList 先可变收集,最后 copy 成不可变结果。若把这些对象混在一个静态工具里,调用方很难知道哪一步会共享状态。

项目中常用 API 的文件落点应围绕语义而非类名:SlugNormalizer 只负责标题到 slug,MoneyParser 只负责文本到金额,PublishedAtParser 只负责时间协议,TagNormalizer 只负责集合规则。展示格式化放在 Web 层,数据库格式化放在 persistence,避免一个 Utils 类成为所有层的垃圾桶。

排错还要关注性能和安全:高频正则没有预编译会增加 CPU,允许用户控制复杂正则可能造成回溯攻击,StringBuilder 未限制输入会扩大内存,标签未限制数量会放大 SQL 和页面。练习是为正常、空、超长、非法和恶意样本建立边界测试,并把拒绝原因输出为稳定错误码。

工具链最终应输出可持久化值和展示值两份结果:数据库只保存标准金额、日期和标签,页面再按 Locale 格式化。把这条边界写成测试,能避免用户切换语言或时区时污染原始数据。

验证清单:准备同一组标题、标签、金额和时间样本,在解析器入口记录文本,在领域层记录规范化对象,在展示层记录最终字符串;三处结果应分别承担不同责任。用 19.919.999、带 +08:00 的时间、重复标签和超长标签测试,确认拒绝原因不会被格式化器改写。复盘时逐一回答 String、BigDecimal、LocalDate/OffsetDateTime、ArrayList 哪些调用返回新对象,哪些操作可能共享集合;如果看不出差异,就给方法命名加上 parsenormalizeformat 这样的边界词。

一个课题专属练习是实现文章摘要管线:先截断标题并保留原文,按输入顺序清洗标签,解析发布时间,最后生成展示 DTO。要求数据库值和页面值都能从日志中独立重建,且任何一步失败都带字段名和稳定错误码。这样可以验证正则、日期、金额和集合并不是孤立 API,而是同一条输入到输出的后端边界链。

进阶附录:Pattern 复用与格式化

高频正则应复用静态 Pattern,避免每次调用都重新编译;但正则含用户输入时要注意复杂度和拒绝服务风险。金额展示可使用 NumberFormat,日期展示可使用带明确 Locale 的 DateTimeFormatter。格式化只改变输出,不应改变持久化值。

本课按「Java 21 常用 API、正则、java.time 与集合入门」的学习范围组织,正文与示例均为本站原创整理。