一、String 的设计

String 是一个线程安全的类。我们可以看到在 String 中没有使用任何 “锁”,那么 String 是如何实现线程安全的?

1、String 被 final 修饰,这使得 String 无法被继承,并修改里面的方法

2、String 被设计成不可变对象

什么是不可变对象? 先来说说线程安全,之所以会存在线程安全的问题,是因为存在多个线程对同一个共享的资源对象进行“读写”操作,单纯只是读并不会有问题,问题出在“读写” 同时发生的情况。 而不可变对象的意思是,每一个线程操作的String 对象,对于当前线程来说都是唯一的,一旦有线程对某个 String 进行修改,那么生成会是一个新的 String 对象。

从代码的角度来看可能,来看几个简单的方法,concatreplace

  1. public final class String {
  2. public String concat(String str) {
  3. int otherLen = str.length();
  4. if (otherLen == 0) {
  5. return this;
  6. }
  7. int len = value.length;
  8. char buf[] = Arrays.copyOf(value, len + otherLen);
  9. str.getChars(buf, len);
  10. return new String(buf, true);
  11. }
  12. public String replace(char oldChar, char newChar) {
  13. if (oldChar != newChar) {
  14. ......
  15. return new String(buf, true);
  16. }
  17. return this;
  18. }
  19. }

这样对当前字符串进行了修改,必定会生成一个新的对象,而不会再此之上进行修改,也就是说对于一个 String 对象,从它创建之后,就不会再进行变更了,所以 初始化之后的String 是一个不可变的对象。

同样的,我们通过简单的代码测试一下,

  1. public static void main(String[] args) {
  2. String a = "a";
  3. System.out.println("a:" + a);
  4. String b = a.concat("b");
  5. System.out.println("b:" + b);
  6. String c = b.replace("a","c");
  7. System.out.println("c:" + c);
  8. // 对比对象
  9. System.out.println("a 和 b是否为同一个对象:" + a == b);
  10. System.out.println("a 和 c是否为同一个对象:" + a == c);
  11. System.out.println("b 和 c是否为同一个对象:" + b == c);
  12. }

二、String 对比 StringBuffer 和 StringBuilder

线程安全问题上

String 被设计成为不可变对象,且String 无法被继承,所以本身就是线程安全的。

StringBuffer 针对所有方法都在方法加上了 synchronized 锁,以当前实例对象为锁保证线程安全问题

StringBuilder 是线程不安全的,所有的append 操作直接在同一个 char[] 数组中进行,多线程并发操作会有线程安全问题。

字符串拼接执行效率对比

“+” 拼接的效率

  1. public class StringDemo {
  2. public static void main(String[] args) {
  3. String s2 = "";
  4. s2 += "abc";
  5. System.out.println(s2);
  6. }
  7. }

来看一下上述代码的字节码信息

01.png
02.png

所以如果简单对比 StringStringBuilder 能够知道,StringBuilder 的效率更高,因为 String 需要多生成一个对象,多一个对象的操作。

StringBuilderStringBuffer 字符串拼接对比

StringBuilderStringBuffer在实现上,几乎差不多,差别就在于StringBuffer` 是线程安全的,所有操作都需要获取 synchroinzed 锁,

所以 StringBuffer 整体上来说会比 StringBuilder 慢一点

总结

总的来说,效率从高到低:StringBuilder > StringBuffer > String

String 每次拼接都需要多生成一个新的对象,相比较于:StringBuilder 和 StringBuffer 来说效率都低。

而 StringBuffer 为了线程安全,加了 synchronized 同步锁,所以效率会比 StringBuilder 低一点。

不过总的来说,在无法确定线程安全问题情况下,直接使用 StringBuffer 是最安全,效率较高的选择。

三、String 的几道面试题

1、以下语句的输出结果

  1. String s1="abc";
  2. String s2="abc";
  3. System.out.println(s1==s2);
  4. System.out.println(s1.equals(s2));
  5. #### 结果
  6. true
  7. true

在 Java 中存在常量池,常量池中的数据只有一份。 虽然 s1 和 s2 指向的是同一个常量对象,但是他们仍然是线程安全,因为该值只有一份。 如果此时有操作对 s1 进行修改,那么 s1 所指向的对象也会随着变更,所以 s1 ,s2 仍然是不可变的对象。

  1. String s1 = "abc";
  2. String s2 = "abc";
  3. System.out.println("s1 == s2:" + ( s1 == s2 ));
  4. System.out.println("s1 equal s2:" + ( s1.equals(s2)) );
  5. // 对 s1 进行写操作
  6. System.out.println("=============================================");
  7. s1 = s1 + s1;
  8. System.out.println("s1 新值:" + s1);
  9. System.out.println("s1 == s2:" + ( s1 == s2 ));
  10. System.out.println("s1 equal s2:" + ( s1.equals(s2)) );
  11. ## 结果
  12. s1 == s2:true
  13. s1 equal s2:true
  14. =============================================
  15. s1 新值:abcabc
  16. s1 == s2:false
  17. s1 equal s2:false

2、 String s1=new String(“abc”)语句创建了几个对象?

  1. String s1 = new String("abc"); //创建了两个对象,一个在常量池中为“abc”,一个在堆内存中也就是s1
  2. String s2 = "abc"; // s2 同 s1 创建的常量池对象
  3. // 地址不一样,但是值一样,一个在常量池一个在堆中,所以有两个对象
  4. System.out.println("s1 == s2:" + ( s1 == s2 ));
  5. System.out.println("s1 equal s2:" + ( s1.equals(s2)) );
  6. ## 结果
  7. s1 == s2:false
  8. s1 equal s2:true

创建了两个对象,一个在常量池中,一个在堆中,所以应该避免使用 new String()

3、对比一下两个代码块

  1. public static void main(String[] args) {
  2. method1();
  3. method2();
  4. }
  5. public static void method1(){
  6. String s1 = new String("abc"); // 堆中
  7. String s2 = "abc"; // 常量池中
  8. // 地址不一样,但是值一样,一个在常量池一个在堆中,所以有两个对象
  9. System.out.println("s1 == s2:" + ( s1 == s2 ));
  10. System.out.println("s1 equal s2:" + ( s1.equals(s2)) );
  11. }
  12. public static void method2(){
  13. String s1 = "a" + "b" + "c"; // 常量池中
  14. String s2 = "abc"; // 常量池中
  15. // 编译过程中,由于常量化机制,s1成为了 "abc" 所以也在常量池中创建
  16. System.out.println("s1 == s2:" + ( s1 == s2 ));
  17. System.out.println("s1 equal s2:" + ( s1.equals(s2)) );
  18. }
  19. ## 结果
  20. s1 == s2:false
  21. s1 equal s2:true
  22. s1 == s2:true
  23. s1 equal s2:true