在实际开发中,经常会遇到字符串拼接相关的操作,例如我们在编写动态SQL的时候,需要对SQL语句进行拼接,而SQL语句就可以看做是一个字符串的拼接操作,而在之前的分享中,我们知道String类型的对象是一个不可变的对象,也就是说SQL语句的拼接其实是需要创建一个新的String对象的。那么下面我们就来看看Java中对于字符串拼接操作有哪些注意的地方。
字符串拼接操作
我们常用的字符串拼接操作就是通过 “+” 来进行连接,如下所示。
String a = "Hello";
String b = "World!";
String str = a + b;
但是实际上,在编译的时候JDK会将上面这段代码编译成如下的这个操作。
String str = new StringBuilder().append(a).append(b).toString();
可以看出,拼接字符串使用了append()方法。该方法中调用了其父类AbstractStringBuilder的append()方法。
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
// 父类的 append方法
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
从源码中可以看到最终调用了它本身的一个getChars()方法,而这个方法内部最终调用的还是System.arraycopy()方法。
测试"+"和append()的性能
下面我们就来测试一下通过 “+” 和通过append() 来进行字符串拼接的操作性能。添加JMH的方式在之前的分享中已经提到过了,这里就不再重复编写了。测试类代码如下。
@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class TestString {
String a = "Hello";
String b = "World!";
@Benchmark
public String sttring(){
String str = a + b;
return str;
}
@Benchmark
public String appendStr(){
String str = new StringBuilder().append(a).append(b).toString();
return str;
}
}
主方法测试类如下所示。
public class Test {
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(TestString.class.getSimpleName())
.warmupBatchSize(5)
.measurementIterations(1)
.mode(Mode.AverageTime)
.forks(1)
.build();
new Runner(opt).run();
}
}
测试结果
Benchmark Mode Cnt Score Error Units
TestString.appendStr avgt 13.825 ns/op
TestString.sttring avgt 13.918 ns/op
从测试结果来看,通过"+"和append的方式来拼接字符串,其效果基本上是一样的,从结果上来看差异性不是太大。这是因为在JVM进行编译的时候会对上述的两段代码都进行优化,可以使用虚拟机参数-XX:+OptimizeStringConcat 来开启字符串优化操作,在高版本的JDK中默认是开启状态,但是在JDK1.6以下,这个操作是默认关闭的。这里笔者使用的是Java11版本,所以默认是开启的。
打破规则的操作?
在默认情况下,-XX:+OptimizeStringConcat配置项是开启的,而这个配置项所起到的作用就是对字符串拼接操作进行优化。但是有一种情况却无法被这个参数所识别。可以在上述代码中加入如下的一个操作。
@Benchmark
public String appendS(){
StringBuilder sb = new StringBuilder();
sb.append(a);
sb.append(b);
return sb.toString();
}
加入上述操作之后,我们再来查看三者的操作性能有差区别。
Benchmark Mode Cnt Score Error Units
TestString.appendS avgt 24.669 ns/op
TestString.appendStr avgt 13.802 ns/op
TestString.sttring avgt 13.909 ns/op
从最终结果来看,通过上面这种方式来进行字符串拼接操作,并没有用到JIT优化。这是因为在append()的底层用到的也是System.arraycopy()操作。
当然与StringBuilder相同的还有StringBuffer,与StringBuilder一样,StringBuffer也继承了AbstractStringBuilder类,唯一不同的是StringBuffer的append()方法上加上了synchronized关键字,也就是说它在并发场景下是线程安全的。但是在一般的情况下,很少出现并发去修改字符串的操作,所以一般很少用到StringBuffer。下面我们来比较一下StringBuffer和StringBuilder的性能。
StringBuffer和StringBuilder性能比较
编写如下的测试代码
@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class TestString {
String a = "Hello";
String b = "World!";
@Benchmark
public String appendStringBuilder(){
String str = new StringBuilder().append(a).append(b).toString();
return str;
}
@Benchmark
public String appendStringBuffer(){
String str = new StringBuffer().append(a).append(b).toString();
return str;
}
}
测试结果
Benchmark Mode Cnt Score Error Units
TestString.appendStringBuffer avgt 14.295 ns/op
TestString.appendStringBuilder avgt 14.326 ns/op
从测试结果来看,二者的性能也没有什么区别。这是因为在虚拟机进行逃逸分析的时候,可能会消除对StringBuffer上的锁操作。可以通过如下的参数来配置。
开启逃逸分析
-XX:+DoEscapeAnalysis
锁消除
-XX:+EliminateLocks
总结
从上面的分析,可以看到,在一般情况下程序都会得到JVM的优化来提升执行性能,但是并不能说明所有的代码都会得到优化,例如上面描述的StringBuilder的第二种情况。
对开发者来讲,并不能一直依赖于自动代码优化,所以在开发的时候要尽量注意对于字符串的拼接做操作上的优化,尤其对于一些涉及字符类型转化的场景中,可能会涉及到字符串的转码,以及字符串的类型转换等操作。所以在使用的时候,一定要注意。从逻辑的角度上来对有些代码操作进行优化。