开始前,会这些就够了
- 知道字符串下标从零开始,区间可包含起点而不包含终点
- 认识正则中的 ^、
$,能读 Java 方法链 - 先读 搜索词已经转义,为什么替换成
$1仍然变了?:先分清规则与替换文本的解释,再检查Matcher的区域和状态。
输入是 XabcY,程序只想检查中间的 abc。给 Matcher 设置 region(1,4) 后,^abc$ 居然匹配成功;可要求前面有 X、后面有 Y 的规则却失败了。原字符串没变,为什么两种直觉同时落空?
region 限制匹配区域,另有两个开关分别决定区域边缘能否充当锚点、前后视能否看见外面。本页保持字符和规则很小,让每次变化都能与实际输出对照。
把例子带到自己的电脑上
下载完整练习包
需要 完整 JDK 17 或更新版本;实测 Windows / JDK 17.0.20.1 / 21.0.12。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- Main.java:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
java -version
javac -version
javac -encoding UTF-8 --release 17 Main.java
java -cp . Main解压后,在含 Main.java 的目录打开终端。命令分别回车执行。真实Matcher,固定ASCII字符串;每JDK39检查,四种边界组合、三类匹配方法、reset/find(start)、空区域与非法区间;每版重复三次一致。未测任意正则、Unicode/多行、性能或并发。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
区域只圈住 abc,锚点和前后文分别由谁控制?
正在准备实验控件…
固定 Java 实跑结果回放:JDK 17 与 21 各运行三次,结果一致。网页只选择已有记录,不启动 JVM,也不接受任意输入执行。下方完整 Java 项目可下载复验。
先圈出 abc,但下标仍来自完整字符串
XabcY 的五个字符下标依次为 0、1、2、3、4。region(1,4) 指定 [1,4) 这段,也就是 abc;4 是不包含的终点,不是最后一个参与匹配的字符下标。
区域设置没有创建一份从零编号的新字符串。匹配到 abc 后,start() 为 1、end() 为 4,group() 是 abc。把这些位置用于原输入的 substring,才对应同一段内容。
默认整个输入都是区域。程序另外核对空区域 [2,2) 可以存在,而负起点、终点超过长度、起点晚于终点都应拒绝;空区域与非法区间不能混成一种情况。
锚点开关:区域边缘能不能充当开头和结尾
Matcher 默认启用 anchoring bounds。在本例不含换行的字符串中,^abc$ 可以把区域起点当作 ^、区域终点当作 $,所以即使外面还有 X 和 Y,matches() 仍为 true。
保持 region(1,4) 不变,只调用 useAnchoringBounds(false),同一规则变为 false。区域边缘不再替这两个锚点提供匹配位置,而 abc 又不在完整输入的两端。
这不等于禁用正则里的所有锚点。控制组把完整输入直接设为 abc,即使关闭区域锚点,^abc$ 仍能匹配。本文不含多行标志与行终止符,没有把这个小控制组扩成任意文本的锚点规则大全。
透明边界:前后视能不能看见圈外的字符
规则 (?<=X)abc(?=Y) 的意思是:匹配 abc,同时要求它前面紧邻 X、后面紧邻 Y。前后视检查上下文,不把这两个外围字符加入本次匹配内容。
默认区域边界不透明,因此这两个检查不能越过 region 去看到 X 和 Y,结果为 false。设置 useTransparentBounds(true) 后,它们能看到圈外上下文,结果为 true;匹配内容仍只有 abc。
透明不等于放宽可消耗的匹配范围。程序另用完整规则 XabcY 对同一个区域做 matches(),即使透明也为 false。四行矩阵独立切换锚点和透明性:本组锚点结果只跟锚点开关走,前后视结果只跟透明开关走。
matches、lookingAt 和 find,问的不是同一件事
把区域换成 abcYY,规则仍是 abc。matches() 要整个区域符合,所以得到 false;lookingAt() 只要求从区域开头匹配一段,所以得到 true;find() 在区域内寻找下一段匹配,也得到 true。
再把区域起点向左挪一格,让它从 x 开始,lookingAt() 失败,find() 仍能在后面找到 abc。这解释了为什么“找到了一个合法片段”不能直接当作“整个字段都符合格式”。
这几项对照每次使用独立 Matcher,避免上一次查找位置影响下一项。业务代码如果复用同一对象,还需要额外记录它目前的区域、查找位置和最近一次匹配是否成功。
reset 会恢复完整区域,find(start) 也会先做重置
程序在 12abc34def56 中先设 region(2,5),只找到 abc。调用 reset() 后,区域恢复 [0,12),最近一次匹配状态被清除;此时直接调用 group() 会抛 IllegalStateException。
两个边界开关却会保留:我们先设为 anchoring=false、transparent=true,reset 后仍是这组值。不能把“重置了”理解成所有设置都回到最初默认值。
接着重新圈出 abc,再调用 find(7),仍能找到原区域之外的 def。原因是这个带起点的重载先 reset,再从完整输入的下标 7 查找;它不是在原区域内部简单改一下游标。region(...) 本身也会清除旧匹配状态,然后设置新区间。
运行完整矩阵,核对 39 项具体条件
保存为 Main.java,先编译再运行。程序只使用 Java 标准库和固定内存字符串,不读取文件,也不执行外部传来的正则。失败检查直接抛 AssertionError,不需要额外加 -ea。
2026-10-23,在 Windows、Microsoft OpenJDK 17.0.20.1 与 21.0.12 分别使用 --release 17 编译;每版运行三次,六份 stdout 统一换行后完全相同,每次通过 39 项检查。
观察四行 bounds 矩阵,再看 reset 与 find(7) 两行的 region。输出分别记录匹配布尔值、范围和内容,不能只保留“没有抛异常”这一项结果。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Main {
private static int checks;
private static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
checks++;
}
private static void rejects(Class<? extends Throwable> expected, Runnable action) {
try {
action.run();
} catch (Throwable error) {
check(expected.isInstance(error), "unexpected exception: " + error);
return;
}
throw new AssertionError("expected " + expected.getSimpleName());
}
private static Matcher middle(String pattern, boolean anchoring, boolean transparent) {
return Pattern.compile(pattern).matcher("XabcY").region(1, 4)
.useAnchoringBounds(anchoring).useTransparentBounds(transparent);
}
public static void main(String[] args) {
Matcher defaults = Pattern.compile("^abc$").matcher("XabcY").region(1, 4);
check(defaults.hasAnchoringBounds(), "default anchoring");
check(!defaults.hasTransparentBounds(), "default opaque bounds");
check(defaults.matches(), "anchors at region edges");
check(defaults.start() == 1 && defaults.end() == 4 && defaults.group().equals("abc"),
"indices remain relative to the complete input");
System.out.println("default: input=XabcY region=[1,4) group=abc span=[1,4)");
for (boolean anchoring : new boolean[]{false, true}) {
for (boolean transparent : new boolean[]{false, true}) {
boolean anchors = middle("^abc$", anchoring, transparent).matches();
boolean context = middle("(?<=X)abc(?=Y)", anchoring, transparent).matches();
check(anchors == anchoring, "anchoring matrix");
check(context == transparent, "lookaround matrix");
check(middle("abc", anchoring, transparent).matches(), "plain region match");
check(!middle("XabcY", anchoring, transparent).matches(),
"transparent lookaround does not consume outside the region");
System.out.println("bounds anchoring=" + anchoring + " transparent=" + transparent
+ ": anchors=" + anchors + " lookaround=" + context);
}
}
check(Pattern.compile("^abc$").matcher("abc").useAnchoringBounds(false).matches(),
"real input edges still work without regional anchors");
Pattern literal = Pattern.compile("abc");
String larger = "xxabcYY";
boolean all = literal.matcher(larger).region(2, 7).matches();
boolean prefix = literal.matcher(larger).region(2, 7).lookingAt();
boolean found = literal.matcher(larger).region(2, 7).find();
check(!all && prefix && found, "whole region versus its prefix or a subsequence");
check(!literal.matcher(larger).region(1, 7).lookingAt(), "prefix starts at region start");
check(literal.matcher(larger).region(1, 7).find(), "find may scan within region");
System.out.println("region=abcYY: matches=" + all + " lookingAt=" + prefix + " find=" + found);
String text = "12abc34def56";
Matcher state = Pattern.compile("[a-z]+").matcher(text).region(2, 5)
.useAnchoringBounds(false).useTransparentBounds(true);
check(state.find() && state.group().equals("abc"), "first regional match");
state.reset();
check(state.regionStart() == 0 && state.regionEnd() == text.length(), "reset restores full region");
check(!state.hasAnchoringBounds() && state.hasTransparentBounds(), "reset keeps bound flags");
rejects(IllegalStateException.class, () -> state.group());
System.out.println("reset: region=[0,12) anchoring=false transparent=true group=undefined");
state.region(2, 5);
check(state.find() && state.group().equals("abc"), "narrow again");
check(state.find(7) && state.group().equals("def"), "find(start) resets before searching");
check(state.regionStart() == 0 && state.regionEnd() == 12
&& state.start() == 7 && state.end() == 10, "find(start) uses full-input indices");
System.out.println("find(7): region=[0,12) group=def span=[7,10)");
state.region(7, 10);
rejects(IllegalStateException.class, () -> state.start());
check(state.matches() && state.group().equals("def"), "region resets previous match state");
check(!state.hasAnchoringBounds() && state.hasTransparentBounds(), "region keeps bound flags");
Matcher empty = Pattern.compile("^$").matcher("abc").region(2, 2);
check(empty.matches() && empty.start() == 2 && empty.end() == 2, "empty region is legal");
rejects(IndexOutOfBoundsException.class, () -> Pattern.compile(".").matcher("abc").region(-1, 2));
rejects(IndexOutOfBoundsException.class, () -> Pattern.compile(".").matcher("abc").region(0, 4));
rejects(IndexOutOfBoundsException.class, () -> Pattern.compile(".").matcher("abc").region(2, 1));
rejects(IndexOutOfBoundsException.class, () -> state.find(13));
System.out.println("empty-region=matched; invalid-regions=3 rejected; invalid-find-start=1 rejected");
System.out.println("PASS " + checks + " checks; fixed ASCII input, no regex performance claim");
}
}
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-23 使用 完整 JDK 17 或更新版本;实测 Windows / JDK 17.0.20.1 / 21.0.12 复跑这份源码并核对完整输出。真实Matcher,固定ASCII字符串;每JDK39检查,四种边界组合、三类匹配方法、reset/find(start)、空区域与非法区间;每版重复三次一致。未测任意正则、Unicode/多行、性能或并发。
运行后对照结果
default: input=XabcY region=[1,4) group=abc span=[1,4)
bounds anchoring=false transparent=false: anchors=false lookaround=false
bounds anchoring=false transparent=true: anchors=false lookaround=true
bounds anchoring=true transparent=false: anchors=true lookaround=false
bounds anchoring=true transparent=true: anchors=true lookaround=true
region=abcYY: matches=false lookingAt=true find=true
reset: region=[0,12) anchoring=false transparent=true group=undefined
find(7): region=[0,12) group=def span=[7,10)
empty-region=matched; invalid-regions=3 rejected; invalid-find-start=1 rejected
PASS 39 checks; fixed ASCII input, no regex performance claim
把区域、规则和状态一起记录,才知道实际检查了什么
若一个校验步骤应该只检查原来的区间,调用任何会 reset 的方法后都要重新核对区域;若规则依赖左右上下文,还要明确透明边界是否符合这项需求。可见上下文与实际匹配内容是两个不同范围。
本实验只含固定 ASCII 字符串和少量规则,验证区域边界及状态转换。没有测 Unicode 字符簇、多行文本、复杂回溯、吞吐或任意用户规则的资源消耗,不能把通过这 39 项写成通用解析器已经安全或完备。
Matcher 是带状态的对象,官方文档不保证多个线程同时使用同一实例安全。这里所有调用在单线程按顺序执行,没有进行并发验证;需要并发使用时,应单独处理每次匹配状态的归属。
记住这三点
- region 指定匹配区域,start
/end仍对应完整输入的位置。 - 锚点边界和透明边界分别控制区域边缘与圈外上下文。
- reset 恢复完整区域但保留边界开关;
find(start)会先重置。
别把例子用过头
- 把 region 等同于先 substring 后创建全新的 Matcher。
- 以为透明边界允许规则把区域外字符一起消耗。
- 用 find 成功代替整个字段符合 matches。
- 调用
find(start)后仍以为旧区域限制保持不变。
自己试一小步
在 XabcY 中只允许匹配 abc,同时要求左边是 X、右边是 Y,你怎样把区域与状态约定写清楚?
- 先画出每个字符下标,列出要匹配的内容和只用于观察的上下文。
- 分别预测两个边界开关四种组合下的锚点规则与前后视规则结果。
- 在一次成功匹配后调用 reset、region 和
find(start),每一步记录区域及 group 是否可读。 - 换成空区域、非法区间和前后字符不符合要求的输入,自写断言并保留完整输出。
做到这些,就算完成
- 观察范围和匹配内容分别记录。
- 下标始终明确对应原输入还是新字符串。
- 不把有限规则实验扩成任意正则性能或安全保证。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Java SE 17 Matcher:区域、边界开关与重置 (新标签页)↗
区域设置、默认边界、matches
/lookingAt/find差别,以及 reset 和find(start)对状态的影响。 - Java SE 17 Pattern:锚点与前后视 (新标签页)↗
本实验所用 ^、
$和零宽正向前后视的语法含义。 - Java SE 17 MatchResult:位置与匹配内容 (新标签页)↗
start
/end的位置含义、group 与原输入 substring 的对应,以及没有有效匹配时的异常。