区别总结

String是常量,不可变的。StringBuffer和StringBuilder都是可变的。使用StringBuilder做字符串拼接能够节省开销。

可变与不可变的原理

String是用final关键字修饰的字符数组来保存字符串,private final char[] value,所以是不可变的。
StringBuffer和StringBuilder都是继承自AbstractStringBuilder,也是用字符数组来保存字符串,但是没有用final来修饰,因此是可变的。
深入理解String不可变性:String的不可变,实际上是因为其value用final来修饰了,private final char value[],不可变的是value这个引用的地址,而数组内容其实是可变的,只是SUN的工程师没有暴露任何修改或者破坏数组内容的方法,从而保证了String的不可变。

  1. final int[] value={1,2,3}
  2. int[] another={4,5,6};
  3. value=another; //编译器报错,final不可变,value用final修饰,编译器不允许我把value指向堆区另一个地址。
  4. final int[] value={1,2,3}; //但如果我直接对数组元素动手,分分钟搞定。
  5. value[2]=100; //这时候数组里已经是{1,2,100},
  6. // 所以String是不可变,关键是因为SUN公司的工程师。
  7. // 在后面所有String的方法里很小心的没有去动Array里的元素,没有暴露内部成员字段。
  8. // private final char value[]这一句里,private的私有访问权限的作用都比final大。
  9. // 而且设计师还很小心地把整个String设成final禁止继承,避免被其他人继承后破坏。
  10. // 所以String是不可变的关键都在底层的实现,而不是一个final。考验的是工程师构造数据类型,封装数据的功力。

线程安全性

String是不可变的,也就可以理解为常量,当然也就是线程安全的。
StingBuilder和StringBuffer的父类都是AbstractStringBuilder,AbstractStringBuilder中定义了一些字符串的基本操作和公共方法,StringBuffer在父类的基础上,对这些方法加上了同步锁或者对调用的方法加上了同步锁,保证了线程安全。
StringBuilder并没有对方法加同步锁,所以不是线程安全的。

性能上

每次对于String类型进行改变的时候,都是重新生成新的String对象,然后将指针指向新的String对象,同时老的对象被GC掉,所以在频繁操作改变字符串时,String的性能最差。
StringBuffer和StringBuilder在改变时,都是对自身进行操作,StringBuilder比StringBuffer性能要好,因为其没有加同步锁的处理,但是性能差异不大(网上一般说是20%左右)。
综上:变更少量时使用String;单线程操作字符串缓冲区并大量变更时,用StringBuilder;多线程操作字符串缓冲区并大量变更时,用StringBuffer。

相关面试题

为什么String要设计成不可变的?

答:主要考录到效率和安全。
效率:1)在早期的JVM实现中,被final关键字修饰的方法会被转为内嵌调用以提升执行效率,不过从Java SE5开始就放弃了这种方式了,现在不用考虑加final来提升执行效率。2)String中缓存的hashcode,正因为String不可变,所以hashcode不变,因为hashcode不变所以才能缓存,不必每次重新计算。
安全:1)字符串池是方法区中的一部分特殊存储,当一个字符串被创建时,首先会s去字符串池中查找,如果找到就直接返回该字符串的引用。例如:String str1=“abcd; String str2=“abcd”;这两个字符串实际上都指向的同一个字符串池中的对象,如果字符串时可变的话,那对其中的一个进行修改,就会影响到另一个。String是常用的参数类型,因此如果可以改变,可能会导致严重安全问题。
image.png

讲一下String.intern()的作用

答:返回一个字符串,字符串的内容与原字符串相同,但是取自于字符串常量池。如果字符串常量池中无当前字符串,则将字符串拷贝到常量池中(JDK7之前拷贝的是字符串对象,JDK7和之后拷贝的是该字符串的地址!),并返回常量池中该字符串的引用。

Strings1=newString(“ab”);//s1指向堆中的字符串对象,同时在常量池创建了字符串对象“ab”
s1.intern();//执行时发现常量池已经有与s1相等(equal)的字符串,不做操作。
Strings2=”ab”;//s2指向常量池中的字符串对象
System.out.println(s1==s2);//s1和s2指向的地址不同,所以false
Strings3=newString(“c”)+newString(“d”);//s3指向堆中的字符串,同时在常量池中创建了“c”,“d”,但是常量池中没有“cd”!!!
s3.intern();//执行时发现常量池不存在与s3相equal的字符串或者地址,所以将s3的地址放入常量池
Strings4=”cd”;//因为常量池中存在的s3的地址与“cd”相equal,所以s4指向常量池中s3的地址,
System.out.println(s3==s4);//s3指向地址和s4最终指向地址相同,所以true。

讲一下直接用String str=“xxx”来实例化字符串和用String str=new String(“xxx”)来实例化的区别

答:String str=“xxx”这种方式会首先去字符串常量池中找是否存在这个字符串,有的话直接指向常量池中的该字符串,没有的话就先向常量池中添加该字符串,然后再直接指向常量池中的该字符串。
String str1 = “hello”;
String str2 = “hello”;
String str3 = “world”;
image.png
String str=new String(“xxx”)这种方式会首先在堆中创建一个该字符串对象,然后再去常量池中检查是否存在相同内容的字符串,有的话就不做操作,没有的话就直接常量池添加一个。
image.png
“执行上面那三行代码创建了几个对象?”,堆中3个常量池中2个,总共是5个。
String s0=”kvill”;
String s1=”kvill”;
String s2=”kv” + “ill”;
System.out.println( s0==s1 );// true
System.out.println( s0==s2 );// true
String s0=”kvill”;
String s1=new String(”kvill”);
String s2=”kv” + new String(“ill”);
System.out.println( s0==s1 );// false
System.out.println( s0==s2 );// false
System.out.println( s1==s2 );// false
String s0= “kvill”;
String s1=new String(”kvill”);
String s2=new String(“kvill”);
System.out.println( s0==s1 );// false
System.out.println( “**” );
s1.intern();
s2=s2.intern(); //把常量池中“kvill”的引用赋给s2
System.out.println( s0==s1);// false
System.out.println( s0==s1.intern() );// true
System.out.println( s0==s2 );// true
String s1=new String(“kvill”);
String s2=s1.intern();
System.out.println( s1==s1.intern() );// false
System.out.println( s1+” “+s2 );
System.out.println( s2==s1.intern() );// true