1. 概述

在这篇快速教程中,我们将聚焦于 Java 中字符串的子串提取功能。

我们主要使用 String 类的方法,也会涉及 Apache Commons 的 StringUtils 类的一些实用方法。

在以下所有示例中,我们将使用如下字符串:

String text = "Julia Evans was born on 25-09-1984. "
  + "She is currently living in the USA (United States of America).";

2. substring 基础用法

我们先来看一个非常简单的例子:通过起始索引提取子串:

assertEquals("USA (United States of America).", 
  text.substring(67));

这个例子中,我们提取了 Julia 的居住国家信息。

我们也可以指定结束索引,如果不指定,substring 会一直提取到字符串末尾。

现在我们去掉结尾多余的句号:

assertEquals("USA (United States of America)", 
  text.substring(67, text.length() - 1));

上面的例子中,我们通过精确位置提取了子串。

2.1. 根据特定字符动态获取子串

如果位置需要根据某个字符或字符串动态计算,我们可以使用 indexOf 方法:

assertEquals("United States of America", 
  text.substring(text.indexOf('(') + 1, text.indexOf(')')));

还有一个类似的方法是 lastIndexOf。我们用它来提取年份“1984”,即最后一个短横线和第一个句号之间的内容:

assertEquals("1984",
  text.substring(text.lastIndexOf('-') + 1, text.indexOf('.')));

indexOflastIndexOf 都可以接受字符或字符串作为参数。我们来提取“USA”及括号中的内容:

assertEquals("USA (United States of America)",
  text.substring(text.indexOf("USA"), text.indexOf(')') + 1));

3. 使用 subSequence

String 类提供了一个与 substring 类似的方法 subSequence

*唯一的区别是它返回的是 CharSequence 而不是 String,并且必须同时指定起始和结束索引:*

assertEquals("USA (United States of America)", 
  text.subSequence(67, text.length() - 1));

4. 使用正则表达式

如果我们要提取符合特定模式的子串,正则表达式会派上用场。

在示例字符串中,Julia 的出生日期格式为“dd-mm-yyyy”。我们可以使用 Java 的正则表达式 API 来匹配这个模式。

首先,我们创建一个匹配“dd-mm-yyyy”的模式:

Pattern pattern = Pattern.compile("\\d{2}-\\d{2}-\\d{4}");

然后,我们将该模式应用于文本中查找匹配项:

Matcher matcher = pattern.matcher(text);

匹配成功后,我们可以提取匹配的字符串:

if (matcher.find()) {                                  
    Assert.assertEquals("25-09-1984", matcher.group());
}

更多关于 Java 正则表达式的知识,可以参考 这篇教程

5. 使用 split

我们可以使用 String 类的 split 方法来提取子串。比如,我们要提取示例字符串中的第一句话:

String[] sentences = text.split("\\.");

由于 split 方法接受正则表达式作为参数,我们需要对句号进行转义。结果是一个包含两句话的数组。

我们取第一句话(或遍历整个数组):

assertEquals("Julia Evans was born on 25-09-1984", sentences[0]);

⚠️ 注意:如果需要更准确的句子检测和分词,推荐使用 Apache OpenNLP。可以参考 这篇教程 了解更多。

6. 使用 Scanner

我们通常使用 Scanner 来解析原始类型和字符串,支持使用正则表达式。

Scanner 会根据分隔符模式将输入分割为多个 token,默认分隔符是空白字符。

我们来看看如何使用 Scanner 提取第一句话:

try (Scanner scanner = new Scanner(text)) {
    scanner.useDelimiter("\\.");           
    assertEquals("Julia Evans was born on 25-09-1984", scanner.next());    
}

在这个例子中,我们将句号作为分隔符(需要转义),然后断言第一个 token。

如果需要,我们可以通过 while 循环遍历所有 token:

while (scanner.hasNext()) {
   // do something with the tokens returned by scanner.next()
}

7. Maven 依赖

我们可以进一步使用一个非常实用的工具类:Apache Commons Lang 库中的 StringUtils

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.14.0</version>
</dependency>

你可以在 Maven Central 上找到最新版本。

8. 使用 StringUtils

Apache Commons 库提供了许多用于操作 Java 核心类型的实用方法。Apache Commons Lang 为 java.lang API 提供了大量辅助工具,尤其是字符串操作方法。

在这个例子中,我们将看到 如何提取两个字符串之间的子串

assertEquals("United States of America", 
  StringUtils.substringBetween(text, "(", ")"));

如果子串被两个相同的字符串包围,还有一个简化版本的方法:

substringBetween(String str, String tag)

同一个类中的 substringAfter 方法可以获取第一个分隔符之后的子串。

分隔符本身不会被返回:

assertEquals("the USA (United States of America).", 
  StringUtils.substringAfter(text, "living in "));

