String:适用于少量的字符串操作
StringBuilder:适用于单线程下在字符串缓冲区进行大量操作
StringBuffer:适用于多线程下在字符串缓冲区进行大量操作
区别一:
String是final类不能被继承且为字符串常量,而StringBuilder和StringBuffer都是字符串变量。
字符串常量是不能改变的,但是字符串对象可以改变,改变的是其内存地址的指向。
StringBuffer和StringBuilder类型是字符串变量,适用append操作时,内存地址不发生改变。
public class StringTest {
public static void main(String[] args) {
String str = "aaa";
System.out.println("字符串常量str的初始地址:" + str.hashCode());
str = str + "666";
System.out.println("字符串常量str的修改后地址:" + str.hashCode());
System.out.println("*****************************************");
StringBuilder builder = new StringBuilder();
builder.append("xxxx");
System.out.println("字符串变量StringBuilder的初始地址:" + builder.hashCode());
builder.append("yyyyyy");
System.out.println("字符串变量StringBuilder的修改后地址:" + builder.hashCode());
System.out.println("*****************************************");
StringBuffer buffer = new StringBuffer();
buffer.append("cccc");
System.out.println("字符串变量StringBuffer的初始地址:" + buffer.hashCode());
buffer.append("ddddd");
System.out.println("字符串变量StringBuffer的修改后地址:" + buffer.hashCode());
}
}
输出结果:
区别二:
String可以通过append方法转换成StringBuffer和StringBuilder
StringBuffer和StringBuilder通过toString方法可以转换成String
public class StringBuilderTest {
public static void main(String[] args) {
String str = "aaaa";
StringBuilder stringBuilder = new StringBuilder(str);
StringBuffer stringBuffer = new StringBuffer(str);
//输出
System.out.println(str.getClass() + ":" + str);
System.out.println(stringBuilder.getClass() + ":" + stringBuilder);
System.out.println(stringBuffer.getClass() + ":" + stringBuffer);
System.out.println("***********************************************");
StringBuffer buffer = new StringBuffer("xxxx");
StringBuilder builder = new StringBuilder("yyyy");
String str1 = buffer.toString();
String str2 = builder.toString();
System.out.println(str1.getClass() + ":" + str1);
System.out.println(str2.getClass() + ":" + str2);
}
}
输出结果:
区别三:
线程安全性方面
StringBuilder非线程安全,用在单线程中,性能比StringBuffer要快。
StringBuffer线程安全,可用在多线程中,使用synchronized保证线程安全,执行速度慢。
区别四:
String用“+”作用数据连接符
StringBuffer和StringBuilder用append()连接数据
由于String是不可变的,导致每次对String的操作都会生成新的String对象,效率低,而且浪费大量的内存空间。
StringBuffer和StringBuilder都是可变的,对此进行多次修改,不会产生新的对象,有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量。当字符串大小超过容量时,会自动增加容量,不会产生新的对象。
我们看下String和StringBuffer的部分源码:
//String
public final class String
{
private final char value[];
public String(String original) {
// 把原字符串original切分成字符数组并赋给value[];
}
}
//StringBuffer
public final class StringBuffer extends AbstractStringBuilder
{
char value[]; //继承了父类AbstractStringBuilder中的value[]
public StringBuffer(String str) {
//继承父类的构造器,并创建一个大小为str.length()+16的value[]数组
super(str.length() + 16);
append(str); //将str切分成字符序列并加入到value[]中
}
}
很显然,String和StringBuffer中的value[]都用于存储字符序列,但是String是常量final数组,只能被赋值一次。
比如:new String("abc")其中value[]={'a','b','c'},之后这个String对象中的value[]再也不能改变了。这也正是大家常说的,String是不可变的原因 。
StringBuffer中的value[]是一个很普通的数组,而且可以通过append()方法将新字符串加入value[]末尾,这样也就改变了value[]的内容和大小。
总结:讨论String和StringBuffer可不可变,本质上指对象中的value[]字符数组可不可变,而不是对象引用可不可变。
字符串String “+”连接符的原理
public class StringTest1 {
public static void main(String[] args) {
//代码1
String s1 = "ab";
String s2 = "cd";
String str = s1 + s2 ;
String ss = "abcd";
System.out.println(str == ss);//false
//代码2
String st = "ab" + "cd";
String sd = "abcd";
System.out.println(st == sd);//true
}
}
原因解释:
代码1中局部变量sa、sb中存储的是堆中常量两个拘留字符串对象的地址,当执行s1+s2时,JVM首先会在堆中创建一个StringBuilder类,同时用s1指向的拘留字符串对象完成初始化,然后调用append方法完成对sb指向的拘留字符串的合并操作,接着调用StringBuilder对象的toString()方法在堆中创建一个String对象,最后将刚生成的String对象的堆地址存放在局部变量str中,而局部变量ss存放的是常量池中“abcd”所对应的拘留字符串对象的地址。所以str和ss的地址不一样。在这个过程中,实际上产生了五个字符串对象,三个拘留字符串对象、一个String对象、一个StringBuilder对象。
代码2中“ab” +“cd”会直接在编译器就合并成常量“abcd”,因此相同字面值常量“abcd”对应的是同一个对象,地址也会相同。
解释拘留字符串:
String s = "abcd",是一种非常特殊的形式,和new有本质的区别。它是Java中唯一不需要new就可以产生对象的途径。以String s = “abcd”形式赋值在Java中叫直接量,它是在常量池中而不是存放在压缩堆中。这样形式的字符串,在JVM内部发生字符串拘留,即当声明这样一个字符串后,JVM会在常量池中先查找有没有一个值为“abcd”的对象,如果有,就会把它赋值给当前引用。即原来那个引用和现在这个引用执向了同一个对象。如果没有,则在常量池中新创建一个“abcd”,即以这样形式声明的字符串,只要值相等,任何多个引用都指向同一对象。源代码中所有相同字面值的字符串常量只可能建立唯一一个拘留字符串对象。实际上JVM是通过一个记录了拘留字符串引用的内部数据结构来维持这一特性的。在Java中程序中,可以调用String的intern()方法来使得一个常规字符串对象成为拘留字符串对象。
String和StringBuffer累加的效率问题
先看下代码:
System.out.println("***********************************************");
//1、字符串常量 +
String ss = "";
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
str = "Hello" + "Word";
}
long end = System.currentTimeMillis();
System.out.println("字符串常量消耗时间:" + (end-start));
//2、字符串常量 + 字符串变量
String tt = "Hello";
String aa = "Word";
String bb = "";
long st = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
bb = tt + aa;
}
long dd = System.currentTimeMillis();
System.out.println("字符串变量消耗时间:" + (dd-st));
System.out.println("***********************************************");
//3、字符串对象 累加
String x = "hello";
String y = "";
long s = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
y = y + x;
}
long d = System.currentTimeMillis();
System.out.println("字符串对象累加消耗时间:" + (d-s));
//4、StringBuffer累加
String x1 = "hello";
StringBuffer y1 = new StringBuffer();
long p = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
y1.append(x1);
}
long e = System.currentTimeMillis();
System.out.println("StringBuffer累加消耗时间:" + (e-p));
输出结果:
原因解释:
代码1中“Hello” + “Word”在编译阶段就已经连接起来,形成了一个字符串常量,并指向堆中的拘留字符串对象,运行时只要将拘留字符串对象地址取出来1w次,存放在局部变量中,消耗时间很少
代码2中局部变量tt和局部变量aa存放在两个不同的拘留字符串对象中,然后通过三个步骤完成 + 连接
1、StringBuilder temp=new StringBuilder(tt),
2、temp.append(aa);
3、str=temp.toString();
虽然在开始和结束时用到了append()方法,但是中间过程中分别创建了StringBuilder和String对象,比较耗时间。
代码3原理同2
代码4 中 y1.append(x1),只需要将value[]数组不断扩大容量来存放,循环过程中无需创建任何新对象,效率较高。
总结:StringBuffer对象的append()效率要高于String对象的“+”连接操作
因为String类型是不可变的对象,每次对String类型进行改变的时候都等同于生成一个新的String对象,然后将指针执行新的String对象,每次生成对象都会对系统性能产生影响,当内存中无引用对象多,JVM的GC进行垃圾清除,耗费性能。
StringBuffer每次都会对对象本身进行操作,不会生成新的对象。
小结:
1、在编译阶段就能够确定的字符串常量,直接使用 + 连接效率更高
2、经常对字符串进行操作,单线程情况下,使用String Builder效率更高
3、多线程情况下,经常操作字符串使用String Buffer更安全,同时可以避免产生多个对象。
参考文档:
String、StringBuffer和StringBuilder的六大区别_Kaikai.的博客-CSDN博客
https://blog.csdn.net/itchuxuezhe_yang/article/details