性能提升10倍,String居然有这种坑

401 阅读2分钟

JDK 的 String 类式java日常开发中最常用的类,但是因为设计时间过早,其中隐含了不少问题,没有做到最好的性能体验。

今天我们要聊的就是String.replace() 方法。

String.replcae() vs StringUtils.replace()

StringUtils 是apache commons lang 中的工具包,这个库是目前业界使用最广泛,历史最悠久的String工具库。

插入mvn排行图

整个mvn依赖库中,排行第九,core库中排行第二。这就是它的实力。

性能对比

这里我们使用JMH 这个java届最权威的框架来进行性能测试,我们在用例中准备了6组测试case,大家感兴趣的话,可以自行运行。

本文测试环境基于jdk8u_312.

package com.moyucoding.test;

import org.apache.commons.lang3.StringUtils;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
 
@Fork(value = 3, jvmArgsAppend = "-Djmh.stack.lines=3")
@Warmup(iterations = 5)
@Measurement(iterations = 7)
public class StringReplaceBenchmark {
 
    private static final String SHORT_STRING_NO_MATCH = "abc";
    private static final String SHORT_STRING_ONE_MATCH = "a'bc";
    private static final String SHORT_STRING_SEVERAL_MATCHES = "'a'b'c'";
    private static final String LONG_STRING_NO_MATCH = 
      "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc";
    private static final String LONG_STRING_ONE_MATCH = 
      "abcabcabcabcabcabcabcabcabcabcabca'bcabcabcabcabcabcabcabcabcabcabcabcabc";
    private static final String LONG_STRING_SEVERAL_MATCHES = 
      "abcabca'bcabcabcabcabcabc'abcabcabca'bcabcabcabcabcabca'bcabcabcabcabcabcabc";


    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }
 
    @Benchmark
    public void testStringReplaceShortStringNoMatch(Blackhole blackhole) {
        blackhole.consume(SHORT_STRING_NO_MATCH.replace("'""''"));
    }
 
    @Benchmark
    public void testStringReplaceLongStringNoMatch(Blackhole blackhole) {
        blackhole.consume(LONG_STRING_NO_MATCH.replace("'""''"));
    }
 
    @Benchmark
    public void testStringReplaceShortStringOneMatch(Blackhole blackhole) {
        blackhole.consume(SHORT_STRING_ONE_MATCH.replace("'""''"));
    }
 
    @Benchmark
    public void testStringReplaceLongStringOneMatch(Blackhole blackhole) {
        blackhole.consume(LONG_STRING_ONE_MATCH.replace("'""''"));
    }
 
    @Benchmark
    public void testStringReplaceShortStringSeveralMatches(Blackhole blackhole) {
        blackhole.consume(SHORT_STRING_SEVERAL_MATCHES.replace("'""''"));
    }
 
    @Benchmark
    public void testStringReplaceLongStringSeveralMatches(Blackhole blackhole) {
        blackhole.consume(LONG_STRING_SEVERAL_MATCHES.replace("'""''"));
    }
 
    @Benchmark
    public void testStringUtilsReplaceShortStringNoMatch(Blackhole blackhole) {
        blackhole.consume(StringUtils.replace(SHORT_STRING_NO_MATCH"'""''"));
    }
 
    @Benchmark
    public void testStringUtilsReplaceLongStringNoMatch(Blackhole blackhole) {
        blackhole.consume(StringUtils.replace(LONG_STRING_NO_MATCH"'""''"));
    }
 
    @Benchmark
    public void testStringUtilsReplaceShortStringOneMatch(Blackhole blackhole) {
        blackhole.consume(StringUtils.replace(SHORT_STRING_ONE_MATCH"'""''"));
    }
 
    @Benchmark
    public void testStringUtilsReplaceLongStringOneMatch(Blackhole blackhole) {
        blackhole.consume(StringUtils.replace(LONG_STRING_ONE_MATCH"'""''"));
    }
 
    @Benchmark
    public void testStringUtilsReplaceShortStringSeveralMatches(Blackhole blackhole) {
        blackhole.consume(StringUtils.replace(SHORT_STRING_SEVERAL_MATCHES"'""''"));
    }
 
    @Benchmark
    public void testStringUtilsReplaceLongStringSeveralMatches(Blackhole blackhole) {
        blackhole.consume(StringUtils.replace(LONG_STRING_SEVERAL_MATCHES"'""''"));
    }
}

经过一个小时的测试后,我们得出的结果是

Benchmark                                                                Mode  Cnt          Score         Error  Units
StringReplaceBenchmark.testStringReplaceLongStringNoMatch               thrpt   21   12210067.250 ±  342987.422  ops/s
StringReplaceBenchmark.testStringReplaceLongStringOneMatch              thrpt   21    3984303.788 ±   74056.443  ops/s
StringReplaceBenchmark.testStringReplaceLongStringSeveralMatches        thrpt   21    3181532.512 ±   74569.757  ops/s
StringReplaceBenchmark.testStringReplaceShortStringNoMatch              thrpt   21   25666110.698 ± 1573992.322  ops/s
StringReplaceBenchmark.testStringReplaceShortStringOneMatch             thrpt   21   11668173.747 ±  290415.490  ops/s
StringReplaceBenchmark.testStringReplaceShortStringSeveralMatches       thrpt   21    6001214.622 ±   98091.136  ops/s
StringReplaceBenchmark.testStringUtilsReplaceLongStringNoMatch          thrpt   21   49604380.534 ±  429136.912  ops/s
StringReplaceBenchmark.testStringUtilsReplaceLongStringOneMatch         thrpt   21   13529127.012 ±  249110.511  ops/s
StringReplaceBenchmark.testStringUtilsReplaceLongStringSeveralMatches   thrpt   21   10322497.588 ±  167876.297  ops/s
StringReplaceBenchmark.testStringUtilsReplaceShortStringNoMatch         thrpt   21  267192070.389 ± 4515902.677  ops/s
StringReplaceBenchmark.testStringUtilsReplaceShortStringOneMatch        thrpt   21   37803496.123 ±  548901.194  ops/s
StringReplaceBenchmark.testStringUtilsReplaceShortStringSeveralMatches  thrpt   21   20540547.076 ±  395061.937  ops/s

这个结果显示,当前运行的是吞吐量模式,统计出来的结果就是 ops/s ,就是每秒能进行的操作量,这个值当然是越大越好。 这个结果不太方便看,所以我为你们贴心地准备了图表。

// 图表

从图表中可以一目了然得看到,在第四个测试用例中,StringUtils 取得了10倍于String 的性能优势。

需要注意的是,这个结果仅针对 jdk8u_312,其他版本的结果:

  • jdk9的String.replace 方法和StringUtils.replace 持平;
  • jdk13中String.replace 方法比StringUtils.replace 快30%.

考虑到大多数项目和公司依然还在jdk8 ,甚至更低的版本,将String.replace 替换喂 StringUtils.replace 是完全有益的:

  • 减少cpu使用
  • 使应用程序响应速度更快

StringUtils 更快的原因

关键在于第3807行,进行其他更重的操作时,会进进行indexOF查找,看看对应的字符串是否匹配,否则直接结束。

JDK9 之所以能和StringUtils 一样,也是因为做了同样的优化

在项目中替换

只需要简单地把"String.replace" 换成 "StringUtils.replace" 即可获得十倍的性能提升。

比如参考下面这个方法

url = url.replace(domain, "bytedance").replace("https://""http://");

替换后

url = StringUtils.replace(url,domain,"bytedance");
url = StringUtils.replace(url,"https://""http://");