环境:SpringBoot3.2.5
1. 简介
在开发中,字符串拼接是非常常见的操作,广泛应用于日志记录、数据处理、用户界面生成等场景。然而,不同的字符串拼接方式在性能上有着显著的差异,这一点往往被开发人员忽视。本文将详细介绍 Java 中的 8 种字符串拼接方式,并通过性能测试揭示这些方法的实际表现,结果令人意外。
Java中可以使用如下8种方式进行字符串的拼接:
- "+"操作符
- String#concat方法
- String#join方法
- String#format方法
- Stream流方式
- StringBuffer
- StringBuilder
- StringJoiner
下面我们依次介绍这8中方式;接下来的示例我们都将建立在JMH之上。
2. 实战案例
2.1 "+"操作符
这是最简单的方法,也是我们可能最熟悉的一种。它可以使用加号(+)运算符来连接字符串字面量、变量或者二者的组合:
@Benchmark
public void plusOperator(Blackhole hole) {
String str1 = "Pack";
String str2 = " xxxooo";
String result = str1 + str2;
hole.consume(result);
}
2.2 String#concat方法
concat() 方法由 String 类提供,可用于将两个字符串连接在一起。
@Benchmark
public void concat(Blackhole hole) {
String str1 = "Pack";
String str2 = " xxxooo";
String result = str1.concat(str2);
hole.consume(result);
}
2.3 String#join方法
String.join() 是 Java 8 以后新增的静态方法。它允许使用指定的分隔符连接多个字符串。
@Benchmark
public void join(Blackhole hole) {
String str1 = "Pack";
String str2 = " xxxooo";
String result = String.join("", str1, str2);
hole.consume(result);
}
2.4 String#format方法
String.format() 用于使用占位符和格式指定符格式化字符串。通过使用实际值替换占位符,可以创建格式化字符串。
@Benchmark
public void format(Blackhole hole) {
String str1 = "Pack";
String str2 = " xxxooo";
String result = String.format("%s%s", str1, str2);
hole.consume(result);
}
2.5 Stream流
它为在对象集合上执行操作提供了一种富有表现力的方法,并允许我们使用 Collectors.joining() 来集中字符串。
@Benchmark
public void stream(Blackhole hole) {
List<String> strList = Arrays.asList("Pack", " xxxooo");
String result = strList.stream().collect(Collectors.joining());
hole.consume(result);
}
2.6 StringBuffer
StringBuffer
提供了一个可变的字符序列。它允许对字符串进行动态操作而无需创建新的对象。值得一提的是,它被设计为线程安全的,这意味着它可以被多个线程安全地并发访问和修改。
@Benchmark
public void stringBuffer(Blackhole hole) {
StringBuffer buffer = new StringBuffer();
buffer.append("Pack") ;
buffer.append(" xxxooo") ;
String result = buffer.toString() ;
hole.consume(result) ;
}
2.7 StringBuilder
StringBuilder
和 StringBuffer
的用途相同。它们之间唯一的区别是 StringBuilder
不是线程安全的,而 StringBuffer
是。在不需要考虑线程安全的单线程场景中,StringBuilder
是非常完美的选择。
@Benchmark
public void stringBuilder(Blackhole hole) {
StringBuilder builder = new StringBuilder() ;
builder.append("Pack") ;
builder.append(" xxxooo") ;
String result = builder.toString() ;
hole.consume(result) ;
}
2.8 StringJoiner
StringJoiner
是从 Java 8 开始引入的一个新类。它的功能与 StringBuilder
类似,提供了一种使用分隔符连接多个字符串的方式。尽管它与 StringBuilder
有相似之处,但 StringJoiner
也不是线程安全的。
@Benchmark
public void stringJoiner(Blackhole hole) {
StringJoiner joiner = new StringJoiner("");
joiner.add("Pack") ;
joiner.add(" xxxooo") ;
String result = joiner.toString() ;
hole.consume(result) ;
}
以上我们简单的介绍了每一种字符串拼接的使用。
3. 性能测试
接下来,我们通过JMH进行性能的测试,首先我们在类上添加如下的注解:
// 预热1s钟,预热3次
@Warmup(iterations = 3, time = 1)
// 启动多少个进程
@Fork(value = 1, jvmArgsAppend = {"-Xms512m", "-Xmx512m"})
// 指定显示结果(枚举值)
@BenchmarkMode(Mode.AverageTime)
// 指定显示结果单位(枚举值)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
// 迭代10次,每次2s
@Measurement(iterations = 10, time = 2, timeUnit = TimeUnit.SECONDS)
public class StringJoinTest {
// 上面8种字符串拼接的方法
}
在本示例中,我们将通过main的方式运行,这种方式稍微有点不是特别准确,你可以选择jar的方式。
public static void main(String[] args) throws Exception {
Options options = new OptionsBuilder()
// 你要测试的类
.include(StringJoinTest.class.getSimpleName())
// 启动几个进程
.forks(1).build();
new Runner(options).run();
}
最终测试结果如下:
图片
下面是对上面每一列的说明(以第一行concat测试为例说明)
Benchmark
a.说明:基准测试的名称,通常是类名+方法名。
b.示例:StringJoinTest.concat。
Mode
a.说明:基准测试的模式,常见模式有:
- avgt:平均时间模式(Average Time),每个操作的平均时间。
- thrpt:吞吐量模式(Throughput),单位时间内完成的操作次数。
- sample:采样模式,用于收集详细的统计信息。
- ss:稳定状态模式,用于评估长时间运行的性能稳定性。
b.示例:avgt
Cnt
- 说明:迭代次数,即基准测试运行的次数。
- 示例:10
Score
a.说明:基准测试的主要结果指标。根据模式的不同,这个值的含义也不同。
- avgt:每个操作的平均时间(秒、毫秒、纳秒等)。
- thrpt:每秒完成的操作次数(ops/s)。
b.示例:8.584
Error
- 说明:结果的标准误差(Standard Error),表示结果的不确定性。
- 示例:0.175
Units
- 说明:结果的单位
- 示例:ns/op(每操作纳秒)
性能排序
- (+)plusOperator:6.154 ± 0.119 ns/op
- concat:8.584 ± 0.175 ns/op
- stringBuilder:11.560 ± 0.216 ns/op
- stringBuffer:12.340 ± 0.150 ns/op
- stringJoiner:29.932 ± 0.236 ns/op
- join:28.210 ± 0.241 ns/op
- stream:34.293 ± 0.284 ns/op
- format:409.691 ± 2.941 ns/op