栈与堆,String和StringBuffer(二) <转>

关于String和StringBuffer之间的性能得先说说编译期和运行期。
原文:[url]http://blog.csdn.net/mydream83/archive/2007/04/21/1573995.aspx[/url]
编译期,也就是在.java文件在编译成.class文件这个过程,这个是靠jdk的编译器来完成的,而运行期则是.class文件在JVM上运行的过程,当然是靠JVM了。那么从性能优化的角度,在编译期能做就不要在运行期做。

拿一个以前看到过的例子

String a = "abc"; String b = "ab" + "c"; System.out.println(a == b);

String a = "abc"; String s = "ab"; String b = s + "c"; System.out.println(a == b);

第一个打印出来为true,第二个为false,这是因为第一个b的值是在编译期确定,第二个b的值是运行期才确定的。jdk的编译器有这样一个功能,在编译String b = "ab" + "c"; 时会把语句变成这样 String b = "abc"; 所以在运行期JVM执行的其实是String a = "abc"; String b = "abc"; 而第二种情况在编译器时编译器并不知道这个s的值,它只能判断语法有无错误,到JVM运行的时候在检查到有变量参与运算会重新分配一个内存来存放值 abc,相当于new了一个对象,而且运行期需要额外的开销当字符串的值无法预先知道的时候,在《提高String和StringBuffer性能的技》一文中已经说到,第二种情况编译器其实会把语句编译成 String b = new StringBuffer().append(s).append("c").toString();

在使用 String对象的时候有个最常用的写法,String s = "a"; s += "b"; 也就是+=操作符,反正我以前是老这样写的,后来代码看得多了,才晓得其间的问题,这句代码就好比上面说到的第二种情况,相当于s = s+ "b"; 这句代码在运行期才确定,并且会重新new一个对象,若是在一个for循环语句中使用,那么循环几次不就会实例几个对象吗,所以说遇到这类情况通通用 StringBuffer来代替,两者的性能差多少呢?《提高String和StringBuffer性能的技》上面有个测试代码,我自己测试了下

public class Test{
public static void main(String[] arg){
long first = System.currentTimeMillis();
StringBuffer sb = new StringBuffer();
String sRes = null;
for(int i = 0; i < 1024*1024; i++){
sb.append("aaa");
}
sRes = sb.toString();
long second = System.currentTimeMillis();
System.out.println(second - first); // 大概420毫秒左右。
}
}

public class Test{
public static void main(String[] arg){
long first = System.currentTimeMillis();
// StringBuffer sb = new StringBuffer();
String sRes = null;
for(int i = 0; i < 1024*1024; i++){
// sb.append("aaa");
sRes += "aaa";
}
//sRes = sb.toString();
long second = System.currentTimeMillis();
System.out.println(second - first);
// -_- !!! 因为这个等待时间太长,我就把它给关了,在这个等待的期间我上了趟厕所,看了会电视
//大概有十分钟。可见两者性能的差距。
}
}

为什么会差这么多,决定去读读StringBuffer类的源代码,StringBuffer类基础了AbstractStringBuilder类,主要的实现都是在AbstractStringBuilder类中的,以这种实例方式 StringBuffer sb = new StringBuffer(); 会调用StringBuffer的

public StringBuffer() {
super(16);
}

可以看出传了参数16,在AbstractStringBuilder带参实例方法中会实例一个数组,char[] value = new char[16]; 可见这个参数是用来实例char数组的。

StringBuffer 的append()方法,我只说说传入对象的情况,若是传的不是String类型的对象,在StringBuffer中会将其转换为String对象,再传给父类的append()方法,父类的append方法中做了件事情:第一,判断传入对象是否为null,若是为null,则给该对象赋"null",第二,将原value数组的长度和传入对象长度求和,若总长度大于原value数组长度,则执行expandCapacity();方法,第三,通过 System.arraycopy来拷贝数组,也就是将传入的对象转换为char[]数组,然后拼接到原value数组的后面。可以看到, StringBuffer类之所以能够变化长度,原因在于它内部维护了一个char[]数组,而这个expandCapacity();方法放在后面得再提一下。

StringBuffer的toString();方法,return new String(value, 0, count); 这里value就是char[],count是它的长度,可见StringBuffer从始至终只new了一个String对象,这也是它性能较优的原因。

expandCapacity()方法是AbstractStringBuilder类的方法,这个方法触发的条件是当原char[] 数组里面实际字符长度加上传入对象的长度之和大于原char[]数组最大长度,即加上传入对象后实际的字符个数已经大于了原来的数组实例化时的长度,自然要重新改变这个char[]的长度,它以什么规则来改变的呢。

void expandCapacity(int minimumCapacity) {
int newCapacity = (value.length + 1) * 2;
if (newCapacity < 0) {
newCapacity = Integer.MAX_VALUE;
} else if (minimumCapacity > newCapacity) {
newCapacity = minimumCapacity;
}
char newValue[] = new char[newCapacity];
System.arraycopy(value, 0, newValue, 0, count);
value = newValue;
}

这里传入的参数minimumCapacity是新数组里实际字符的长度,数组的长度并不代表里面字符长度,比如说

StirngBuffer sb = new StringBuffer(); 它会创建默认长度为16的char[]数组,但只时并没有传入对象,所以里面的16个字符都是空, sb.append ("abcdabcdabcdabcdabcd"); 加入长度为20的字符串,那么这个minimumCapacity 其实等于20,并不是16+20,它会重新实例一个char[]数组,长度是原数组长度加1的2倍,若这个新长度还小于minimumCapacity ,则直接将 minimumCapacity赋给新长度,这样新数组的长度就确定了。这就有个问题,当char[]数组已经相当大时,而传入对象又导致超过原长度,则会创建一个更大的数组并且对每个字符进行拷贝,很有可能造成空间的浪费,对字符的拷贝也相当耗费资源,所以当字符串长度比较大时使用带参的构造方法更合适些,StringBuffer sb = new StringBuffer(250000); 这个长度可以根据append进去的对象总长度估计一下。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值