String对象一旦创建之后就是不可变的,不可变的!
问题:既然String对象是不可变的,那么其包含的常用修改值的方法是如何实现的呢?
Demo
substring(int,int) 字符串截取 split(String,int) 字符串分割 toLowerCase() 字符串所有字母小写
其实,这些方法底层都是通过重新创建一个String对象来接收变动后的字符串,而最初的String字符串对象并未发生改变。
注意点(重要)
我们经常使用String字符串进行+或者+=操作,来改变字符串的值,这种情况比较特殊!
首先在JAVA中+和+=是仅有的两个重载过的操作符,在进行字符串相加的时候其String对象并未发生改变,而是值发生了变化!
案例1:
String str="hello"+"world";
使用javap命令对其命令进行反编译后得到如下代码:
str=new StringBuffer("hello").append("world").toString()
结论:从上面的案例得出,String字符串的+的操作,底层其实是通过StringBuffer执行的!
从效率角度出发,在大部分情况下,使用+连接字符串并不会造成效率上的损失,而且还可以提高程序的易读性和简洁度。
案例2:
String str="0";
String append="1";
for(int i=0;i<10000;i++){ //结果为01111...
str+=append;
}
这种情况下,如果进行反编译的话,得到的是如下代码:
...
for(int i = 0; i < 10000; i++){// 结果为:str = 011111.......(很多1)
str = (new StringBuilder()).append(str).append(append).toString();
}
这种情况下,由于大量StringBuilder创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个StringBuilder对象调用append()方法手动拼接!
案例3:
除了上面的两种情况,我们再来分析一种特殊情况,即:
当+两端都是字符串常量(此时指的是“xxx”而不是final修饰的String对象)的时候,在编译过后会直接拼接好,例如:
System.out.println("hello"+"wordld");
反编译后变为:
System.out.println("hellowordld");
这种情况下通过+拼接字符串效率是最佳的!
1.2 StringBuilderStringBuilder是一个可变的字符串类,可以把它看作是一个容器。
- StringBuilder可以通过toString()方法转换成String
- String可以通过StringBuilder的构造方法,转换成StringBuilder
StringBuilder拼接字符串的效率较高,但是它不是线程安全的!
String str=new StringBuilder().toString(); StringBuilder stringBuilder=new StringBuilder(new String());1.3 StringBuffer
StringBuilder是一个可变的字符串类,可以把它看作是一个容器。
- StringBuffer可以通过toString()方法转换成String
- String可以通过StringBuffer的构造方法,转换成StringBuffer
String str=new StringBuffer().toString(); StringBuffer stringBuffer=new StringBuffer(new String());
StringBuffer拼接字符串的效率相对于StringBuilder较低,但是它是线程安全的!
2、String/StringBuffer/StringBuilder源码 2.1、String源码分析 2.1.1、String 类public final class String
implements java.io.Serializable, Comparable, CharSequence {
...
}
- 首先从代码可以看出,String类被final关键字修饰,该类不能被继承!
- String还实现了Serializable、Comparable、CharSequence等接口,能够被序列化,支持字符串判断等比较,且是一个char值的可读序列!
- Comparable接口有compareto(String s)方法,CharSequence接口有length,charAt(int index),subSequence(int start,int end)方法.
//不可变的char数组用来存放字符串,说明其是不可变量 private final char value[]; //int型的变量hash用来存放计算后的哈希值 private int hash; // 默认为0 //自己指定序列化serialVersionUID的值,防止反序列之前对象被改变了不能序列化成功 private static final long serialVersionUID = -6849794470754667710L;2.1.3 String类的构造函数
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);
}
这里,需要注意一点,怎么去理解String对象是不可变的?
什么是不可变对象?
众所周知,在JAVA中,String类是不可变的。那么到底什么是不可变对象呢?可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。
首先区分对象和对象的引用
String s="aaa"; System.out.println(s); s="123456"; System.out.println(s);
测试结果为:
aaa 123456
首先创建一个String对象s,然后让s的值为aaa,然后又让s的值为123456,从打印结果来看,s的值确实是改变了。那么怎么还说String对象是不可变的呢?其实这里存在一个误区:s只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占用的空间就越大。引用只是一个4字节的数据,里面存放了它所有指向的对象的地址,通过这个地址可以访问对象。也就是说,s只是一个引用,它指向了一个具体的对象,当s=123456;这句代码执行过之后,又创建了一个新的对象123456,而引用s重新指向了这个新的对象,原来的对象aaa还在内存中存在,并没有改变。内存结构如下图所示:
String不可变是value这个数组引用不可变,但是String对象引用是可以改变的。String创建的对象存在 堆中,每次会在栈中存储对象的引用。每次修改String内容后,原有内容不会删除,系统重新开辟新数组,然后把新的内容指向这个引用。
2.1.4 String类的常用方法简单方法
public int length() {
return value.length;
}
public boolean isEmpty() {
return value.length == 0;
}
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
// 得到字节数组
public byte[] getBytes() {
return StringCoding.encode(value, 0, value.length);
}
重点方法
equals(Object anObject)
public boolean equals(Object anObject) {
//如果引用的是同一个对象,则返回true
if (this == anObject) {
return true;
}
//如果不是String类型的数据,返回false
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
//如果与char数组的长度不相等 返回false
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0; //用以记录循环比较的时候每个字符的下标
//从后往前单个字符判断,如果有不相等,则返回false
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
该方法判等规则:
- 如果二者内存地址相同,则为true(说明是同一个对象)
- 如果对象类型不是String类型,则为false,如果是就继续判等
- 如果对象字符长度不相等,则为false,如果相等就继续判等
- 从后往前,判断String类char数组value中的每个字符是否相等,有不相等的则为false。如果一直相等则直到最后一个数,则返回true
结论:根据判等规则得出,如果两个超长的字符串进行比较,是非常费时间的!
hashCode()方法
public int hashCode() {
int h = hash;
//如果hash没有被计算过,并且字符串不为空 则进行hashCode计算
if (h == 0 && value.length > 0) {
char val[] = value;
//计算过程:
//val[0]*31^(n-1) + val[1]*31^(n-2) + ... + val[n-1]
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
String类重写了hashCode方法,Object中的hashCode方法是一个Native方法调用。Striong类的hash采用多项式计算得来。我们完全可以通过不相同的字符串得出同样的hash,所以两个String对象的hashCode相同,并不代表两个String是一样的。
那么计算hash值得时候为什么要使用31作为基数呢?
先要明白为什么需要HashCode,每个对象根据值计算HashCode,这个code大小虽然不奢求必须唯一(因为这样通常计算会非常慢),但是要尽可能地不能重复,因此基数要尽量的大。另外,31N可以被编译器优化为左移5位后减1即31N=(N<<5)-1,有较高的性能。使用31的原因可能是为了更好的分配hash地址,并且31只占用 5bits
结论:
- 基数要用质数:质数的特性(只有1和自己是因子)能够使得它和其他数相乘后得到的结果比其他方式更容易产生唯一性,也就是hash code值得冲突得概率最小。
- 选择31是观测哈希值分布结果后得一个选择,不清楚原因,但的确有利(更分散,减少冲突)
compareTo(String another):
public int compareTo(String anotherString) {
//自身字符串长度len1
int len1 = value.length;
//被比较对象字符串长度len2
int len2 = anotherString.value.length;
//取出两耳个字符串长度中的最小值lim
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
//从value的第一个字符开始到最小长度lim处为止,如果字符不相等,
//返回自身(对象不相等处字符-被比较对象不相等处字符)
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
//如果前面都相等,则返回(自身长度-被比较对象的长度)
return len1 - len2;
}
这个方法写的很巧妙,先从0开始判断字符大小,如果两个对象能比较字符的地方比较完了还相等,就直接返回自身长度减去被比较对象长度,如果两个字符串长度相等,则返回0,巧妙地判断了三种情况。
startsWith(String prefix,int offset)
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
int to = toffset;
char pa[] = prefix.value;
int po = 0;
int pc = prefix.value.length;
//如果起始地址小于0或者(起始地址+所比较对象的长度)大于自身对象长度,返回false
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
//从所比较对象的末尾开始比较
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
//startsWith重载方法1
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
//startsWith重载方法2
public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}
起始比较和末尾比较都是比较经常用得到的方法,例如在判断一个字符串是不是http协议的,或者初步判断一个文件是不是mp3文件,都可以采用这个方法进行比较。
concat(String str)
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
//注意 这里是新new一个String对象返回,而并非原来的String对象
return new String(buf, true);
}
//位于java.util.Arrays类中
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
//调用底层c++方法实现
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
replace(char oldChar,char newChar)
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value;
//找到旧值最开始出现的位置
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++;
}
//注意 这里是new一个String对象返回,而不是原来的String对象
return new String(buf, true);
}
}
return this;
}
trim()
public String trim() {
int len = value.length;
int st = 0;
char[] val = value;
//找到字符串前段没有空格的位置
while ((st < len) && (val[st] <= ' ')) {
st++;
}
//找到字符串末尾没有空格的位置
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
//如果前后都没有出现空格,则返回字符串本身
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
毫无疑问,trim()用首尾指针的方式删除了开头和结尾处的u0000 ~ u0020的所有字符,并且返回了一个新的子串;如果字符串的首尾部分没有u0000 ~ u0020的字符,那么则返回原字符串对象。
案例分析
public class Test02 {
public static void main(String[] args) {
String a="a"+"b"+1;
String b="ab1";
//ab1是放在常量池(Constant Pool,准确来说是放在串池中的)中的
//所以,a,b虽然都等于ab1,但是内存中只有一份副本,所以==的结果才为true
System.out.println("a的哈希值:"+a.hashCode());
System.out.println("b的哈希值:"+b.hashCode());
//identityHashCode方法都会返回Object类默认hashCode()方法会返回的值
System.out.println("a的地址:" +System.identityHashCode(a));
System.out.println("b的地址:" +System.identityHashCode(b));
System.out.println(a==b);
System.out.println("==================================");
String a1=new String("ab1");
String b1="ab1";
//new方法决定了Strong "ab1"被创建放在了内存区heap区(堆上),被a1所指向
//b1位于串池中,因此==返回的false
System.out.println("a1的哈希值:"+a1.hashCode());
System.out.println("b1的哈希值:"+b1.hashCode());
System.out.println("a1的地址:" +System.identityHashCode(a1));
System.out.println("b1的地址:" +System.identityHashCode(b1));
System.out.println(a1==b1);
}
}
输出结果如下:
a的哈希值:96304 b的哈希值:96304 a的地址:856419764 b的地址:856419764 true ================================== a1的哈希值:96304 b1的哈希值:96304 a1的地址:621009875 b1的地址:856419764 false2.2、StringBuilder源码分析 2.2.1、StringBuilder类
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
...
}
首先,StringBuilder同String一样被final关键字修饰该类不能被继承!
从继承体系可以看出,StringBuilder继承了AbstractStringBuilder类,该类中包含了可变字符串的相关操作方法:append()、insert()、delete()、replace()、charAt()等等。StringBuffer和StringBuilder均继承该类
比如在StringBuilder类的append(String str)方法(后文还会讲到):
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
//其成员属性value[]数组扩容,类似于ArrayList的扩容
//扩容算法:int newCapacity = (value.length << 1) + 2;
//注意容量有上限:MAX_ARRAY_SIZE
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
StringBuilder还实现了Serializable接口和CharSequence接口,说明该类的对象可以被序列化,并附带了堆字符序列进行只读访问的方法,比如:length()、charAt()、subSequence()、toString()方法等。
2.2.2 StringBuilder类的属性//用于字符存储的char数组,value是一个动态的数组,当存储容量不足的时候,会对它进行扩容 char[] value; //表示value数组中已经存储的字符数 int count;
关键点:
- 该数组和String中的value数组不同,其未被final关键字修饰,其值是可以被修改的!
- 这两个成员属性并不位于java.lang.StringBuilder中,而位于其父类java.lang.AbstractStringBuilder中,同样的StringBuffer中的这两个属性也是位于该父类中!
Stringuilder类提供了4个构造方法。构造方法主要完成了对value数组的初始化。
//位于父类的StringBuilder中
//空参StringBuilder默认char数组容量为16
public StringBuilder() {
super(16);
}
//由参数传入容量,构造StringBuilder对象
public StringBuilder(int capacity) {
super(capacity);
}
//根据字符串参数的长度,构建容量为16+字符串长度的StringBuilder
public StringBuilder(String str) {
super(str.length() + 16);
//调用父类的append方法添加字符串str
append(str);
}
//接收一个CharSequence对象作为参数,设置了value数组的初始容量为CharSequence对象的长度+16
//并把CharSequence对象中的字符添加到value数组中
public StringBuilder(CharSequence seq) {
this(seq.length() + 16);
//调用父类的append方法添加字符串seq
append(seq);
}
//位于父类的AbstractStringBuilder中
//创建指定容量的AbstractSytringBuilder
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
总结:
其实String、StringBuilder、StringBuffer也类似于容器,因为其底层均在维护一个char类型的数组!
2.2.4 StringBuilder 类中的方法append(Object obj)方法
append方法的重载方法参数有多种,比如String int等等,原理类似,这里只举例String和Boolean为例子:
//位于父类StringBuilder中
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
//java.lang.StringBuilder类中
@Override
public StringBuilder append(String str) {
//调用父类AbStractStringBuilder的append(String str)方
super.append(str);
return this;
}
//位于父类StringBuilder中
@Override
public StringBuilder append(boolean b) {
super.append(b);
return this;
}
//位于父类AbstractStringBuilder中
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
//其成员属性value[]数组扩容,类似于ArrayList的扩容
//扩容算法:int newCapacity = (value.length << 1) + 2;
//注意容量有上限:MAX_ARRAY_SIZE
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//位于父类bstractStringBuilder中
//添加Boolean类型
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;
}
//扩容的方法
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
//如果给定的长度大于原char数组的长度,
//则开始复制原数组中的数据到扩容后的数组中,newCapacity(int)是真正执行扩容的方法
//创建了一个新的指定长度的char数组来赋值给value引用
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
//真正执行扩容的方法
private int newCapacity(int minCapacity) {
// overflow-conscious code
//扩容的方式是原char数组的长度左移一位并加上2并与要扩容的长度进行比较
//注意:扩容是有范围限制的 不能超过MAX_ARRAY_SIZE
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
//当newCapacity(新扩容的长度)小于0或者大于MAX_ARRAY_SIZE的时候,长度都默认为MAX_ARRAY_SIZE
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
append()方法讲指定参数类型的字符串表示形式追加到字符串序列的末尾。
StringBuilder类提供了一系列的append()方法,它可以接受boolean、char、char[]、CharSequence、double、float、int、long、Object、String、StringBuffer这些类型的参数。
这些方法最终都是调用父类AbstractStringBuilder类中对应的方法。最后,append()方法返回了StringBuilder对象自身,以便用户可以链式调用StringBuilder类中的方法。
AbstractStringBuilder类的各个append()都大同小异。append()方法在追加字符到value数组中之前都会调用ensureCapacityInternal()方法来确保value数组有足够的容量,然后才把字符追加到value数组中。
ensureCapacityInternal()方法判断value数组的容量是否足够,如果不够,则使用newCapacity()进行扩容
newCapacity()方法默认情况下将数组容量扩大到原来数组容量的2倍+2,数组的容量最大只能扩容到Integer.MAX_VALUE。
最后,通过Arrays的Arrays.copyOf(value,
newCapacity(minimumCapacity));来创建一个新的数组接受原来char数组中的数据并把其赋值给原来的引用对象,
delete()方法
@Override
public StringBuilder delete(int start, int end) {
//调用父类AbstractStringBuilder类中的方法
super.delete(start, end);
return this;
}
//位于父类AbstractStringBuilder中 前闭后开
public AbstractStringBuilder delete(int start, int end) {
if (start < 0)
throw new StringIndexOutOfBoundsException(start);
if (end > count)
//如果结束下标大于value数组中存在元素的个数,则默认为元素个数
end = count;
if (start > end)
throw new StringIndexOutOfBoundsException();
int len = end - start;
if (len > 0) {
//调用底层c++方法
System.arraycopy(value, start+len, value, start, count-end);
count -= len;
}
return this;
}
delete()方法删除指定位置的字符。删除的字符从指定的start位置开始,直到end-1位置。delet()方法也调用了父类AbstractStringBuilder类中对应的方法。
delet() 方法首先检查参数的合法性。当end大于value数组中已经存储的字符数count的时候,end取count的值。最后,需要删除的字符数大于等于1的时候,调用System类的arrayCopy()静态方法进行数组拷贝完成删除字符的操作,并更新count的值。
replace()方法
//位于父类StringBuilder类中
@Override
public StringBuilder replace(int start, int end, String str) {
super.replace(start, end, str);
return this;
}
//父类中真正执行replac替换的方法
public AbstractStringBuilder replace(int start, int end, String str) {
//先检查参数的有效性
if (start < 0)
throw new StringIndexOutOfBoundsException(start);
if (start > count)
throw new StringIndexOutOfBoundsException("start > length()");
if (start > end)
throw new StringIndexOutOfBoundsException("start > end");
if (end > count)
//如果结束下标大于value数组中存在元素的个数,则默认为元素个数
end = count;
int len = str.length();
int newCount = count + len - (end - start);
//再进行真正的替换操作之前要先进行扩容
//当newCount>value.length的时候,则会进行扩容操作,扩容为原来数组长度的2倍+2,并把
//原来数组中的内容复制到扩容后的数组中,并把新数组赋值给原来的引用对象
ensureCapacityInternal(newCount);
System.arraycopy(value, end, value, start + len, count - end);
str.getChars(value, start);
count = newCount;
return this;
}
//将str的值放入到原数组中的指定位置
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
replace()方法将指定位置的字符串替换成字符串中的字符,替换的字符从指定的start位置开始,直到end-1位置结束。
replace()方法也调用了父类AbstractStringBuilder类中对应的方法。
replace() 方法首先检查参数的合法性。当end大于value数组中已存储的字符数count的时候,end取count的值。然后调用ensureCapacityInternal(int)方法确保value数组有足够的容量。接着调用System类的arraycopy()静态方法进行数组拷贝,主要作用是从start位置开始空出替换的字符串长度len大小的位置。最后,调用String类的getChars(char[],int begin)方法将替换的字符串中的字符拷贝到value数组中。这样就完成了字符串的替换操作了。
toString()方法
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
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);
}
public static char[] copyOfRange(char[] original, int from, int to) {
int newLength = to - from;
if (newLength < 0)
throw new IllegalArgumentException(from + " > " + to);
char[] copy = new char[newLength];
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
总结:
- StringBuilder类使用了一个char数组来存储字符,该数组是一个动态的数组,当存储容量的容量不太的时候
- StringBuilder对象是一个可变的字符序列
- StringBuilder类是非线程安全的
2.3.1 StringBuffer类
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
从继承体系中得出,StringBuffer和StringBuilder继承了相同父类,实现了相同的接口,且都被final关键字修饰不可被继承,所以不再重复赘述。
2.3.2 StringBuffer类的属性
//用于字符存储的char数组,value是一个动态的数组,当存储容量不足的时候,会对它进行扩容 char[] value; //表示value数组中已经存储的字符数 int count;
StringBuffer和StringBuilder以及String一样本质上都维护了一个字符数组!且,StringBuffer和StringBuilder的字符数组没有final修饰可以倍重新赋值,而String中的字符数组加了final不可变(常量)!
2.3.3 StringBuffer类的构造函数
public StringBuffer() {
super(16);
}
public StringBuffer(int capacity) {
super(capacity);
}
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
在构造方法上StringBuffer和StringBuilder没什么区别,不再重复赘述!
2.3.4 StringBuffer的方法
StringBuffer的主要操作对象有append、insert等,这些操作都是在value上进行的,而不是像String一样每次操作都要new一个新的String对象,因此,StringBuffer在效率上要高于String。有了append、insert等操作,value的大小就会改变,那么StringBuffer是如何操作容量的改变的呢?
StringBuffer的扩容
StringBuffer有个继承自AbstractStringBuilder类的ensureCapacity的方法:
//位于StringBuffer类中
@Override
public synchronized void ensureCapacity(int minimumCapacity) {
super.ensureCapacity(minimumCapacity);
}
//位于父类中
public void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > 0)
ensureCapacityInternal(minimumCapacity);
}
StringBuffer 对其进行了重写,直接调用父类的ensureCapacity方法,这个方法用来保证value的长度大于给定的参数minimumCapacity,在父类的ensureCapacityInternal方法中这样操作:
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
private 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;
}
private int hugeCapacity(int minCapacity) {
//这个方法是为了防止newCapacity溢出,因为最终扩容的结果都是max(newCapacity,minCapacity)
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
最后得到的新value数组大小是max(value.length*2+2,minCapacity),上面代码的第二个判断是为了防止newCapacity溢出。
setLength()方法
该方法用于直接设置字符数组元素的count数量:
//位于StringBuffer中
@Override
public synchronized void setLength(int newLength) {
toStringCache = null;
super.setLength(newLength);
}
//在父类中
public void setLength(int newLength) {
if (newLength < 0)
throw new StringIndexOutOfBoundsException(newLength);
ensureCapacityInternal(newLength);
if (count < newLength) {
Arrays.fill(value, count, newLength, ' ');
}
count = newLength;
}
从代码中可以看出,如果newLength大于count,那么就会在后面添加’ ’补充,如果小于count,就直接使 count=newLength。
append/insert方法
StringBuffer中的每一个append和insert函数都会调用父类的函数。
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
//其成员属性value[]数组扩容,类似于ArrayList的扩容
//扩容算法:int newCapacity = (value.length << 1) + 2;
//注意容量有上限:MAX_ARRAY_SIZE
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
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);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
@Override
public synchronized StringBuffer insert(int index, char[] str, int offset,
int len)
{
toStringCache = null;
super.insert(index, str, offset, len);
return this;
}
public AbstractStringBuilder insert(int index, char[] str, int offset,
int len)
{
if ((index < 0) || (index > length()))
throw new StringIndexOutOfBoundsException(index);
if ((offset < 0) || (len < 0) || (offset > str.length - len))
throw new StringIndexOutOfBoundsException(
"offset " + offset + ", len " + len + ", str.length "
+ str.length);
ensureCapacityInternal(count + len);
System.arraycopy(value, index, value, index + len, count - index);
System.arraycopy(str, offset, value, index, len);
count += len;
return this;
}
toString方法
StringBuffer可以通过toString方法转换为String,它有一个私有的字段toStringCache!
这个字段表示是一个缓存字段,用来保存上一次调用toString的结果,如果value的字符串序列发生改变,就会将它清空
//该字段不可被序列化
private transient char[] toStringCache;
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
首先判断toStringCache 是否为null,如果是,则先将value复制到缓存里面,然后使用toStringCache为参数,去new一个String对象并返回!
总结:
可以看出,StringBuffer和StringBuilder在添加append()方法上的区别是,前者加上了synchronized锁,因此其是线程安全的,后者是非线程安全的!
而判断是否扩容以及扩容方式,StringBuffer和StringBuilder二者没什么区别!



