背景

String是Java中最常用的一种数据类型,它表示字符串。面试的时候很常见,也有很多坑,下面记录一些它的用法or坑。

原理

首先在源码中看看String是什么样的?

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
    
    // ...
}

可以看到,在JDK8中,String的底层是一个被final修饰的数组,它把写进去的串都存到value[]中,并且不可以修改它的引用。因此,它还有不可变性,这样做可以保证:

  • String 对象的安全性。假设 String 对象是可变的,那么 String 对象将可能被恶意修改。
  • 保证 hash 属性值不会频繁更,确保了唯一性,使得类似 HashMap 容器才能实现相应的 key-value 缓存功能。
  • 可以实现字符串常量池。在 Java 中,通常有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,如 String str=“abc”;另一种是字符串变量通过 new 形式的创建,如 String str = new String(“abc”)。

举个小例子

String s = "hello";
s = "world";

其实它是生成两个字符串对象,它们都存在常量池中,s首先指向hello所在的地址,然后再指向world所在的地址,而不是把s指向的字符串从hello改成了world。

内存分配

看看下面这三个串,结果是如何?

String str1= "abc";
String str2= new String("abc");
String str3= str2.intern();
assertSame(str1==str2);
assertSame(str2==str3);
assertSame(str1==str3);

结果是:

false
false
true

首先,使用类似于str1的方式创建字符串,会去字符串常量池中检查是否有该存,若有,则返回其地址;若无,则直接返回该串地址。

对于str2,在代码加载时,同样会像str1中,在常量池寻找或创建字符串。然后见到new,就肯定会在堆内存中创建一个字符串对象,它会让str2指向堆中对象,而堆中的对象又指向常量池中的字符串。而这里使用的是==作比较,比较的是引用地址,str1指向常量池,str2指向堆,这肯定不一样,所以是false。

在字符串常量中,默认会将对象放入常量池;在字符串变量中,对象是会创建在堆内存中,同时也会在常量池中创建一个字符串对象,复制到堆内存对象中,并返回堆内存对象引用。

对于str3,调用intern方法会在常量池中判断是否有这个字符串的对象,发现有,就返回常量池中的引用,所以其与1比较,为true。


下面再来一个关于intern的题目:

String a =new String("abc").intern();
String b = new String("abc").intern();
   
if(a==b) {
    System.out.print("a==b");
}

如果调用 intern 方法,会去查看字符串常量池中是否有等于该对象的字符串,如果没有,就在常量池中新增该对象,并返回该对象引用;如果有,就返回常量池中的字符串引用。堆内存中原有的对象由于没有引用指向它,将会通过垃圾回收器回收。

一开始创建a的时候,会先去常量池查找是否有abc,这里没有。然后在堆中创建一个字符串对象,后面调用intern时,查看是否有等于该字符串的对象,此时肯定有,因此a指向常量池中的abc。

创建b时,同理,会在堆中创建一个字符串对象;发现常量池中有abc,就不再创建。调用intern方法后,发现有这个串,就返回引用,最后作比较,为true。只是会发现,有两个堆内存中的对象没有被引用,他们将会被GC回收。

一些优化

刚学编程的时候,会被教导,不能这样写:String str= "ab" + "cd" + "ef";

首先会生成 ab 对象,再生成 abcd 对象,最后生成 abcdef 对象,这样是低效的。

但是编译器自动优化了这行代码,变成了:String str= "abcdef";

若是这样呢?

String str = "abcdef";
 
for(int i=0; i<1000; i++) {
      str = str + i;
}

这会首先1000个不同的对象啊,但它仍然替我们进行了优化,而且是使用了StringBuilder。

String str = "abcdef";
 
for(int i=0; i<1000; i++) {
	str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}

只能感叹它真智能,不过你仍然会发现,它每次都会创建一个新的StringBuilder对象,确实是会让系统更低效。因此,还是显示地声明比较好。