开始前,会这些就够了
- 会读 Java 字符串字面量;知道源码里的双反斜线表示运行时一个反斜线。
- 理解方法参数与返回值;了解正则分组的基本用途。
- 先读 把模糊需求写成可测试的提示:已有单遍模板实验说明替换值按字面写入;接着对比用户搜索词与替换词各自的解释规则。
后台有一个“批量替换商品说明”的工具,用户输入搜索词 a.b,希望只改这三个字符。随后又输入替换内容 $1\archive,希望把它原样保存。两个输入都是普通文本,但只给搜索词转义,仍不能保证替换内容按原文写回。
本实验将同一段文本分别送进搜索规则和替换规则。你会看到点号使搜索范围扩大,而 $1 在替换位置会引用已经匹配到的分组。它们发生在不同阶段,所以需要分别确定输入的含义。
把例子带到自己的电脑上
下载完整练习包
需要 JDK 17 或更新版本,使用 Java 标准库;制作时实测 Windows / JDK 17.0.20.1 / 21.0.12。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- RegexLiteralDemo.java:完整可运行示例
- README.md:运行说明与独立练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和产物验证记录
- SHA256SUMS.txt:内容摘要
在这个文件夹打开 PowerShell,逐行运行
java -version
javac -version
javac -encoding UTF-8 --release 17 RegexLiteralDemo.java
java -cp . RegexLiteralDemo解压到独立练习文件夹,在含 RegexLiteralDemo.java 的目录打开终端运行。Windows 内存固定字符串;搜索/替换分别对照、特殊字符、未命中及空搜索规则;JDK17/21各9项断言。未测试文件写回、任意正则性能或其他语境转义。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
第一步:先只改变搜索位置
在“把模糊需求写成可测试的提示”的单遍模板实验里,重点是让替换参数按原文填入。这里还允许用户填写搜索词,因此把搜索规则也纳入同一组对照。
输入固定为 a.b axb a.b。直接编译 a.b 后,三个片段都被替换成 X;这说明搜索规则里的点号没有被当成普通句点。
把搜索词交给 Pattern.quote 再编译,输出变成 X axb X。此时我们没有修改替换文字,唯一变化是搜索词的解释方式。因此可以把两种输出的差异归到搜索阶段。
这里讨论的是把完整用户输入作为一个字面量片段嵌入规则;若产品本来允许用户编写正则,就应把它明确设计为另一个模式,不能先转义再声称支持正则语法。
第二步:搜索正确,也要检查替换位置
代码用一个明确的捕获组匹配 a.b,再分别传入原始 $1 和经过 Matcher.quoteReplacement 处理的 $1。前者输出 a.b,后者输出 $1。这组对照把搜索固定住,只改变替换解释。
捕获组可以理解为匹配时暂存的一段文字;替换规则里的 $1 表示取第一段。因此原始 $1 并不是随意丢失,而是被执行成了替换指令。
Pattern.quote 处理要交给 Pattern 的文本;Matcher.quoteReplacement 处理要交给替换步骤的文本。两者不能因为名字都有 quote 就互换,也不能将同一份已处理字符串不断重复处理。
第三步:把两个输入接回同一个方法
literalReplace 先拒绝空搜索词,再分别处理 needle 和 replacement。完整输出保留了替换文本中的美元符号和反斜线,同时没有改动中间的 axb。
源码用 String.replace(CharSequence, CharSequence) 做一项独立对照:如果需求只是原样查找、原样替换,可以直接使用这个更短的入口。它的名字也叫 replace,但不能与接收正则的 replaceAll 混为一谈。
另一项输入故意带有反斜线加 E。它检查使用 API 处理复杂文本仍能找到原样片段;不要自己在任意输入两端拼接转义标记,以为中间永远不会包含结束标记。
第四步:把产品规则留在 API 之外
本例拒绝空搜索词,是工具自己的规则。空内容究竟表示“没有填写”还是“每个位置都匹配”,应由产品约定,不能交给某个库默认行为替你决定。
实跑只覆盖内存中的固定字符串、字面量搜索与替换、一个捕获组和未命中场景。没有读取用户文件、执行批量保存或测试正则性能,也不提供 SQL、HTML 等其他语境的转义。
带回项目时,分别记录输入来源、允许的搜索模式、替换是否支持分组、空输入规则,再写回归样本。这样排错时能知道问题发生在查找、替换还是写回。
运行单文件,按实际输出逐项核对
保存为 RegexLiteralDemo.java,在源码目录执行下方命令;需要完整 JDK 17 或更新版本。无需 Maven、网络或第三方依赖。
2026 年 10 月 18 日在 Windows 上分别以 Microsoft OpenJDK 17.0.20.1 与 21.0.12、--release 17 编译运行,输出一致。每项断言失败都会终止程序;下方是本次真实输出。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.regex.*;
public class RegexLiteralDemo {
static int checks;
static void check(boolean ok) { if (!ok) throw new AssertionError(); checks++; }
static String literalReplace(String input, String needle, String replacement) {
if (needle.isEmpty()) throw new IllegalArgumentException("empty search is undefined here");
return Pattern.compile(Pattern.quote(needle)).matcher(input)
.replaceAll(Matcher.quoteReplacement(replacement));
}
public static void main(String[] args) {
String input = "a.b axb a.b";
String unquoted = Pattern.compile("a.b").matcher(input).replaceAll("X");
String searchOnly = Pattern.compile(Pattern.quote("a.b")).matcher(input).replaceAll("X");
check(unquoted.equals("X X X")); check(searchOnly.equals("X axb X"));
System.out.println("unquoted_search=" + unquoted);
System.out.println("quoted_search=" + searchOnly);
Pattern grouped = Pattern.compile("(a\\.b)");
String interpreted = grouped.matcher("a.b").replaceAll("$1");
String literal = grouped.matcher("a.b").replaceAll(Matcher.quoteReplacement("$1"));
check(interpreted.equals("a.b")); check(literal.equals("$1"));
System.out.println("replacement_$1: interpreted=" + interpreted + "; literal=" + literal);
String replacement = "$1\\archive";
String result = literalReplace(input, "a.b", replacement);
check(result.equals("$1\\archive axb $1\\archive"));
check(result.equals(input.replace("a.b", replacement)));
System.out.println("both_literal=" + result);
String odd = "left\\E.right";
check(literalReplace("<" + odd + ">", odd, "$0").equals("<$0>"));
check(literalReplace("no match", "a.b", "$1").equals("no match"));
boolean emptyRejected=false;
try { literalReplace("abc", "", "X"); } catch (IllegalArgumentException e) { emptyRejected=true; }
check(emptyRejected);
System.out.println("embedded_quote_end=preserved; absent_match=unchanged; empty_search=rejected");
System.out.println("PASS checks=" + checks);
}
}
怎样运行:在源码所在目录,先单独执行 javac -encoding UTF-8 --release 17 RegexLiteralDemo.java;编译成功后,再执行 java -cp . RegexLiteralDemo。两条命令分别回车。2026-10-18 已在 Windows、JDK 17.0.20.1 / 21.0.12 编译运行此份源码并通过检查。Windows 内存固定字符串;搜索/替换分别对照、特殊字符、未命中及空搜索规则;JDK17/21各9项断言。未测试文件写回、任意正则性能或其他语境转义。
运行后对照结果
unquoted_search=X X X
quoted_search=X axb X
replacement_$1: interpreted=a.b; literal=$1
both_literal=$1\archive axb $1\archive
embedded_quote_end=preserved; absent_match=unchanged; empty_search=rejected
PASS checks=9记住这三点
- 搜索规则与替换规则是两个入口,各自确定是否按原文处理。
- 原样查找使用
Pattern.quote;原样替换使用Matcher.quoteReplacement。 - 只做字面量替换时,可直接评估
String.replace。
别把例子用过头
- 只转义搜索词,却把用户替换内容直接交给
replaceAll。 - 把源码转义、正则转义和替换转义当成同一件事。
- 没有定义空搜索词的行为。
自己试一小步
加入“只替换第一个命中”和“有意支持 $1”的两种模式,怎样避免含义混淆?
- 先列出每种模式对搜索和替换的承诺。
- 选择至少六组包含美元符号、反斜线、括号、空输入及未命中的数据,先预测结果。
- 保留普通文本模式,再新增明确命名的方法,不修改用户输入。
- 提交模式表、断言、完整输出与 Java 版本。
做到这些,就算完成
- 字面量模式中的特殊字符保持原样。
- 启用分组的模式说明缺失分组如何处理。
- 没有把本例能力宣称为其他文本语境的转义工具。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Pattern.quote:字面量搜索片段 (新标签页)↗
支持搜索参数的处理位置。
- Matcher.quoteReplacement:字面量替换内容 (新标签页)↗
支持美元符号和反斜线在替换中的不同含义。
- String.replace 与 replaceAll (新标签页)↗
支持纯字面量入口的选择。