引言
根据我在网上查到的资料显示,这三者的区别主要是:
String:字符串常量
StringBuffer:字符创变量(多线程)
StringBuilder:字符创变量(单线程)
对String的操作表面上看是对同一个变量的操作,但实际上是新建了一个常量,然后修改对象的引用。基于这样的机制,需要不停的GC旧的对象,其效率也很低下。
而StringBuffer与StringBuilder就不一样了,他们是字符串变量,是可改变的对象,每当我们用它们对字符串做操作时,实际上是在一个对象上操作的,当然速度快。
源码分析
上述是一般博客上都能查到的东西,但是我们还是要看一下源码的具体实现,比如,为啥一个是常量一个是变量。
成员变量
StringBuffer与StringBuilder类似,故只介绍string与StringBuilder的对比。
String:
/** 用来存放字符串的内容 */
private final char value[];
/** 缓存本字符串的hash值 */
private int hash; // 默认为0
/** 因为实现了Serializable接口,所以需要定义一个序列化ID */
private static final long serialVersionUID = -6849794470754667710L;
StringBuilder:
/** 同上,序列化ID */
static final long serialVersionUID = 4383685877147921099L;
因为StringBuilder继承的是AbstractStringBuilder,很多方法都是直接调用父类的方法,变量也是使用父类的变量。
AbstractStringBuilder:
/**用来存放字符串的内容.*/
char[] value;
/** 这个变量用来统计已经使用的字符数,字符数组不一定所有的空间都被使用.*/
int count;
对比很明显,String对象的字符数组直接被声明为final,一旦被赋值就不能再修改。所以String被称为常量。
构造函数
String:(过时的不介绍)
/**
创建一个空串,但是这个构造函数没有必要,因为数组不可变,一旦被定义,那就是一个空串,但这毫无意义。
*/
public String() {
this.value = "".value;
}
/**
以字符串为参数构造,把该字符串的hash值及内容赋值给新的字符串。
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
/**
把字符数组的内容赋值给新的字符串
*/
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
/**
把字符数组的一部分赋给新的字符串
*/
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
/**
将存放unicode编码的数组赋值到String中去
*/
public String(int[] codePoints, int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= codePoints.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > codePoints.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
final int end = offset + count;
// Pass 1: Compute precise size of char[]
int n = count;
for (int i = offset; i < end; i++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))
continue;
else if (Character.isValidCodePoint(c))
n++;
else throw new IllegalArgumentException(Integer.toString(c));
}
// Pass 2: Allocate and fill in char[]
final char[] v = new char[n];
for (int i = offset, j = 0; i < end; i++, j++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))
v[j] = (char)c;
else
Character.toSurrogates(c, v, j++);
}
this.value = v;
}
值得一提的是,几乎所有的构造函数注释都提到一句,使用String的构造函数毫无意义。其实是因为如果使用String s = “ABC”,那么s是存放在字符串常量池中的,如果池中已经有字符串“ABC”,就不会新建一个字符串对象,而是会使用原来的对象。
但是,如果你使用String s1 = new String(s),那么,就会再java堆中重新开辟一篇内存区域用来存放这个s对象。最明显的区别莫过于s1 == s 的结果是false,毕竟起始地址不同,而且后者会浪费资源。
StringBuilder:
/**
传入一个整数作为容量。
*/
public StringBuilder(int capacity) {
super(capacity);
}
/**
传入一个字符串作为参数,把字符串的长度+16作为容量
*/
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
/**
传入字符序列作为参数,把字符序列的长度+16作为容量,然后调用第一个构造函数
*/
public StringBuilder(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
以上都是调用父类的构造函数,而父类的构造函数只做了一件事情:
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
初始化一个长度为capacity的字符数组。
从构造函数来看,二者都是给字符数组赋值,只不过String一般是调用Arraycopy方法一步到位,而String Builder一般是先新建一个一定长度的数组,再调用append方法。
内容的修改
String
最常用的莫过于以下的情况:
String s1 = "PangYuQing";
String s2 = "Pang";
String s3 = "YuQing";
String s4 = "Pang" + "YuQing";
String s5 = s2 + s3;
String s5s = s5.intern();
String s6 = "Pang" + s3;
String s6s = s6.intern();
String s7 = new String("PangYuQing");
String s7s = s7.intern();
其实以上就两种情况,s4是一种,s5,s6是一种。s4经过jvm的优化,其实质就是String s4 = “PangYuQing”。后者,便转化为new StringBuilder.append(s2).append(s3)。然后再调用toString方法。因为需要额外生成一个String对象,速度自然变慢。
而s5s,s6s,s7s,官方API的解释是“当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串 (用equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。”
StringBuilder
一般使用append方法,以下选几个常用的介绍:
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
@Override
public StringBuilder append(char[] str) {
super.append(str);
return this;
}
可以看到,其都是调用父类的append方法。
而父类的方法是:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
//把str的内容拷贝到value里去
str.getChars(0, len, value, count);
count += len;
return this;
}
其中
/**
如果需要的容量大于当前数组的长度,则进行扩容
*/
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
/**
扩容,如果栈溢出则把容积置为Integer.MAX_VALUE
*/
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
值得注意的一点是,char[] value是父类私有的成员,所以StringBuilder中所有对内容的修改都是调用父类的方法类完成。
可以看到,在修改内容方面,String新建一个String对象或者往字符串常量池中添加常量,而StringBuiler是直接对原有的字符串数组扩容后添加字符。
小结
各位还是用StringBuilder吧。。。。
tips
发现一个有趣的事情
private AbstractStringBuilder appendNull() {
int c = count;
ensureCapacityInternal(c + 4);
final char[] value = this.value;
value[c++] = 'n';
value[c++] = 'u';
value[c++] = 'l';
value[c++] = 'l';
count = c;
return this;
}
原来这个方法是真往字符串后面加个null啊。。。