类似地,substringBefore 方法获取第一个分隔符之前的子串。

分隔符也不会被返回:

assertEquals("Julia Evans", 
  StringUtils.substringBefore(text, " was born"));

更多关于 Apache Commons Lang 的字符串处理功能,可以参考相关教程。

9. 提取子串前后的文本

前面我们探讨了多种提取子串的方法。有时候,我们的输入中包含特定子串,但我们不是要提取该子串本身,而是 提取它前后的文本

举个例子,假设我们要提取以下子串前后的文本:

"was born on 25-09-1984. She "

期望结果为:

Before: "Julia Evans "
After: "is currently living in the USA (United States of America)."

为了简化,我们假设输入字符串中该子串只出现一次。

9.1. 使用 substring() 方法

我们已经知道如何使用 substring() 提取子串。现在我们再次使用它来解决这个问题:

String substring = "was born on 25-09-1984. She ";
int startIdx = text.indexOf(substring);
String before = text.substring(0, startIdx);
String after = text.substring(startIdx + substring.length());
 
assertEquals("Julia Evans ", before);
assertEquals("is currently living in the USA (United States of America).", after);

如代码所示,使用 indexOf()substring() 的组合可以解决问题。

9.2. 使用 split() 方法

另一种思路是将子串视为分隔符,使用 split() 方法提取前后文本:

String substring = "was born on 25-09-1984. She ";
String[] result = text.split(Pattern.quote(substring));
assertEquals(2, result.length);
 
String before = result[0];
String after = result[1];
 
assertEquals("Julia Evans ", before);
assertEquals("is currently living in the USA (United States of America).", after);

可以看到,split() 返回一个包含两个元素的字符串数组:前后文本。

这个方法是有效的。但要注意的是,*我们使用了 Pattern.quote(substring) 而不是直接传入 substring*

这是因为 split() 接受正则表达式作为参数。Pattern.quote(substring) 告诉正则引擎将 substring 视为字面字符串,即其中的字符不具有特殊含义。

举个例子说明区别:

String input = "This is an *important* issue.";
String substring = " *important* ";
String[] resultWithoutQuote = input.split(substring);
assertEquals(1, resultWithoutQuote.length);
assertEquals(input, resultWithoutQuote[0]);

测试结果表明,如果直接传入 substring,返回的数组只有一个元素:整个输入字符串。这是因为 substring 中的某些字符具有正则表达式的特殊含义。

而使用 Pattern.quote() 后,结果正确:

String[] result = input.split(Pattern.quote(substring));
String before = result[0];
String after = result[1];
 
assertEquals("This is an", before);
assertEquals("issue.", after);

✅ 所以,当我们希望将正则表达式作为字面字符串处理时,应使用 Pattern.quote() 方法。

10. 提取两个字符串之间的文本

接下来我们来看另一个实际问题,并使用前面学到的技术来解决。

假设有如下输入字符串:

String input = "a <%One%> b <%Two%> c <%Three%>";

可以看到,文本中的一些词被“<%”和“%>”包围。我们的目标是提取这些子串。由于有多个“<% … %>”对,我们希望得到一个列表:

List<String> expected = List.of("One", "Two", "Three");

我们可以使用正则表达式的 PatternMatcher 来完成这个任务。首先,我们需要构建一个匹配“<%”和“%>”之间文本的正则表达式。这很简单:

"<%(.*)%>"

我们将中间的文本放在捕获组中以便提取。

接下来,我们测试这个正则表达式是否有效:

Pattern pattern = Pattern.compile("<%(.*)%>");
Matcher matcher = pattern.matcher(input);
List<String> result = new ArrayList<>();
while (matcher.find()) {
    result.add(matcher.group(1));
}
assertEquals(1, result.size());
assertEquals("One%> b <%Two%> c <%Three", result.get(0));

测试结果表明,我们没有得到预期的三个元素列表,而是一个元素。这是因为“*”是正则表达式中的贪婪量词。 换句话说,它会匹配从第一个“<%”到最后一个“%>”之间的所有内容。

✅ 修复方法很简单:在“*”后添加“?”,使其变为非贪婪匹配:

"<%(.*?)%>"

现在,正则表达式会匹配每一对“<%”和“%>”之间的内容。

再次测试:

Pattern pattern = Pattern.compile("<%(.*?)%>");
Matcher matcher = pattern.matcher(input);
List<String> result = new ArrayList<>();
while (matcher.find()) {
    result.add(matcher.group(1));
}
assertEquals(expected, result);

✅ 测试通过,我们成功提取了期望的字符串列表。

11. 总结

在这篇快速教程中,我们介绍了多种在 Java 中提取子串的方法。我们还提供了 其他字符串操作教程 供进一步学习。

代码示例可以在 GitHub 上找到。


原始标题:Get Substring from String in Java | Baeldung