一、String 的设计
String 是一个线程安全的类。我们可以看到在 String 中没有使用任何 “锁”,那么 String 是如何实现线程安全的?
1、String 被 final 修饰,这使得 String 无法被继承,并修改里面的方法
2、String 被设计成不可变对象
什么是不可变对象? 先来说说线程安全,之所以会存在线程安全的问题,是因为存在多个线程对同一个共享的资源对象进行“读写”操作,单纯只是读并不会有问题,问题出在“读写” 同时发生的情况。 而不可变对象的意思是,每一个线程操作的String 对象,对于当前线程来说都是唯一的,一旦有线程对某个 String 进行修改,那么生成会是一个新的 String 对象。
从代码的角度来看可能,来看几个简单的方法,concat 和 replace
public final class String {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);}public String replace(char oldChar, char newChar) {if (oldChar != newChar) {......return new String(buf, true);}return this;}}
这样对当前字符串进行了修改,必定会生成一个新的对象,而不会再此之上进行修改,也就是说对于一个 String 对象,从它创建之后,就不会再进行变更了,所以 初始化之后的String 是一个不可变的对象。
同样的,我们通过简单的代码测试一下,
public static void main(String[] args) {String a = "a";System.out.println("a:" + a);String b = a.concat("b");System.out.println("b:" + b);String c = b.replace("a","c");System.out.println("c:" + c);// 对比对象System.out.println("a 和 b是否为同一个对象:" + a == b);System.out.println("a 和 c是否为同一个对象:" + a == c);System.out.println("b 和 c是否为同一个对象:" + b == c);}
二、String 对比 StringBuffer 和 StringBuilder
线程安全问题上
String 被设计成为不可变对象,且String 无法被继承,所以本身就是线程安全的。
StringBuffer 针对所有方法都在方法加上了 synchronized 锁,以当前实例对象为锁保证线程安全问题
StringBuilder 是线程不安全的,所有的append 操作直接在同一个 char[] 数组中进行,多线程并发操作会有线程安全问题。
字符串拼接执行效率对比
“+” 拼接的效率
public class StringDemo {public static void main(String[] args) {String s2 = "";s2 += "abc";System.out.println(s2);}}
来看一下上述代码的字节码信息


所以如果简单对比 String 和 StringBuilder 能够知道,StringBuilder 的效率更高,因为 String 需要多生成一个对象,多一个对象的操作。
StringBuilder 和 StringBuffer 字符串拼接对比
StringBuilder和StringBuffer在实现上,几乎差不多,差别就在于StringBuffer` 是线程安全的,所有操作都需要获取 synchroinzed 锁,
所以 StringBuffer 整体上来说会比 StringBuilder 慢一点
总结
总的来说,效率从高到低:StringBuilder > StringBuffer > String
String 每次拼接都需要多生成一个新的对象,相比较于:StringBuilder 和 StringBuffer 来说效率都低。
而 StringBuffer 为了线程安全,加了 synchronized 同步锁,所以效率会比 StringBuilder 低一点。
不过总的来说,在无法确定线程安全问题情况下,直接使用 StringBuffer 是最安全,效率较高的选择。
三、String 的几道面试题
1、以下语句的输出结果
String s1="abc";String s2="abc";System.out.println(s1==s2);System.out.println(s1.equals(s2));#### 结果truetrue
在 Java 中存在常量池,常量池中的数据只有一份。 虽然 s1 和 s2 指向的是同一个常量对象,但是他们仍然是线程安全,因为该值只有一份。 如果此时有操作对 s1 进行修改,那么 s1 所指向的对象也会随着变更,所以 s1 ,s2 仍然是不可变的对象。
String s1 = "abc";String s2 = "abc";System.out.println("s1 == s2:" + ( s1 == s2 ));System.out.println("s1 equal s2:" + ( s1.equals(s2)) );// 对 s1 进行写操作System.out.println("=============================================");s1 = s1 + s1;System.out.println("s1 新值:" + s1);System.out.println("s1 == s2:" + ( s1 == s2 ));System.out.println("s1 equal s2:" + ( s1.equals(s2)) );## 结果s1 == s2:trues1 equal s2:true=============================================s1 新值:abcabcs1 == s2:falses1 equal s2:false
2、 String s1=new String(“abc”)语句创建了几个对象?
String s1 = new String("abc"); //创建了两个对象,一个在常量池中为“abc”,一个在堆内存中也就是s1String s2 = "abc"; // s2 同 s1 创建的常量池对象// 地址不一样,但是值一样,一个在常量池一个在堆中,所以有两个对象System.out.println("s1 == s2:" + ( s1 == s2 ));System.out.println("s1 equal s2:" + ( s1.equals(s2)) );## 结果s1 == s2:falses1 equal s2:true
创建了两个对象,一个在常量池中,一个在堆中,所以应该避免使用 new String()
3、对比一下两个代码块
public static void main(String[] args) {method1();method2();}public static void method1(){String s1 = new String("abc"); // 堆中String s2 = "abc"; // 常量池中// 地址不一样,但是值一样,一个在常量池一个在堆中,所以有两个对象System.out.println("s1 == s2:" + ( s1 == s2 ));System.out.println("s1 equal s2:" + ( s1.equals(s2)) );}public static void method2(){String s1 = "a" + "b" + "c"; // 常量池中String s2 = "abc"; // 常量池中// 编译过程中,由于常量化机制,s1成为了 "abc" 所以也在常量池中创建System.out.println("s1 == s2:" + ( s1 == s2 ));System.out.println("s1 equal s2:" + ( s1.equals(s2)) );}## 结果s1 == s2:falses1 equal s2:trues1 == s2:trues1 equal s2:true
