Java中的字符串拼接方式
今天,我们要深入探讨为什么不建议在循环中使用+号进行字符串拼接。为了使你更清楚地理解其工作原理及效率差异,我们将引入实际的代码示例、对源码进行分析和对各种方式的效率进行对比。我们将分析String、StringBuilder、StringBuffer以及String.concat()的工作原理,并且通过实际的效率测试来对比它们的性能。
字符串String的特性
在Java中,String是一个常见的类,它是一种不可变的类。一旦创建,无法更改它。当我们在原有字符串上添加内容时,Java会创建一个新的String对象,旧对象被抛弃。
String str1 = "Hello";
String str2 = "World";
String str = str1 + str2; // 创建新的String对象
Java源码中的String类是这么实现的:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
// The value is used for character storage.
private final char value[];
// ...
}
可以看到,String类被声明为final,即不可被继承,且value[]数组也是final的,一旦被赋值,就不能更改。
字符串拼接的方式
- 使用加号(+)
在Java中,使用加号+连接两个字符串,会产生一个新的字符串对象。这是因为String对象是不可变的,所以每次连接都会生成新的String对象。
- 使用String.concat()方法
使用concat()方法也是创建新的String对象。然而,与字符串加法不同,concat()方法只有在处理长度相对较短的字符串时才比加号拼接更有效。
String str1 = "Hello";
String str2 = "World";
String str = str1.concat(str2); // 使用concat方法进行字符串拼接
concat()方法的实现如下:
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
可以看到,concat()方法会创建一个新的字符串对象,这可能会导致额外的内存消耗。
- 使用StringBuilder
StringBuilder类是一种可变字符序列。相比于String,StringBuilder在进行大量字符串拼接时可以大大提高性能,因为它可以在原有字符序列的基础上进行修改,避免了不必要的对象创建。
StringBuilder sb = new StringBuilder("Hello");
sb.append("World"); // 使用StringBuilder进行字符串拼接
String str = sb.toString(); // 将StringBuilder对象转换为String对象
实际上,StringBuilder内部维护了一个char数组,并且可以根据需要动态扩容。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;
}
- 使用StringBuffer
StringBuffer类与StringBuilder类似,它们都是可变字符序列。不过,StringBuffer是线程安全的,因此在多线程环境下,使用StringBuffer进行字符串拼接是安全的。
StringBuffer sb = new StringBuffer("Hello");
sb.append("World"); // 使用StringBuffer进行字符串拼接
String str = sb.toString(); // 将StringBuffer对象转换为String对象
实际上,StringBuffer类的大部分方法(包括append()方法)都使用了synchronized关键字进行同步,因此在多线程环境下是安全的,但也造成了性能开销。
效率对比
我们可以通过一个简单的测试来比较各种方式的效率。测试代码如下:
long startTime;
long endTime;
String testStr = "";
// 加号连接
startTime = System.nanoTime();
for (int i = 0; i < 10000; i++) {
testStr += "test";
}
endTime = System.nanoTime();
// 使用+的耗时:130704167ns
System.out.println("使用+的耗时:" + (endTime - startTime) + "ns");
// 使用concat连接
testStr = "";
startTime = System.nanoTime();
for (int i = 0; i < 10000; i++) {
testStr = testStr.concat("test");
}
endTime = System.nanoTime();
// 使用concat的耗时:52840017ns
System.out.println("使用concat的耗时:" + (endTime - startTime) + "ns");
// 使用StringBuilder连接
StringBuilder sb = new StringBuilder("");
startTime = System.nanoTime();
for (int i = 0; i < 10000; i++) {
sb.append("test");
}
endTime = System.nanoTime();
// 使用StringBuilder的耗时:4944645ns
System.out.println("使用StringBuilder的耗时:" + (endTime - startTime) + "ns");
// 使用StringBuffer连接
StringBuffer sbf = new StringBuffer("");
startTime = System.nanoTime();
for (int i = 0; i < 10000; i++) {
sbf.append("test");
}
endTime = System.nanoTime();
// 使用StringBuffer的耗时:8976758ns
System.out.println("使用StringBuffer的耗时:" + (endTime - startTime) + "ns");
运行上述代码,可以发现StringBuilder的性能最优,其次是StringBuffer,而加号和concat方式性能相对较差。
结语
在实际编程中,为了提高效率,我们应该考虑使用合适的字符串拼接方式。对于短字符串或者拼接操作并不频繁的情况,使用加号或concat方法即可。然而,如果你需要拼接大量的字符串,尤其是在循环中进行拼接,那么选择StringBuilder或StringBuffer就会更加合理。它们可以在原有字符序列的基础上进行修改,从而避免了不必要的对象创建和内存消耗。同时,在多线程环境下,使用StringBuffer可以保证线程安全。因此,选择合适的字符串拼接方式不仅可以使代码运行得更快,还能提高代码的质量和可维护性。
福利
关注公众号 小白技术圈 回复关键词 C02 可以领取图片中书籍