Fork me on GitHub

String、StringBuffer、StringBuilder

String

在java中使用非常频繁,String类提供了构造管理字符串的各种方法。

1
2
3
4
5
6
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final int count;
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields =new ObjectStreamField[0];
.....
}

可以看到,String被声明为final class,而且所有属性也是final的,所以它是不可变的,类似拼接、裁剪字符串等操作都会产生新的String对象,但是final特性保证了String是线程安全的,因此有时候在多线程中我们可以利用final属性保证变量的安全(只读),优化同步和上锁消耗的时间;

在底层实现中,String利用了char数组(基于1.8),我们知道有一块字符串常量池(固定大小的hash表)专门来存放字符串的来提高效率,for example:

1
2
3
String s1="111";//直接赋值,放入常量池
String s2=new String("111");//通过new
//s1==s2为false

详细来说就是:在创建字符串时,如果是直接赋值,则先检查池中是否有相同的字符串对象,有则直接返回池中该对象的引用,无则新建字符串对象,返回新建的字符串对象的引用,并且将新创建的对象放入池中;
注:有人说,如果是new的方式,则不检查常量池存不存在,直接在堆或者栈中创建一个对象,也不会把字符串放入池中。来,我们验证一哈:

1
2
3
String s2 = new String("StringTest");
String s1 = new StringBuilder().append("String").append("Test").toString();
System.out.println(s1.intern() == s1);//false(JDK 8)

所以上面的结论可想而知是错的,至于原因可以继续往下看。

详细可见


String提供了intern()方法,调用该方法时,如果常量池中包括了一个等于此String对象的字符串(由equals方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并且返回此池中对象的引用。
即:

1
2
String s3=s2.intern();
//s3==s1 为true

注意:此方法的机制在1.7发生了变化,在1.6的时候,字符串常量池存储于永久代中(也就是方法区),很难被GC照顾,所以容易OOM,但1.7及以后,字符串常量池被放到了堆中,没有被引用的字符串是可以被回收的。
看看下面这段代码:

1
2
String s1 = new StringBuilder().append("String").append("Test").toString();
System.out.println(s1.intern() == s1);//1.7以上:true 以下:false

1)如果常量池不存在该字符串,则直接在常量池中添加该字符串对象的引用(注意放的是引用),也就是s1,所以为true;但是如果在1.6的时候是直接在常量池中创建”StringTest”对象并返回它的引用,所以结果为false。
2)如果常量池已经存在该字符串”StringTest”,不符合“首次出现”原则,则直接返回该字符串的引用,所以结果也为false,因为一个是堆上对象的引用,一个是常量池中的引用。这也是上面的结论的原因。

所以intern返回的都是池中的东西。


//下面这个问题de原因,还在研究中……目前能测试出来的就是append方式并不会将拼接后的StringTest放入常量池,但是会将每一个分项(比如说Test)放入常量池,有梦想的童鞋可以自行验证。

1
2
3
4
5
6
7
8
9
10
11
String s1=new String("StringTest");
System.out.println(s1.intern()==s1);//false(JDK 8)
String s1 = new StringBuilder().append("String").append("Test").toString();
System.out.println(s1.intern() == s1);//true(JDK 8)
String s1 = new StringBuilder("StringTest").toString();
System.out.println(s1.intern() == s1);//false(JDK 8)
String s1 = new StringBuilder().append("StringTest").toString();
System.out.println(s1.intern() == s1);//false(JDK 8)


拓展:String的hashcode值:

1
2
3
4
5
6
7
8
9
10
11
12
private int hash; // Default to 0
public int hashCode() {
int h = hash;
final int len = length();
if (h == 0 && len > 0) {
for (int i = 0; i < len; i++) {
h = 31 * h + charAt(i);
}
hash = h;
}
return h;
}

这是String属性中唯一一个不是final的(原因??目前还未找到);
可以看到String的hash值可以抽成一个算法:假如n=字符串的长度:
h=char[0]31^(n-1) + char131^(n-2)+·······+char[n-1];
这里的因子31,首先因为它是素数,这样可以尽量减少hash冲突,但是为什么不是其他的素数,有说避免乘法过大,防止溢出;有说31的乘法可以由i*31== (i<<5)-1来表示,用位运算和减法替代乘法,现在很多虚拟机里面都有做相关优化,能更好的分配hash地址。我更倾向后者。


String.valueOf和Integer.toString的区别:
1).valueOf()方法有各种不同的重载方法,比如String.valueOf(Object obj)方法最终调用的是obj.toString(),如果对象为空则返回”null”;而String.valueOf(int i)则会直接调用Integer.toString(i)方法。
2)Integer.toString()方法直接返回一个字符串对象。


String.substring(int var1, int var2)在1.6和1.7上的不同:
(1.6)String类包含三个成员变量:char value[], int offset,int count。他们分别用来存储真正的字符数组,数组的第一个位置索引以及字符串中包含的字符个数。 1.6的substring会创建一个新的string对象,但是这个string的值仍然指向堆中的同一个字符数组,这两个对象中只有count和offset 的值是不同的。所以容易使这个字符数组被大量引用,造成内存泄漏。
(1.7)substring方法会在堆内存中创建一个新的数组,避免重复引用。


StringBuffer StringBuilder

字符串变量,StringBuffer是为解决字符串拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用StringBuilder。

1
2
3
4
5
6
7
8
9
10
11
12
13
//StringBuffer
@Override
public synchronized StringBuffer append(char c) {
toStringCache = null;
super.append(c);
return this;
}
//StringBuilder
@Override
public StringBuilder append(char c) {
super.append(c);
return this;
}

可以看到两者的源码并没有多大区别,只是stringbuffer利用了synchronized保证了线程安全。
StringBuffer和StringBuilder都是继承自AbstractStringBuilder的final类,底层利用可修改的char数组。这个数组初始大小默认为16。
在字符串拼接上,java在编译时做了优化,for example:

1
2
3
String s1 = "aa" + "bb" + "cc" + "dd";
String s2 = new StringBuilder().append("aa").append("bb").append("cc").append("dd").toString();
System.out.println(s1==s2);//false

在jvm中,String str = “aa” + “bb” + “cc” + “dd”;其实就是String str = “aabbccdd”;