在这篇文章中,我们将讨论另一种流行的代码结构String.replaceAll和String.replace方法的使用,我们将研究它如何影响 Java 11 中代码的性能以及您可以做些什么。
(请从性能的角度考虑以下所有代码)
(请不要关注数字,它们只是证明这一点的指标)
String.replaceAll
我不会说我喜欢编造示例,所以这次,我们将从 Spring 框架的现有代码库开始。我们看一下spring-messaging模块的MetadataEncoder类的这一行:
value.contains(".") ?value.replaceAll("\\.","%2E") :值;
你知道这里有什么问题吗?
这段代码试图做的就是对点符号进行编码,然后将结果传递给 HTTP URL。我很幸运能在流行的代码库中找到那个特定的代码片段。它在一行中有几件事。
如果您是一位经验丰富的开发人员,您已经知道String.replaceAll 方法使用正则表达式模式作为第一个参数:
公共字符串替换所有(字符串正则表达式,字符串替换){
返回模式。<em>编译</em>(正则表达式).matcher(this)
.replaceAll(replacement);
}
但是,在上面的代码中,我们只替换了点字符。很多时候,当您执行替换操作时,您不需要任何模式匹配。就性能而言,您可以使用另一种非常相似且更轻的方法 - String.replace:
公共字符串替换(CharSequence 目标,CharSequence 替换)
例如,当您需要像我们的示例中那样替换单个单词或单个字符时。
注意:在 Java 8 中,String.replace方法 使用里面的Pattern作为String.replaceAll。但是,自 Java 9 以来,它发生了变化。这为我们在现有代码库中提供了很大的优化空间。
此外,String.replace和String.replaceAll方法的使用在设计上似乎很容易出错。这是因为当您开始在 IDE 中输入内容并仔细查看这两种方法时,您可能会认为String.replace仅替换第一个匹配项,而replaceAll替换所有匹配项。直观地,您将选择String.replaceAll而不是String.replace。
另一个有趣的地方是上面的代码已经有了一个微优化,就是value.contains(“.”)方法的使用。因此,如果没有可替换的内容,则会检查字符串中的点符号以避免模式匹配。
好的,让我们修复上面的示例:
value.replace(".", "%2E");
我们还可以尝试应用String.indexOf(“.”)优化并检查这是否有助于Java 11 中使用String.replace方法:
value.contains(".") ?value.replace(".", "%2E") : 值;
让我们编写基准测试:
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 10, time = 1)
public class ReplaceAll {
@Param({"", "somePathNoDoT", "some.Path.With.Dot"})
String value;
@Benchmark
public String replaceAllWithContains() {
return value.contains(".") ? value.replaceAll("\\.", "%2E") : value;
}
@Benchmark
public String replaceAll() {
return value.replaceAll("\\.", "%2E");
}
@Benchmark
public String replace() {
return value.replace(".", "%2E");
}
@Benchmark
public String replaceWithContains() {
return value.contains(".") ? value.replace(".", "%2E") : value;
}
}
结果(分数越低意味着越快):
JDK 11.0.8, OpenJDK 64-Bit Server VM, 11.0.8+10
```
Benchmark (value) Mode Cnt Score Error Units
ReplaceAll.replace avgt 10 3.836 ± 0.060 ns/op
ReplaceAll.replaceAll avgt 10 147.857 ± 1.550 ns/op
ReplaceAll.replaceAllWithContains avgt 10 3.777 ± 0.069 ns/op
ReplaceAll.replaceWithContains avgt 10 3.778 ± 0.075 ns/op
ReplaceAll.replace somePathNoDoT avgt 10 7.647 ± 0.646 ns/op
ReplaceAll.replaceAll somePathNoDoT avgt 10 156.495 ± 1.751 ns/op
ReplaceAll.replaceAllWithContains somePathNoDoT avgt 10 7.640 ± 0.291 ns/op
ReplaceAll.replaceWithContains somePathNoDoT avgt 10 7.545 ± 0.298 ns/op
ReplaceAll.replace some.Path.With.Dot avgt 10 123.856 ± 1.686 ns/op
ReplaceAll.replaceAll some.Path.With.Dot avgt 10 389.632 ± 18.308 ns/op
ReplaceAll.replaceAllWithContains some.Path.With.Dot avgt 10 378.527 ± 6.434 ns/op
ReplaceAll.replaceWithContains some.Path.With.Dot avgt 10 126.918 ± 0.940 ns/op
```
看起来String.indexOf(“.”)与String.replaceAll方法一起使用,实际上是有道理的,即使对于空输入字符串,编译和匹配模式也需要太多时间。约 50 倍的差异是巨大的。这同样适用于没有任何点符号的输入字符串。而当必须执行实际的替换工作时,String.replace方法的性能比String.replaceAll 高出三倍。
此外,似乎String.indexOf优化对Java 11 中的String.replace方法没有任何意义,而在 Java 8 中当我们内部有模式匹配时需要它。现在它甚至让它变慢了一点。这是因为String.replace方法的实现,因为它已经在里面执行了String.indexOf搜索。所以我们在这里做双重工作。
“但是你可以预编译正则模式表达式并使用它来提高String.replaceAll方法的性能”,你会说。同意。事实上,我们在Blynk中经常这样做。
让我们检查预编译模式如何更改数字:
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 10, time = 1)
public class ReplaceAllPrecompiled {
@Param({"", "somePathNoDoT", "some.Path.With.Dot"})
String value;
private static final Pattern DOT_PATTERN = Pattern.compile("\\.");
@Benchmark
public String replaceAllWithPrecompiled() {
return DOT_PATTERN.matcher(value).replaceAll("%2E");
}
}
结果(分数越低意味着越快):
JDK 11.0.8, OpenJDK 64-Bit Server VM, 11.0.8+10
```
Benchmark (value) Mode Cnt Score Error Units
replace avgt 10 3.843 ± 0.100 ns/op
replace somePathNoDoT avgt 10 8.693 ± 0.693 ns/op
replace some.Path.With.Dot avgt 10 127.022 ± 8.842 ns/op
replaceAll avgt 10 156.550 ± 10.741 ns/op
replaceAll somePathNoDoT avgt 10 159.619 ± 4.654 ns/op
replaceAll some.Path.With.Dot avgt 10 391.912 ± 11.445 ns/op
replaceAllPrecompiled avgt 10 52.466 ± 0.791 ns/op
replaceAllPrecompiled somePathNoDoT avgt 10 64.337 ± 0.854 ns/op
replaceAllPrecompiled some.Path.With.Dot avgt 10 274.867 ± 3.535 ns/op
是的,现在数字好多了。但是,仍然不如String.replace方法那么好。我们可以尝试更多地专注于优化正则模式,但是关于它的帖子已经足够多。
您可能会认为最初的示例只是一个地方,而且非常罕见。让我们看看 GitHub:
GitHub 刚刚索引了一些存储库,在第一个屏幕上,六个String.replaceAll用法中有五个可以替换为String.replace!是的,许多项目仍在使用 Java 8,这对它们没有任何影响。但是,在大多数开发人员迁移到 Java 11 之后,我们将有很多缓慢的遗留代码。我们可以立即开始改进它。
StringUtils.replace
在 Java 11 之前,当您有使用String.replace方法的热门路径时,您必须在一些 3-d 方库中找到更快的替代方案,甚至编写自己的自定义版本。最著名的 3-d 方替代方案是 Apache Commons StringUtils.replace方法。
例如,可以在 Spring 框架中找到自定义替换方法的示例。在这里。
让我们看另一个 Spring代码片段:
字符串内部名称 = StringUtils 替换(类名,“。”,“/”);
你知道这里有什么问题吗?
让我们在基准测试中检查 Spring(最新源代码)、Apache Commons(commons-lang3 的最新版本 3.11)和 Java 方法:
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 10, time = 1)
public class ReplaceCustom {
@Param({"", "somePathNoDoT", "some.Path.With.Dot"})
String value;
@Benchmark
public String replace() {
return value.replace(".", "/");
}
@Benchmark
public String replaceSpring() {
return springReplace(value, ".", "/");
}
@Benchmark
public String replaceApache() {
return StringUtils.replace(value, ".", "/");
}
}
结果(分数越低意味着越快):
JDK 11.0.8, OpenJDK 64-Bit Server VM, 11.0.8+10
```
Benchmark (value) Mode Cnt Score Error Units
ReplaceCustom.replace avgt 10 4.497 ± 1.085 ns/op
ReplaceCustom.replace somePathNoDoT avgt 10 7.944 ± 0.289 ns/op
ReplaceCustom.replace some.Path.With.Dot avgt 10 106.270 ± 5.095 ns/op
ReplaceCustom.replaceApache avgt 10 3.784 ± 0.065 ns/op
ReplaceCustom.replaceApache somePathNoDoT avgt 10 8.168 ± 0.052 ns/op
ReplaceCustom.replaceApache some.Path.With.Dot avgt 10 121.248 ± 1.935 ns/op
ReplaceCustom.springCustomReplace avgt 10 3.628 ± 0.058 ns/op
ReplaceCustom.springCustomReplace somePathNoDoT avgt 10 7.991 ± 0.268 ns/op
ReplaceCustom.springCustomReplace some.Path.With.Dot avgt 10 108.191 ± 6.138 ns/op
嗯,看起来所有方法都非常接近。Apache Commons 有点慢,但那是因为它有额外的逻辑来处理不区分大小写的替换。所以一切都是有道理的。
现在,由于我们具有相似的性能,我们不再需要自定义方法或 3-d 方库来执行Java 11 中的快速String.replace。
但是这条线还是不行:
返回值.replace(".", "/");
你知道这里有什么问题吗?
与发生实际字符串替换的第一个示例相反,这里我们有一个用于搜索和替换的字符。众所周知,Java 有一个专门的字符替换版本:
字符串替换(char oldChar, char newChar)
让我们也将它添加到我们的基准测试中:
@Benchmark
public String replaceChar() {
return value.replace('.', '/');
}
Apache Commons 库也有StringUtils.replaceChars,但它在内部使用String.replace(char, char),所以我们将跳过它。我们离从您的项目中消除这个 3-d 派对库更近了一步。
结果(分数越低意味着越快):
JDK 11.0.8, OpenJDK 64-Bit Server VM, 11.0.8+10
```
Benchmark (value) Mode Cnt Score Error Units
ReplaceCustom.replace avgt 10 4.497 ± 1.085 ns/op
ReplaceCustom.replace somePathNoDoT avgt 10 7.944 ± 0.289 ns/op
ReplaceCustom.replace some.Path.With.Dot avgt 10 106.270 ± 5.095 ns/op
ReplaceCustom.replaceChar avgt 10 3.689 ± 0.040 ns/op
ReplaceCustom.replaceChar somePathNoDoT avgt 10 8.787 ± 0.119 ns/op
ReplaceCustom.replaceChar some.Path.With.Dot avgt 10 26.824 ± 0.391 ns/op
ReplaceCustom.springCustomReplace avgt 10 3.628 ± 0.058 ns/op
ReplaceCustom.springCustomReplace somePathNoDoT avgt 10 7.991 ± 0.268 ns/op
ReplaceCustom.springCustomReplace some.Path.With.Dot avgt 10 108.191 ± 6.138 ns/op
用于单个字符替换的 Java 字符专用版本比重载的String.replace(String, String)和自定义 Spring方法快四倍。有趣的是,即使在 Java 8 中,String.replace(char, char)的优化也足够好。所以 Spring 可以安全地使用String.replace(char, char)。
字符串.remove
我希望你还不累:)?让我们看一下最后一个代码示例:
value.replace(".", "");
你知道这里有什么问题吗?
不幸的是,Java 仍然没有String.remove方法。作为替代方案,我们可以使用String.replace(char, char)方法,但 Java 也没有空字符文字,我们不能编写这样的代码:
value.replace('.', '');
所以,相反,我们必须使用上面的“hack”。
幸运的是,有许多 3-d 方实现,例如 Apache Commons StringUtils.remove(String, char)。例如,Spring 使用自己的自定义实现,基于自己的自定义替换方法:
public static String delete(String inString, String pattern) {
return StringUtils. <em>替换</em>(inString,模式,“”);
}
让我们再次检查我们的基准测试中的 Spring、Apache Commons 和 Java 方法以进行删除操作:
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 5, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Measurement(iterations = 10, time = 1)
public class RemoveChar {
@Param({"", "somePathNoDoT", "some.Path.With.Dot"})
String value;
@Benchmark
public String remove() {
return value.replace(".", "");
}
@Benchmark
public String removeApache() {
return StringUtils.remove(value, '.');
}
@Benchmark
public String removeSpring() {
return springDelete(value, ".");
}
}
结果(分数越低意味着越快):
JDK 11.0.8, OpenJDK 64-Bit Server VM, 11.0.8+10
```
Benchmark (value) Mode Cnt Score Error Units
RemoveChar.remove avgt 10 4.405 ± 1.646 ns/op
RemoveChar.remove somePathNoDoT avgt 10 7.820 ± 0.296 ns/op
RemoveChar.remove some.Path.With.Dot avgt 10 92.185 ± 3.752 ns/op
RemoveChar.removeApache avgt 10 3.708 ± 0.070 ns/op
RemoveChar.removeApache somePathNoDoT avgt 10 10.018 ± 0.103 ns/op
RemoveChar.removeApache some.Path.With.Dot avgt 10 37.822 ± 0.486 ns/op
RemoveChar.removeSpring avgt 10 3.609 ± 0.033 ns/op
RemoveChar.removeSpring somePathNoDoT avgt 10 7.996 ± 0.308 ns/op
RemoveChar.removeSpring some.Path.With.Dot avgt 10 87.148 ± 1.391 ns/op
```
专门的 Apache Commons 版本几乎赢了三倍。这里有趣的是,即使是专门和优化的 char 删除也比String.replace(char, char)中的 char 替换要慢。肯定还有进一步改进的空间。
希望有一天我们会在 Java 中看到这一点。
结论
- 使用 Java 11
- 尽可能在String.replaceAll 上使用 S string.replace
- 如果必须使用String.replaceAll,请尝试在热路径中预编译正则表达式
- 用的专门版本,请与string.replace(字符,字符),而不是与string.replace(字符串,字符串)时,你可以
- 对于热路径,您仍然需要考虑 3-d 方库或自定义方法,而不是String.replace(value, “”)代码模式
这是基准测试的源代码,您可以自己尝试。
感谢您的关注,敬请期待。