String的值是不可变的,每次对String的操作都会生成新的String对象,这样不仅效率低下,而且大量浪费有限的内存空间。
String初始值为"hello",然后再这个字符串后面加上新的字符串"world",这个过程需要重新在栈堆内存中开辟新的内存空间,最终得到"hello world",字符串相应的也需要开辟内存空间,这样两个字符串需要开辟三次内存空间,这是对内存空间的极大浪费。
String的底层数据结构是char类型数组。
//String类固定有一个char类型的数组。
private final char value[];
我们构造String字符串时,实际上就是在构造char数组,已经确定char[]数组不可在改变。string类没有所谓的add方法,所有的增删操作都是通过new新的string来实现。
// 初始化传入一个String类型参数的构造代码块。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
//传入一个char [] ,value[]copy这个char[].
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
//该构造函数根据入参StringBuffer对象中的字符内容来构建新的String对象。
public String(StringBuffer buffer) {
synchronized(buffer) { //synchronized:线程安全。
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
//一个特殊的保护类型的构造方法。
//String(char[] value)方法在创建String的时候会用到Arrays的copyOf方法
//将value中的内容逐一复制到String当中,
//而这个String(char[] value, boolean share)方法则是直接将value的引用赋值给String的value。
//那么也就是说,这个方法构造出来的String和参数传过来的char[] value共享同一个数组。
//share 只支持true. 只是为了和string(char [] value)这个函数有所区别。以便进行重载。
String(char[] value, boolean share) {//重点: 赋值的只是一个引用。
// assert share : "unshared not supported";
this.value = value;
}
这两个构造方法都是针对StringBuffer的操作,第一个是直接将StringBuffer传给String,因为此时StringBuffer可能被多个线程操作,String直接对数组进行copy可能会发生数据错乱。因此给其加了与StringBuffer类中一样的同步锁。
第二个构造方法主要是针对StringBuffer的toString()方法专门进行设计的,第二个参数实际上没有任何意义只是为了重载。需要注意的是,这里传进来的char数组并没有进行数据copy变成String私有的数据,而是直接进行引用拷贝,通过下文的StringBuffer.toString()方法研究。我们可以得知,在toString()方法调用后,StringBuffer中将不在保留这个数组的引用,因此,实际上也变成了String的私有数据。至于为什么要放在toString()里进行数据copy工作,也是为了线程安全,详解见下文。
改变char数组里面任何一个字符也需要new string。
//替换char数组里所有字符
//我们可以看出,只要进行修改,最终都是通过new string()来返回结果。
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
我们可以看出来,StringBuffer和StringBuilder有共同父类,通过源码我们可以得知,这两个实现类主要业务逻辑都是靠其父类AbstractStringBuilder实现的。因此我们对AbstractStringBuilder进行分析。
char[] value;
其底层数据结构和string一样,也是char类型数组,唯一不同的是,其是可以进行增删改操作的,并且还不需要new新的对象。
rivate int newCapacity(int minCapacity) {
// overflow-conscious code
//二倍加二的扩容方式
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
由源码可以看出,其增长方式为2n+2。
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
//二倍扩容扩容并复制数组
ensureCapacityInternal(count + len);
//将新加入的元素放入数组中
str.getChars(0, len, value, count);
count += len;
return this;
}
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;
}
public AbstractStringBuilder append(boolean b) {
if (b) {
ensureCapacityInternal(count + 4);
value[count++] = 't';
value[count++] = 'r';
value[count++] = 'u';
value[count++] = 'e';
} else {
ensureCapacityInternal(count + 5);
value[count++] = 'f';
value[count++] = 'a';
value[count++] = 'l';
value[count++] = 's';
value[count++] = 'e';
}
return this;
}
我们以append方式进行举例,每次对AbstractStringBuilder的操作,实际上是对char类型的value数组。在我们进行apped操作时,首先对检查value数组的剩余位置能否够新的数据写入,如若不能则先进行2n+2扩容在进行数据写入。
需要注意的是,null值和boolean类型值在char数组中的表现形式,而在String中,是不能传入这两种类型的。
从上文可以得出,其基本业务实现靠AbstractStringBuilder完成。
public StringBuilder() {
//初始化16
super(16);
}
public StringBuilder(int capacity) {
super(capacity);
}
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
public StringBuilder(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
我们需要注意的是,在初始创建StringBuilder时,无论是直接存放数据,还是不存放数据,StringBuilder必须保证在初次操作后char类型数组还有一定的空间可以用来进行append操作。为什么不直接调用其2n+2的扩容方式呢?对于我们日常大多数的业务逻辑来说,删和改是不需要扩容的,并且一般初始化以后,扩容都是很少的一部分,甚至有的转换成StringBuilder只是为了修改和删除。因此,我们直接进行二倍扩容在初始字符串很大的情况下,会很浪费空间。
从上文可以得出,其基本业务实现也是靠AbstractStringBuilder完成。
public StringBuffer() {
//初始容量16
super(16);
}
public StringBuffer(int capacity) {
super(capacity);
}
//多处16位的创建方式
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
public StringBuffer(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
其构造函数和其它增删改方法和StringBuffer是一致的。
//转换为字符串时的缓存
private transient char[] toStringCache;
public synchronized String toString() {
//得到转换成String的数组,String不需要在创建数组,而是直接使用toStringCache
//这样做的原因是,StringBuffer可以多线程操作,如果传过去让String进行copy会出现问题,
//因此StringBuffer就代为处理,并且此操作后,toStringCache的引用就会变为null
//因此只有String的char数组持有数据的引用,保证了数据安全。
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}