String是使用final修饰的类,让我们来看下他的源码:
从这里可以看出为什么String是一个不允许变的了。
通常我们会使用一下代码来不断地改变String:
String str = "a";
str+="b";
str+="c";
System.out.println(str);
得到的结果就是abc
给人的一个错觉好像str是可以改变的,但是从其源码的final修饰的char value[]可以看出,String从来都是不变的。
那么上面的结果是如何出来的呢?
在回答这个问题前我不经想要问,上面的那段代码创建了几个类?
通常人会觉得是一个类,其实不然,是三个类。
String str = “a”;创建了一个类
str+=“c”;此时不放将其装换为
str = str+“b”;
此时可以知道str指向的值是“ab”,那我想问一下,之前的那个a呢?
要知道常量是存在常量池中的,在ab没有出来之前常量池中并没有ab,所以只能将其创建出来。
所以上面的行为可以表现为:
str从指向a字符串,转换到了指向ab字符串。
同理str+=“c”,也是str从指向ab字符串转换到了指向abc字符串。
所以现在的常量池中应该有的字符串为:
a
ab
abc
所以简单的一个操作为底层的jvm带来的却是比较的的操作。
所以如果是大量需要拼接字符串的话,那么对于jvm来说是一场灾难。
所以如果存在大量的拼接操作跟推荐使用StringBuilder。
我们来看一下StringBuilder的源码:
从这里似乎看不出什么,只是知道stringbuilder是一个final的类。
我们来看一下他的append的方法:
从这里知道了,其实他是将数据交给了其父类AbstractStringBuilder去处理。
让我们到其父类里看一看:
其父类的append方法:
public AbstractStringBuilder append(String str) {
//如果是null的换,那么就直接添加null
if (str == null)
return appendNull();
//计算str的长度
int len = str.length();
//如果加上str的长度超过了当前的char数组的长度,扩容
ensureCapacityInternal(count + len);
//将具体的str填充到value数组中去
str.getChars(0, len, value, count);
//更新当前有具体值的长度
count += len;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
// 如果当前的需求量超过了value的长度,那么扩容
//这个minimumCapacity就是上面的count + len
//count是value中原本的有数据的长度
//有数据是指,原本vlaue的长度十位10
//但是只有前三个空有数据,所以有数据长度就是3
//len是后加的str的长度
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
void expandCapacity(int minimumCapacity) {
//将自身长度*2+2,就是新的数组的长度
int newCapacity = value.length * 2 + 2;
//如果当前的扩容后的大小还是小于所需要的长度
//那么就设置当前的长度为所需要的长度
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
//如果小于0,报错
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
//执行赋值
value = Arrays.copyOf(value, newCapacity);
}
public static char[] copyOf(char[] original, int newLength) {
//创建一个新的char类型的数组
char[] copy = new char[newLength];
//使用native方法来实现数据的赋值
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
//src,需要复制的数据,就上面的而言,就是原始的数据
//srcPos,从哪里开始复制,就上面的来说是0
//dest,复制的内容存在哪里,也就是将src的数据复制到dst中,dst就是之前创建的新的char数组
//dstPos,从新的数组的那个地方开始赋值,就上面来说是从0开始
//length,也就是src的长度。
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
//如果需求长度没有超过value的长度,那么直接进行赋值
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
//这个就是之前说过的函数,使用native
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
所以从上面就可以知道SringBuilder是对于char数组的操作,而且这个char数组不是final修饰的,是可变的。并且char数组的扩容算法就是:
newcapacity = old*2+2;
如果newcapacity<needlength 那么newcapacity = needlength
所以对于StringBuilder来说添加多个string类型的值,并不会创建多个String对象,始终都是正对于父类的vlaue的操作。
对于StringBudder来说他的添加的机制和StringBuffer是一样的,唯一不同的地方在于StringBuffer的append的方法上多了个synchronized:
并且眼睛比较细的同学可以发现,StringBuffer的父类不就是AbstractStringBuilder吗?
所以StringBuffer和StringBuilder都是AbstractStringBuilder的子类。
从上面的源码解析中我们也可以学到如果需要使用将不同的字符串或是其他的容器进行合并时可以使用System.arraycopy这个函数。
扩容也是可以借用上面的思想。