在牛客网上做了一道题:
答案是StringBuffer以及String。String不用说,是字符串常量类。但是StringBuffer是常量类,用了这么多年我还是才知道,今天研究一下StringBuffer类。首先,我是带着以下几个问题研究的:
(1)StringBuffer 使用final修饰,为什么可以拼接字符串,改变内容?
(2)为什么打印StringBuffer对象打印的是拼接后的值,而不是StringBuffer对象?
(3)使用StringBuffer 的优势在哪里?什么情况下使用最合适?
第一个问题:StringBuffer 使用final修饰,为什么可以拼接字符串,改变内容?
首先总结一下final的作用:
(1)修饰变量,则该变量无法修改
(2)修饰类,则类不可继承且引用地址不可变,但是内容可变。
(3)修饰方法,方法不可继承。
举个例子:创建了一个buffer对象,使用append拼接了一个char类型的值和String类型的值,之后打印buffer对象的地址,拼接前后地址不变。
地址未变,说明引用未变,但是值改变了,通过源码分析一下:
StringBuffer是个常量类,继承AbstractStringBuilder,对值进行插入或者修改操作的方法都加了synchronized,保证线程安全。
拼接char类型源码:buffer.append(char),继承父类AbstractStringBuilder的append(char),并且使用父类的append(char)方法
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
@Override
public synchronized StringBuffer append(char c) {
toStringCache = null;
//调用父类AbstractStringBuilder的append方法
super.append(c);
//返回当前对象
return this;
}
}
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
@Override
public AbstractStringBuilder append(char c) {
//调用扩容方法,因为char类型占一个字节,因此长度+1即可
ensureCapacityInternal(count + 1);
//将char值放入数组中
value[count++] = c;
return this;
}
//扩容方法,调用Arrays.copyOf扩容
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
//调用Arrays.copyOf方法扩容value数组
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
}
public class Arrays {
public static char[] copyOf(char[] original, int newLength) {
//创建一个新的长度数组
char[] copy = new char[newLength];
//将原来的数组复制到新数组中,这里是浅拷贝,copy新数组的地址不变
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
//返回新数组
return copy;
}
}
AbstractStringBuilder类中有数组value,用于存储拼接后的数组值,当调用append方法时,会先计算需要的数组长度(旧数组的长度+参数的长度),之后创建新数组,并且将原来数组拷贝到新数组中,这里是浅拷贝,因此返回新数组的地址和原来数组地址已经不一样,在jvm中又重新开辟了一块内存用于存放新数组。地址这里可以打debugger看value数组前后的地址即可验证:
字符串拼接:buffer.append(s);StringBuffer实现这个方法仍然是继承自AbstractStringBuffer的append(String s)方法,直接看父类的append的源码:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
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;
}
//扩容方法,调用Arrays.copyOf扩容
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
//调用Arrays.copyOf方法扩容value数组
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
//拼接空值数组后拼接"null"
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 final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
//将当前值依次放到对应的数组中,放在数组的后面,浅拷贝
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);
}
}
看完了上述源码,大致明白了为何可以实现字符串拼接了,StringBuffer使用了父类AbstractStringBuilder中的数组变量value用于拼接数据,支持int、char、String、double、long、Object以及其他类型的数据,拼接完成之后返回当前对象this,因此对象一直没变,但是value数组中的数据已经改变。
第二个问题:为什么打印StringBuffer对象打印的是拼接后的值,而不是StringBuffer对象
我们发现,打印StringBuffer的值,并不是调用其中的方法,而是直接打印StringBuffer的对象:
我们平时打印对象时,会打印当前对象的class对象信息,比如这种:com.example.demo.A@3578436e。为什么StringBuffer却是打印的拼接后的值呢?因为StringBuffer重写了toString()方法,toString方法里面将当前最新的数组转化为字符串常量返回,因此打印buffer时为当前拼接后的数值信息。
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
/**
* A cache of the last value returned by toString. Cleared
* whenever the StringBuffer is modified.
使用transient反序列化修饰,每次StringBuffer的值被修改时,都置为null
*/
private transient char[] toStringCache;
@Override
public synchronized String toString() {
if (toStringCache == null) {
//将当前拼接后的最新数组放到toStringCache中
toStringCache = Arrays.copyOfRange(value, 0, count);
}
//转化为String类型
return new String(toStringCache, true);
}
}
第三个问题:使用StringBuffer 的优势在哪里?
那么使用优势在哪里呢?首先,StringBuffer修改数组的方法都是被synchronized修饰,因此在多线程环境下是安全的。在性能方面,我们将StringBuffer与StringBuilder/String三个做字符拼接的所用时间进行比较,我将拼接次数分别设置为100次和10000次,比较各自耗费时间:
@Test
public void test04(){
String s = "a";
long before;
long after;
before = System.currentTimeMillis();
for(int i = 0; i < 100000; i++){
s = s + "a";
}
after = System.currentTimeMillis();
System.out.println("String : " + (after - before));
StringBuilder builder = new StringBuilder();
before = System.currentTimeMillis();
for(int i = 0; i < 100000; i++){
builder.append("a");
}
after = System.currentTimeMillis();
System.out.println("builder : " + (after - before));
StringBuffer buffer = new StringBuffer();
before = System.currentTimeMillis();
for(int i = 0; i < 100000; i++){
buffer.append("a");
}
after = System.currentTimeMillis();
System.out.println("buffer : " + (after - before));
}
运行结果:
很明显,在大量字符串拼接方面,性能比较结果为StringBuilder>StringBuffer>String。但是StringBuffer和StringBuilder略有差异,但是差异不大,后面我又运行了几次,发现两者所用时间都是0,可以忽略不计。因此在多线程环境,并且需要大量拼接字符串的时候,可以使用StringBuffer。单线程环境也可以使用,毕竟和StringBuilder所用时间差不大。
StringBuffer的优势:
(1)线程安全,多线程环境下保证数据安全。
(2)拼接时间比String所用时间少,和StringBuilder所用时间相差不大,因此性能也不错。
(3)提供了各种append类型的方法,兼容性强。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/158129.html