Java当中对数组初始化之后,该数组所占的内存空间、数组长度的都是不可变的,我们在编写程序的时候,对于数组的初始化都需要给定长度才行。对于创建一个字符串,为字符串对象分配内存空间,会耗费掉一定的时间与空间的代价,也即CPU和内存的代价消耗,作为最基础的数据类型,大量频繁的创建字符串,会极大程度地影响程序的性能。
每次连接字符串时都会创建一个新的String对象,随着拼接次数的增多,这个对象会越来越大。比如说,进行100次拼接需要创建100个String对象才能够达到目的,对于这点的详细说明请见我的前期博客https://blog.csdn.net/pf6668/article/details/108760427
接下来我们以代码的形式来具体比较一下String、StringBuffer和StringBuilder三者对于字符串拼接所体现出来的效率问题,代码如下:
public class Test {
public static void main(String[] args) {
StringTest(10000);
StringBufferTest(10000);
StringBuilderTest(10000);
}
public static void StringTest(int n){
String str = "";
Long startTime = System.currentTimeMillis();
for(int i=0;i<n;i++){
str += i;
}
Long endTime = System.currentTimeMillis();
System.out.println("String 连接"+ n +"次消耗"+(endTime-startTime)+"ms");
}
public static void StringBufferTest(int n){
StringBuffer str = new StringBuffer();
Long startTime = System.currentTimeMillis();
for(int i=0;i<n;i++){
str.append(i);
}
Long endTime = System.currentTimeMillis();
System.out.println("StringBuffer 连接"+ n +"次消耗"+(endTime-startTime)+"ms");
}
public static void StringBuilderTest(int n){
StringBuilder str = new StringBuilder();
Long startTime = System.currentTimeMillis();
for(int i=0;i<n;i++){
str.append(i);
}
Long endTime = System.currentTimeMillis();
System.out.println("StringBuilder 连接"+ n +"次消耗"+(endTime-startTime)+"ms");
}
}
运行结果如下:
我们可以清楚的看到String的字符串的连接效率是最低的,这一点对于大量字符串的拼接可以很明显的表示出来,所以说大量字符串的拼接最好不要选择String。StringBuffer和StringBuilder对于字符串的拼接效率是大致相同的。
另外需要指出来的一点就是,“+”对于字符串的拼接操作底层到底是怎么实现的呢?这个问题值得我们去探讨,我们再来看看另外一段代码:
public class StringTest {
public static void main(String[] args) {
String a = "abc";
String b = "def";
String c = a + b;
String d = "abc" + "def";
System.out.Println(c);
System.out.Println(d);
}
}
很明显输出的结果都是abcdef,那么直接用静态的字符串连接以及用字符串类型的变量进行连接有何不同呢?我们可以通过其编译的代码看到其中的不同。
1.如果是直接用静态字符串进行连接的话,JVM会认为字符串之间的连接“+”是没有用的,就会在编译阶段直接把这个“+”省略而把字符串连接起来,也就是说在编译期是有优化的,编译期生成的字节码已经是拼接之后的结果,这个时候性能是很高的。
2.如果使用变量进行连接的话,“+”拼接的底层是用StringBuilder来实现的,连接的过程就是StringBuilder使用append方法进行连接的过程,最后再调用toString()方法转为字符串。
对于String+的方式进行字符串的连接,每循环一次,就会重新new一个StringBuilder对象,这对内存的消耗是极大的,这也是进行“+”拼接字符串效率不高的最主要的原因。
String在java中是不可变长的,一旦初始化就不能修改长度,简单的字符串拼接其实是创建新的String对象,再把拼接后的内容赋值给新的对象,在频繁修改的情况下会频繁创建对象,而StringBuilder则不会,从头到尾只有一个实例对象,那StringBuilder是怎么实现的呢?
StringBuilder在进行append连接字符串的时候并不是用String存储,而是存放到一个名为value的char数组当中,字符串是固定长度的,而数组是可以扩容的,这样就不需要不停创建对象了。
从以上两张图片我们就可以看出来为什么String对字符串的操作不可变了。那StringBuilder中数组的初始长度是多少呢?扩容系数是多少呢?我们来看下面的源码图片:
数组默认的初始长度是16,扩容系数是value.length * 2 + 2,也即 (value.length << 1) + 2,而且只有当append之后的数据长度大于value.length时才会扩容一次,并不是每次连接都会进行扩容操作。
对于这个问题我们还是从JDK源码分析入手来看:
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
StringBuffer:线程安全;
StringBuilder:线程不安全。
因为StringBuffer的所有公开方法都是synchronized修饰的,而StringBuilder并没有synchronized修饰。
除了线程安全问题,StringBuilder和StringBuffer还有另外两点需要比较的地方:
1.缓冲区问题
//StringBuilder的toString()方法
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
//StringBuffer的toString()方法
/**
* A cache of the last value returned by toString. Cleared
* whenever the StringBuffer is modified.
*/
private transient char[] toStringCache;
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
StringBuffer每次获取toString都会直接使用缓存区的 toStringCache值来构造一个字符串。StringBuilde 则每次都需要复制一次字符数组,再构造一个字符串。所以说StringBuffer利用缓存区进行了部分优化操作。
StringBuffer中比StringBuilder多了一个toStringCache字段,字段上的解释是返回最后一次toString的缓存值,一旦StringBuffer被修改就清除这个缓存值。看到这里有的小伙伴可能就有疑问了,为什么StringBuilder不使用这样的一个字段呢,参考了其它博客我了解到,StringBuffer是线程安全的,并且其方法都使用了synchronized关键字,性能与StringBuilder相比当然大打折扣,线程安全的应用场景是多线程,那么访问的数据量肯定比单线程环境要大的多,所以说这个缓存机制可以平衡StringBuffer的性能。
2.性能问题
StringBuffer是线程安全的,它的所有公开方法都是同步的,而StringBuilder 是没有对方法加锁同步的,因此StringBuilder的性能要大于StringBuffer。
四、总结
1.String为固定长度的字符串,StringBuilder和StringBuffer为变长字符串;
2.stringBuffer是线程安全的,StringBuilder是非线程安全的;
3.StringBuffer和StringBuilder的默认初始容量是16,可以提前预估好字符串的长度,进一步减少扩容带来的额外开销。
以上内容均为个人学习的一点心得,其中的知识点若有错误请留言提醒,若有侵权内容提醒马上删除。