本章记录关于String类型的一些常见问题

String StringBuffer StringBuilder 的区别是什么?String 为什么是不可变的?

可变性

  • String 不可变

String底层是用字符串数组实现的,String 类中使⽤ final关键字修饰字符数组来保存字符串, private final char value[] ,所以 String 对象是不可变的。

在 Java 9 之后,String 类的实现改⽤ byte 数组存储字符串

  • ⽽ StringBuilder 与 StringBuffer 都继承⾃ AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使⽤字符数组保存字符串 char[] value 但是没有⽤ final 关键字修饰,所以这两种对象都是可变的。

线程安全性性能

String 中的对象是不可变的,也就可以理解为常量,线程安全。所谓的更改字符串,都会⽣成⼀个新的 String 对象,然后将指针指向新的 String对象。大量数据时不宜使用。

StringBulider 与 StringBuffer 就用法上没有本质区别,但是 StringBuffer 增加了线程安全的机制,效率相对于StringBuilder略低。

对于三者使⽤的总结:

  1. 操作少量的数据: 适⽤ String
  2. 单线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuilder
  3. 多线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuffer

StringTable(字符串常量池/串池)

更多关于StringTable相关内存的问题 请见我的 “JVM笔记-JVM内存结构” 这篇文章 ,里面关于方法去的相关内容

经典面试问题

String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();


System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);

要不要优化?要不要创建?创建几个,创建了什么?在哪里创建?

  • 1、 上述前两行代码都是采用 惰加载的方式创建的(基本上所有java对象都是惰加载的方式),只有执行到这样代码时,才会讲字符串对应的字符串对象放入”串池“(才算真正创建这个对象);同时注意,只有”串池“中没有这个数据才会向其添加,有的话只会将引用指向它

  • 2、String s3 = "a" + "b"; 编译器进行优化 直接优化为 String s3 = "ab" 所以 System.out.println(s3 == s5)结果为true;

  • 3、String s4 = s1 + s2; 首先创建一个`StringBulider对象 在调用 append方法 把 s1 和 s2 拼起来 变成StringBuilder 类型的”ab“,

    然后toString 转成String 等价于 new String(“ab”) 这个对象不会放在常量池,而会放在堆内存

  • 所以System.out.println(s3 == s4); 结果为false 因为一个在常量池一个在堆内存肯定地址不一样

  • 4、String x2 = new String("c") + new String("d");一共创建了几个“字符串对象”?

    • “c”、”d” 作为常量被放入了串池中 (两个对象)
    • new String(“c”) 、new String(“d”) 放入了堆中 (两个对象)
    • new String(“c”) + new String(“d”) 调用 StringBulider对象拼接 并返回字符串类型 “cd” 注意这个字符串对象不会放进串池,只有字符串常量才会放入串池,拼接的只会放进堆里(一个对象,字符串对象)
    • 共计五个字符串对象
  • 5、System.out.println(s3 == s6);结果为ture;String s6 = s4.intern(); 会试图s4放入常量池中,但是发现常量池中已经有了,所以返回的是常量值中的地址。

关于 intern的版本问题

String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
  • 关于 intern 方法,会将尝试这个字符串对象放入串池,如果串池不会放入,没有就会放入,这个例子中,有就不放入串池,就不放入串池了,所以System.out.println(x1 == x2);结果为false,因为x2虽然调用了intern 但是返回值才是串池;

  • 如果调换了【最后两行代码】的位置呢

    • 为true ,可以理解为串池不是一个物理概念,而是一个逻辑概念;x2就地变为串池的内容(实际上是在常量池中记录此字符串的引用,并返回该引用。)

      x2 = x2.intern(); 就地变串池内容(或者说,是串池主动添加的他,他自己的地址不用变)

    • 如果是jdk1.6呢(如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回) 优惠常见一个对象

      • jdk1.6 串池像是一个物理概念一样,放入串池指的是,在串池中新建这个对象

        x2!=x2.intern(); x2.intern() 的返回地址是重新在串池中复制的字符串的地址。

8 种基本类型的包装类和常量池

Java 基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean;

前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean 直接返回True Or False。如果超出对应范围仍然会去创建新的对象。

如:

Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22);// 输出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false

注意:

  1. Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。

  2. Integer i1 = new Integer(40);这种情况下会创建新的对象。

Integer 比较更丰富的一个例子

Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println("i1=i2 " + (i1 == i2));
System.out.println("i1=i2+i3 " + (i1 == i2 + i3));
System.out.println("i1=i4 " + (i1 == i4));
System.out.println("i4=i5 " + (i4 == i5));
System.out.println("i4=i5+i6 " + (i4 == i5 + i6));
System.out.println("40=i5+i6 " + (40 == i5 + i6));


// 输出
i1=i2 true
i1=i2+i3 true
i1=i4 false
i4=i5 false
i4=i5+i6 true
40=i5+i6 true

解释:

语句 i4 == i5 + i6,因为+这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较