2. 问题场景介绍

在Java开发中,我们经常需要提取字符串中括号内的文本。当输入仅包含一对括号时,可以通过两次replaceAll()调用简单实现:

String myString = "a b c (d e f) x y z";
 
String result = myString.replaceAll(".*[(]", "")
  .replaceAll("[)].*", "");
assertEquals("d e f", result);

原理:第一次调用删除(之前的所有字符,第二次调用删除)之后的所有字符。

但当输入包含多对括号时,这种方法就失效了。例如:

static final String INPUT = "a (b c) d (e f) x (y z)";

我们期望提取出以下结果:

static final List<String> EXPECTED = List.of("b c", "e f", "y z");

接下来我们探讨几种高效解决方案。

3. 贪婪 vs 非贪婪正则表达式

正则表达式是处理此类问题的利器。初学者可能会尝试这种模式:

String myRegex = "[(](.*)[)]";
Matcher myMatcher = Pattern.compile(myRegex)
  .matcher(INPUT);
List<String> myResult = new ArrayList<>();
while (myMatcher.find()) {
    myResult.add(myMatcher.group(1));
}
assertEquals(List.of("b c) d (e f) x (y z"), myResult);

踩坑警告:结果只匹配到"b c) d (e f) x (y z",因为.*贪婪匹配,会从第一个(匹配到最后一个)

解决方案:使用非贪婪匹配(在*后加?):

String regex = "[(](.*?)[)]";
List<String> result = new ArrayList<>();
Matcher matcher = Pattern.compile(regex)
  .matcher(INPUT);
while (matcher.find()) {
    result.add(matcher.group(1));
}
assertEquals(EXPECTED, result);

关键点.*?确保每次匹配到最近的)就停止。

4. 使用否定字符类

除了非贪婪量词,还可以用否定字符类[^)]

String regex = "[(]([^)]*)";
List<String> result = new ArrayList<>();
Matcher matcher = Pattern.compile(regex)
  .matcher(INPUT);
while (matcher.find()) {
    result.add(matcher.group(1));
}
assertEquals(EXPECTED, result);

模式解析

  • [(]:匹配左括号
  • ([^)]*):捕获组,匹配任意非)字符
  • ⚠️ 注意:[^)]会持续匹配直到遇到),完美避开贪婪问题

进阶技巧:使用正向回顾断言(lookbehind)简化模式:

String regex = "(?<=[(])[^)]*";
List<String> result = new ArrayList<>();
Matcher matcher = Pattern.compile(regex)
    .matcher(INPUT);
while (matcher.find()) {
    result.add(matcher.group());
}
assertEquals(EXPECTED, result);

优势(?<=\[(\])是零宽断言,不消耗字符,无需额外捕获组。

5. 使用Apache Commons Lang3的StringUtils

如果项目已集成Apache Commons Lang3,可直接使用StringUtils

// 单对括号场景
String myString = "a b c (d e f) x y z";
String result = StringUtils.substringBetween(myString, "(", ")");
assertEquals("d e f", result);

// 多对括号场景
String[] results = StringUtils.substringsBetween(INPUT, "(", ")");
assertArrayEquals(EXPECTED.toArray(), results);

推荐场景:当项目已依赖该库时,这是最简洁的方案。

6. 总结

本文探讨了Java中提取括号文本的四种方案:

  1. 基础正则:适合单括号场景
  2. 非贪婪匹配:解决多括号贪婪问题
  3. 否定字符类:高效避免回溯
  4. StringUtils工具类:依赖库时的最佳实践

完整示例代码见GitHub仓库


原始标题:Extracting Text Between Parentheses in Java