String、StringBuffer和StringBuilder都是Java中用来表示字符串的。在Java中,String类是一个不可变类,任何对String的改变都会引发新的String对象的生成。StringBuffer和StringBuilder则都是可变类,任何对他所指代的字符串都不会产生新的对象。StringBuilder是Java5后提出来的,与StringBuffer相比,StringBuilder有更高的执行效率,但其不是线程安全的。
1. 性能对比下面对String,StringBuffer和StringBuilder进行append *** 作的性能对比。主要比对代码如下:
public static void compareAddTime(String string,StringBuilder stringBuilder,StringBuffer stringBuffer){
long preTime,afterTime;
preTime = System.currentTimeMillis();
for(int i = 0;i < 100000; ++i){
string = string + 'a';
}
afterTime = System.currentTimeMillis();
System.out.println("String操作10000遍需要时长为:" + (afterTime - preTime) + "ms");
preTime = System.currentTimeMillis();
for(int i = 0;i < 10000000; ++i){
stringBuffer.append('a');
}
afterTime = System.currentTimeMillis();
System.out.println("StringBuffer操作1000000000遍需要时长为:" + (afterTime - preTime) + "ms");
preTime = System.currentTimeMillis();
for(int i = 0;i < 10000000; ++i){
stringBuilder.append('a');
}
afterTime = System.currentTimeMillis();
System.out.println("StringBuilder操作1000000000遍需要时长为:" + (afterTime - preTime) + "ms");
}
执行以上方法,得出如下结果图:
由于String每次" + “ *** 作都要new新对象,因此仅” + "10000次,否则容易内存溢出。我们可以看见,String具有最低的性能效率,在相同数量级下StringBuilder *** 作效率要高于StringBuilder,且当其数量级越高,差距越明显。因此在实际中我们应该尽量少用String对字符串进行 *** 作,不仅效率低下,还容易导致频繁垃圾回收,影响代码效率。
1.1 String " + " *** 作具体实现为了更直观查看String是如何new新对象的,我对String进行了以下探索:
public static void main(String[] args){
String string = new String();
string = string + "abc";
}
针对如上简单的" + " *** 作,使用javac指令对其进行编译,并使用javap -c指令对其进行反编译。得到如下结果
Code:
0: new #2 // class java/lang/String
3: dup
4: invokespecial #3 // Method java/lang/String."":()V
7: astore_1
8: new #4 // class java/lang/StringBuilder
11: dup
12: invokespecial #5 // Method java/lang/StringBuilder."":()V
15: aload_1
16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: ldc #7 // String abc
21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore_1
28: return
我们可以看到,当进行" + " *** 作时,Java会先new一个新的StringBuilder对象,并对其进行append *** 作,最后返回toString()字符串。反应到Java代码,即为如下过程:
new StringBuilder().append(string).append("abc").toString()
(Tips:当String并没有 *** 作String对象,而是 *** 作两个独立字符串时(即:string = “abc” + “def”),JVM对其进行性能优化,此时无需new StringBuilder用于拼接)
1.2 StringBuffer和StringBuilder类append *** 作对比针对StringBuffer和StringBuilder,我们可以查看他们的实现方式。
//StringBuffer的append方法
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
//StringBuilder的append方法
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
如上所示,在StringBuffer中,使用了synchronized保证其线程安全性。此外在StringBuffer中有一个toStringCache数组,用以缓存StringBuffer对象toString后结果。当StringBuffer发生修改了字符串的 *** 作时,需要把缓存清除。
StringBuffer和StringBuilder均继承了AbstractStringBuilder类,因此他们的append方法均一致。查看其append实现方法,其实现代码如下所示:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
//判断是否有足够空间存储str,若不足,触发扩容
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
可以看到在append其,需要判断类对象是否有充足空间,是否需要扩容。其基本逻辑与ArratList和LinkedList类似(想了解更多关于ArrayList和LinkedList,可以看Java基础(一)-ArrayList和LinkedList性能对比与原因探索)。他们均有一个最大值MAX_ARRAY_SIZE(即Integer.MAX_VALUE - 8,为何要减8在ArrayList中阐述,这里不在解释),当容量不足时进行扩容:
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
//若扩容后容量大于MAX_ARRAY_SIZE或者int已溢出,则把预留的 - 8位也用于数组,此时可能导致内存溢出
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
可以看到其扩容机制为原有长度 * 2 + 2(这里 + 2应该是避免0 * 2仍是0导致的无法扩容情况),若仍不足,扩容到需要的最小长度minCapacity。若扩容后容量大于MAX_ARRAY_SIZE或者int已溢出,则把预留的 - 8位也用于数组,此时可能导致内存溢出。
2. 线程安全性对比我们可以看到,在StringBuffer中用synchronized保证了线程安全,在StringBuilder中则没有。且String每次都会生成新对象。这里编写代码测试他们的线程安全性:
AppendThread类:
用于对StringBuffer,StringBuilder,String进行append *** 作的线程类。
package com.thread.demo.string;
public class AppendThread extends Thread{
private StringBuffer stringBuffer;
private StringBuilder stringBuilder;
private String string;
public AppendThread(StringBuffer stringBuffer,StringBuilder stringBuilder,String string){
this.stringBuffer = stringBuffer;
this.stringBuilder = stringBuilder;
this.string = string;
}
@Override
public void run(){
for(int i = 0;i < 1000; ++i){
stringBuffer.append("a");
stringBuilder.append("a");
string = string + "a";
}
System.out.println("StringBuffer Size:" + stringBuffer.length() + " | " + "StringBuilder Size:" + stringBuilder.length() + " | " + "String Size:" + string.length());
}
}
Main方法:
new十个新线程,同步append。
package com.thread.demo.string;
public class stringCompare {
public static void main(String[] args){
StringBuffer stringBuffer = new StringBuffer();
StringBuilder stringBuilder = new StringBuilder();
String string = new String();
//测试运行时间
//compareAddTime(string,stringBuilder,stringBuffer);
//string = string + "abc";
//验证线程安全性
for(int i = 0;i < 10; ++i){
new AppendThread(stringBuffer,stringBuilder,string).start();
}
}
/**
* @Description 查看String,StringBuilder和StringBuffer操作时长
* @author Sc_Cloud
* @param * @param string
* @param stringBuilder
* @param stringBuffer
* @return void
* @date 2022/4/8 19:44
*/
public static void compareAddTime(String string,StringBuilder stringBuilder,StringBuffer stringBuffer){
long preTime,afterTime;
preTime = System.currentTimeMillis();
for(int i = 0;i < 100000; ++i){
string = string + 'a';
}
afterTime = System.currentTimeMillis();
System.out.println("String操作10000遍需要时长为:" + (afterTime - preTime) + "ms");
preTime = System.currentTimeMillis();
for(int i = 0;i < 10000000; ++i){
stringBuffer.append('a');
}
afterTime = System.currentTimeMillis();
System.out.println("StringBuffer操作1000000000遍需要时长为:" + (afterTime - preTime) + "ms");
preTime = System.currentTimeMillis();
for(int i = 0;i < 10000000; ++i){
stringBuilder.append('a');
}
afterTime = System.currentTimeMillis();
System.out.println("StringBuilder操作1000000000遍需要时长为:" + (afterTime - preTime) + "ms");
}
}
正常情况下十个线程append1000次,长度应该为10000。实际执行效果如下:
可以看到StringBuffer最大值为10000,StringBuilder最大值小于10000,String由于每次需要new新对象,因此长度均为单个线程append长度1000,符合预期。
总结因此,对String、StringBuffer和StringBuilder,我们一般有如下结论:
- String在对字符串进行 *** 作时会生成新的对象;
- StringBuffer和StringBuilder会 *** 作原有对象,减少新生成对象;
- StringBuffer是线程安全的,效率低;
- StringBuilder不是线程安全的,但是执行效率高;
由于StringBuilder相较于StringBuffer有速度优势,因此多数情况下建议使用StringBuilder
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)