1、 字符串相关类之可变字符序列:StringBuffer、StringBuilder
因为String对象是不可变对象,虽然可以共享常量对象,但是对于频繁字符串的修改和拼接操作,效率极低,空间消耗也比较高。因此,JDK又在java.lang包提供了可变字符序列StringBuffer和StringBuilder类型。
注明:此文仅个人理解,如有错误还有友友们指出。
1.1 StringBuffer与StringBuilder的理解
1.1.1、三个类的对比:String、StringBuffer、StringBuilder
- String:不可变的字符序列;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
- StringBuffer:可变的字符序列;JDK1.0声明,线程安全的,效率低;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
- StringBuilder:可变的字符序列;JDK5.0声明,线程不安全的,效率高;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
为什么说StringBuffer是线程安全的呢?我们来看一看StringBuffer的源码
我们可以看到StringBuffer类的方法前面使用了synchronized关键字,确保在访问或修改字符串时,多个线程之间不会发生竞争条件。这种线程安全性是通过牺牲一些性能来实现的。这个在多线程里面会讲到。(这个也是StringBuffer与StringBuilder的主要区别)
1.1.2、StringBuffer/StringBuilder的可变性分析(源码分析)
因为StringBuffer和StringBuilder很相似,这里我们拿StringBuilder来说。
我们可以看到StringBuilder继承了AbstractStringBuilder类(StringBuffer也是继承的这个类)。AbstractStringBuilder类定义了两个重要的变量:
char[] value; //存储字符序列的数组
int count; //实际存储的字符的个数
请记住这个count。我们再往下看
(1)关于构造方法
我们可以看到new StringBuilder()的时候,如果不传任何参数,构造方法内部声明了super(16);我们知道,这是调用了父类的构造方法,我们点进去来看看在父类中究竟发生了什么事。
清晰明了了吧,new一个StringBuilder的时候调用了super(16),而在StringBuilder的父类AbstractStringBuilder中new了一个char数组,数组长度为16。因此当不传入参数的时候StringBuilder会自动将存储字符序列的长度设置为16。
接着我们看看StringBuilder的有参构造,当参数是一个int类型的数组时比如new StringBuilder(n),StringBuilder会将存储字符序列的长度设置n。当参数是一个String类型或者StringBuilder类型的时候,StringBuilder会将数组长度设置为形参的字符串长度加上16,然后apprnd()。关于append我们下面会讲到。
总结:当我们new一个StringBuilder对象的时候,StringBuilder会自动帮我们开辟一个有空间的char示例用来存储字符序列。那么为什么要这么做呢?我们前面提到了StringBuilder是可变的,正因为是可变的,所以new一个StringBuilder对象时候自动给我们开辟了空间以防止我们频繁扩容。因此,在实例化StringBuilder对象时为其分配了初始容量,可以减少扩容的次数,提高效率。
思考:因为StringBuilder是可变的,那么当StringBuilder给我们分配的空间不够用了怎么办?
(2)关于append()方法和StringBuilder的自动扩容探究
我们知道,StringBuilder是可变的,StringBuilder对象追加字符串调用的是append()方法,例如:str.append(“abc”)。那么在StringBuilder底层是怎样运行的呢?以及自动扩容是怎么回事?我们阅读源码看看究竟是怎么回事。
可以看到是调用了父类AbstractStringBuilder的append()方法。这种设计是为了提高代码复用,因为StringBuilder和StringBuffer都继承了AbstractStringBuilder类,而StringBuilder和StringBuffer的方法基本是一致的。
接下来我们看看AbstractStringBuilder类append()方法是怎么实现的。
这里特别注意这段代码:
if (str == null)
return appendNull();
一般情况下当str.append(null)也就是形参为null时我们会怎么认为?我们点进appendNull()的源码看看:
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;
}
可以看到当调用的append()方法实参为null时,StringBullder会将调用者的字符序列后面追加"null"字符串。例如:
public void test(){
StringBuilder str = null;
StringBuilder stringBuilder = new StringBuilder("hell");
stringBuilder.append(str);
System.out.println(stringBuilder); //hellnull
}
下面我们来看重头戏,我们主要来看看append()方法调用的**ensureCapacityInternal(count + len);**这个方法。我们点进去
if(minimumCapacity - value.length > 0)//表示当前对象字符序列如果在append()追加字符串后大小大于new StringBuilder()时自动给我们分配的大小的话,也就是给我们分配的内存不够用了
这句代码主要表示当前对象字符序列如果在append()追加字符串后大小大于new StringBuilder()时自动给我们分配的大小的话,也就是给我们分配的内存不够用了。这时候就需要扩容,那么扩容的逻辑是什么呢?我们继续看这句:
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
Arrays的copyOf()方法传回的数组是新的数组对象,改变传回数组中的元素值,不会影响原来的数组。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;
}
首先,我们看这句代码 int newCapacity = (value.length << 1) + 2;使用位移操作符(<<)将当前字符数组的长度(也就是new StringBuilder时给我们分配的数组大小)(value.length)左移一位,并加上2,得到一个新的预计容量(newCapacity)。这里采用左移操作是为了进行乘以2的操作,加上2是为了留出一些额外的空间。
所以我们得到的新容量就是当前数组长度大小*2+2。注意这里指的数组长度并不是当前字符序列的长度,而是整个数组的长度。要区别length和length()。这就是StringBuilder的扩容机制。
1.1.3、对比三者的执行效率
效率从高到低排列:
StringBuilder > StringBuffer > String
测试一下:
public void test4(){
//初始设置
long startTime = 0L;
long endTime = 0L;
String text = "";
StringBuffer buffer = new StringBuffer("");
StringBuilder builder = new StringBuilder("");
//开始对比
startTime = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
buffer.append(String.valueOf(i)).reverse();
}
endTime = System.currentTimeMillis();
System.out.println("StringBuffer的执行时间:" + (endTime - startTime));
startTime = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
builder.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuilder的执行时间:" + (endTime - startTime));
startTime = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
text = text + i;
}
endTime = System.currentTimeMillis();
System.out.println("String的执行时间:" + (endTime - startTime));
}
运行结果:
1.1.4、StringBuilder、StringBuffer的API
StringBuilder、StringBuffer的API是完全一致的,并且很多方法与String相同。
常用API**
(1)StringBuffer append(xx):提供了很多的append()方法,用于进行字符串追加的方式拼接
(2)StringBuffer delete(int start, int end):删除[start,end)之间字符
(3)StringBuffer deleteCharAt(int index):删除[index]位置字符
(4)StringBuffer replace(int start, int end, String str):替换[start,end)范围的字符序列为str
(5)void setCharAt(int index, char c):替换[index]位置字符
(6)char charAt(int index):查找指定index位置上的字符
(7)StringBuffer insert(int index, xx):在[index]位置插入xx
(8)int length():返回存储的字符数据的长度
(9)StringBuffer reverse():反转
当append和insert时,如果原来value数组长度不够,可扩容。
如上(1)(2)(3)(4)(9)这些方法支持
方法链操作
。原理:
我是这么理解的,这些方法在执行时改变了自己本身的。也就是StringBuffer.reverse()时调用这个方法的StringBuffer对象改变了,并且也可以将他赋值给另一个对象,比如:StringBuffer buffer = str.reverse()。
与此同时也可以这样操作:**buffer.append(“abc”).reverse();**也就是支持这种链式编程的结构。
2、其它API
(1)int indexOf(String str):在当前字符序列中查询str的第一次出现下标
(2)int indexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询str的第一次出现下标
(3)int lastIndexOf(String str):在当前字符序列中查询str的最后一次出现下标
(4)int lastIndexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询str的最后一次出现下标
(5)String substring(int start):截取当前字符序列[start,最后]
(6)String substring(int start, int end):截取当前字符序列[start,end)
(7)String toString():返回此序列中数据的字符串表示形式
(8)void setLength(int newLength) :设置当前字符序列长度为newLength