在 Java 字符串处理过程中,很多业务数据会带有“前缀(关键内容*后续描述)”这样的格式,例如日志打印、接口返回值或配置文件中的占位符。程序往往只需要提取括号内星号之前的子字符串,而不需要星号本身以及其后的内容。面对这种需求,如果采用手动查找下标再截取的方式,需要先定位左括号、再定位星号、再定位右括号,还要处理多个括号片段或缺失部分时的边界情况,代码会显得冗长且容易出错。正则表达式提供了一种声明式的匹配方案,能把“找到完整结构”和“只保留目标部分”两件事合并到一个表达式中,配合 Java 的替换或匹配 API,可以显著简化实现。

本文围绕一个典型字符串进行说明。假设输入为 test(abc*def)demo(xyz*123),期望提取出 abc 和 xyz。核心思路是使用正则捕获组识别括号中星号之前的部分,再通过 replaceAll 将整个括号片段替换为捕获组内容。接下来从需求拆解、正则语法、代码实现和特殊场景等角度展开。
需求拆解与匹配目标
目标字符串的结构可以抽象为:普通前缀文本、左括号、需要提取的子串、星号、无需保留的后缀文本、右括号、普通后续文本。例如在 test(abc*def) 中,需要提取的子串是 abc,星号之后的 def 和括号本身都需要在结果中去掉。多个这样的结构可能连续出现,也可能被其他文本分隔。
如果仅用 indexOf 和 substring,需要分别找到左括号、星号和右括号的位置,并对每一段切片处理。当括号数量不固定、括号内可能没有星号、或者同一字符串中混杂其他括号时,位置计算会变得复杂。而正则替代表达的是“什么样的文本模式应该被替换成什么”,更接近需求本身。
因此,匹配目标可以概括为:每次匹配必须覆盖从左括号到右括号的完整片段,同时通过捕获组记住星号之前的内容,最终替换时只保留该捕获组。这个目标就决定了正则的结构。
正则表达式设计与语法解析
用于该需求的正则可以写为 (([^*]+)*.*?)。它由若干个关键部分组成:( 用于匹配左括号,因为左括号在正则中表示分组的开始,所以需要反斜杠转义;([^*]+) 是捕获组,匹配一个或多个非星号字符;* 匹配星号本身;.*? 表示星号之后到右括号之前的内容,使用非贪婪匹配;最后的 ) 匹配右括号。
其中 [^*]+ 使用排除型字符类。方括号内的 ^ 放在开头表示取反,即匹配任意不属于星号的字符;加号表示至少匹配一次。如果括号中星号前为空,例如 (*abc),这样的内容不会被该正则匹配到。根据业务需要,如果允许空字符串作为提取结果,可以将加号改为星号,即 ([^*]*)。
另一个需要关注的是 .*? 的非贪婪特性。如果不加问号,.* 会尽可能多地匹配,可能跨越多个括号片段。例如在处理 test(abc*def)demo(xyz*123) 时,贪婪匹配可能一次性吞掉到最后一个右括号的内容,导致捕获组和替换结果都不正确。使用 .*? 可以让每次匹配尽量短,并在遇到第一个右括号时结束。
public class RegexExtractDemo {
public static void main(String[] args) {
// 待处理的原始字符串
String originalStr = "test(abc*def)demo(xyz*123)other(hello*world)";
// 定义正则表达式,匹配括号内的星号前内容结构
String regex = "\(([^*]+)\*.*?\)";
// 使用replaceAll替换,保留捕获组1的内容
String result = originalStr.replaceAll(regex, "$1");
// 输出结果:testabcdemoxyzotherhello
System.out.println(result);
}
}
Java 代码实现与两种提取方式
在 Java 中,最直接的写法是使用 String 类的 replaceAll 方法。它的第一个参数是正则表达式,第二个参数是替换字符串。正则中的捕获组可以在替换字符串中通过 $1、$2 等方式引用。上述正则只有一个捕获组,因此替换为 $1 即可保留括号内星号前的内容,同时丢弃括号、星号及星号后的部分。执行上面代码后,输出结果为 testabcdemoxyzotherhello。
这段代码的执行过程是:正则引擎在原始字符串上查找所有满足模式的子串,每找到一处,就按照替换字符串的规则生成替代文本,最后返回新的字符串
如果只需要得到最终替换字符串,replaceAll 已经足够;但如果要判断每处匹配的位置、获取完整匹配子串,或者对捕获组做进一步校验,更适合使用 Pattern 与 Matcher。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexExtractWithMatcher {
public static void main(String[] args) {
String originalStr = "test(abc*def)demo(xyz*123)other(hello*world)";
String regex = "\(([^*]+)\*.*?\)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(originalStr);
StringBuilder builder = new StringBuilder();
while (matcher.find()) {
builder.append(matcher.group(1));
}
System.out.println(builder.toString());
}
}
这段代码的运行结果同样是 testabcdemoxyzotherhello。matcher.find() 会推动匹配游标从前往后扫描,每找到一处符合正则的子串就返回 true;matcher.group(1) 则返回第一个捕获组,也就是括号内星号前的内容。相比 replaceAll,这种方式能拿到每一次匹配的更多信息,例如起止下标、完整匹配文本,也便于在循环中加入自己的业务判断。
两种方式怎么选
实际开发中可以根据需求决定:如果只是将原字符串中的结构统一替换,replaceAll 最直接;如果需要对每个匹配项单独处理、记录位置或者过滤部分结果,Pattern 与 Matcher 会更灵活。两者使用的正则表达式可以完全一致。
几个容易忽略的边界情况
- 星号前内容不能为空:正则中的
[^*]+要求至少一个非星号字符。如果业务允许空内容,比如(*def)也需要匹配,应改成(([^)]*?)*.*?)或其他符合要求的表达式。 - 括号嵌套问题:当前正则默认括号不嵌套。如果字符串中存在嵌套括号,如
(ab(cd)*ef),表达式中的.*?可能在第一个右括号处结束,从而无法正确匹配完整结构,需要更复杂的设计或解析器。 - 换行与点号:默认情况下
.不匹配换行符。如果括号内容跨行,需要在编译时使用Pattern.compile(regex, Pattern.DOTALL),或者把.*?改成[sS]*?。 - 特殊符号转义:括号和星号在正则中都有特殊含义,因此括号需要写成
(、),星号需要写成*。在 Java 字符串里还要再转义一层,所以写成"\(([^*]+)\*.*?\)"。
掌握了这些要点后,再遇到类似“从指定结构中提取部分内容”的需求,就可以先确定结构边界,再用捕获组保留目标内容,最后根据替换或遍历场景选择 replaceAll 或 Matcher。本文的示例虽然简单,但已经覆盖了正则提取中最常用的转义、捕获、非贪婪匹配和 Java 实现方式,可以作为处理更复杂字符串的起点。