Java性能陷阱:StringBuffer的性能真的比String好吗?

发表于:2007-05-25来源:作者:点击数: 标签:java性能StringBuffer真的陷阱
看了cherami写的 使用String还是StringBuffer? 以及后面多为网友的评论,感觉这个问题有必要好好的深究一下,因此编写了一个简单的 测试 类和一个脚本来运行它。通过修改那个测试类为不同的参数,并且在不同的JDK上测试发现这个问题实在是一个非常有趣的问题
看了cherami写的使用String还是StringBuffer?以及后面多为网友的评论,感觉这个问题有必要好好的深究一下,因此编写了一个简单的测试类和一个脚本来运行它。通过修改那个测试类为不同的参数,并且在不同的JDK上测试发现这个问题实在是一个非常有趣的问题。下面让我们开始吧。

第一步,准备工作


为了方便后面测试工作的进行,有必要编写一个简单的脚本:
echo test by jdk1.2.2
/opt/java/jdk1.2.2/bin/javac StringTest.java
/opt/java/jdk1.2.2/bin/java StringTest
echo test by jdk1.3.1_09
/opt/java/jdk1.3.1_09/bin/javac StringTest.java
/opt/java/jdk1.3.1_09/bin/java StringTest
echo test by jdk1.4.2
/opt/java/jdk1.4.2/bin/javac StringTest.java
/opt/java/jdk1.4.2/bin/java StringTest

上面的脚本根据需要可以应用在windows或者linux上,我是在linux进行测试的,因此我把它保存为一个文件stringtest.sh,如果你在windows上测试,你可以保存为stringtest.bat。

意:本文后面的运行结果都是连续运行多次并取其中一个比较平均和典型的样本值)

第二步,开始写代码


最开始我几乎没有怎么考虑就写出了如下的代码:
  1. public class StringTest {
  2.   public static void main(String[] args) {
  3.     long start=System.currentTimeMillis();
  4.     for (int i=0; i<10000; i++) {
  5.       String s="This is a "+"long test string for "+"different JDK performance "+"testing.";
  6.     }
  7.     long end=System.currentTimeMillis();
  8.     System.out.println("Directly string contact:"+(end-start));
  9.     start=System.currentTimeMillis();
  10.     for (int i=0; i<10000; i++) {
  11.       StringBuffer buffer = new StringBuffer();
  12.       buffer.append("This is a ");
  13.       buffer.append("long test string for ");
  14.       buffer.append("different JDK performance ");
  15.       buffer.append("testing.");
  16.       String ss=buffer.toString();
  17.     }
  18.     end=System.currentTimeMillis();
  19.     System.out.println("StringBuffer contact:"+(end-start));
  20.   }
  21. }

运行结果:
test by jdk1.2.2
Directly string contact:0
StringBuffer contact:120
test by jdk1.3.1_09
Directly string contact:1
StringBuffer contact:47
test by jdk1.4.2
Directly string contact:0
StringBuffer contact:53

呵呵,是不是大出意外?!!!我开始也是,但是别急,实际上我犯了一个错误,由于进行字符串+操作的都是字符串直接量,因此编译器在编译的时候进行了优化,所以String s="This is a "+"long test string for "+"different JDK performance "+"testing.";编译以后实际上变成了:String s="This is a long test string for different JDK performance testing.";,呵呵,这就是一个简单的赋值操作了,难怪所用的时间几乎没有了。

第三步,修改代码


  1. public class StringTest {
  2.   public static void main(String[] args) {
  3.     String s1="This is a ";
  4.     String s2="long test string for ";
  5.     String s3="different JDK performance ";
  6.     String s4="testing.";
  7.     
  8.     
  9.     long start=System.currentTimeMillis();
  10.     for (int i=0; i<10000; i++) {
  11.       String s=s1+s2+s3+s4;
  12.     }
  13.     long end=System.currentTimeMillis();
  14.     System.out.println("Directly string contact:"+(end-start));
  15.     start=System.currentTimeMillis();
  16.     for (int i=0; i<10000; i++) {
  17.       StringBuffer buffer = new StringBuffer();
  18.       buffer.append(s1);
  19.       buffer.append(s2);
  20.       buffer.append(s3);
  21.       buffer.append(s4);
  22.       String ss=buffer.toString();
  23.     }
  24.     end=System.currentTimeMillis();
  25.     System.out.println("StringBuffer contact:"+(end-start));
  26.   }
  27. }

运行结果:
test by jdk1.2.2
Directly string contact:140
StringBuffer contact:123
test by jdk1.3.1_09
Directly string contact:32
StringBuffer contact:21
test by jdk1.4.2
Directly string contact:48
StringBuffer contact:37

从上面的结果看我们确实可以得到《使用String还是StringBuffer?》中的结论,而且还可以看出不同的JDK的版本的性能的差异还是比较大的,JDK1.2.2的性能最差,而JDK1.3.1的性能最好。

讨论结束了吗?呵呵,不要急,还远远没有结束呢。:)

第四步,减少循环次数(从10000次变成1000)


  1. public class StringTest {
  2.   public static void main(String[] args) {
  3.     String s1="This is a ";
  4.     String s2="long test string for ";
  5.     String s3="different JDK performance ";
  6.     String s4="testing.";
  7.     
  8.     
  9.     long start=System.currentTimeMillis();
  10.     for (int i=0; i<1000; i++) {
  11.       String s=s1+s2+s3+s4;
  12.     }
  13.     long end=System.currentTimeMillis();
  14.     System.out.println("Directly string contact:"+(end-start));
  15.     start=System.currentTimeMillis();
  16.     for (int i=0; i<1000; i++) {
  17.       StringBuffer buffer = new StringBuffer();
  18.       buffer.append(s1);
  19.       buffer.append(s2);
  20.       buffer.append(s3);
  21.       buffer.append(s4);
  22.       String ss=buffer.toString();
  23.     }
  24.     end=System.currentTimeMillis();
  25.     System.out.println("StringBuffer contact:"+(end-start));
  26.   }
  27. }

运行结果:
test by jdk1.2.2
Directly string contact:12
StringBuffer contact:19
test by jdk1.3.1_09
Directly string contact:9
StringBuffer contact:13
test by jdk1.4.2
Directly string contact:12
StringBuffer contact:18

你看到什么了?上面的结论又被推翻了,字符串直接连接操作的性能比使用StringBuffer的性能好,无论是在那个版本的JDK上,而且在JDK1.3.1上的性能依然是最高的。我们不禁想问为什么。很遗憾,我也不知道为什么。我的想法是上面两种程序的差异主要在于循环的次数,而次数的不同导致创建的对象的个数也就是内存的使用情况不同,另外一个不同就是运行次数不同而导致JIT进行运行时优化的强度不同。这两个因素只是我认为对程序结果肯定会有影响的因素,但是是如何影响的,我就不知道了。

到了这里,也许我们的故事该结束了,但是从上面的结果看,我感觉有必要再做一些测试。

第五步,将所有的中间结果串起来


  1. public class StringTest {
  2.   public static void main(String[] args) {
  3.     String s1="This is a ";
  4.     String s2="long test string for ";
  5.     String s3="different JDK performance ";
  6.     String s4="testing.";
  7.     
  8.     
  9.     long start=System.currentTimeMillis();
  10.     String s="";
  11.     for (int i=0; i<1000; i++) {
  12.       s+=s1+s2+s3+s4;
  13.     }
  14.     long end=System.currentTimeMillis();
  15.     System.out.println("Directly string contact:"+(end-start));
  16.     start=System.currentTimeMillis();
  17.     StringBuffer buffer = new StringBuffer();
  18.     for (int i=0; i<1000; i++) {
  19.       buffer.append(s1);
  20.       buffer.append(s2);
  21.       buffer.append(s3);
  22.       buffer.append(s4);
  23.       String ss=buffer.toString();
  24.     }
  25.     end=System.currentTimeMillis();
  26.     System.out.println("StringBuffer contact:"+(end-start));
  27.   }
  28. }

运行结果:
test by jdk1.2.2
Directly string contact:997
StringBuffer contact:13
test by jdk1.3.1_09
Directly string contact:1900
StringBuffer contact:21
test by jdk1.4.2
Directly string contact:2157
StringBuffer contact:11

我们终于看到使用StringBuffer的巨大优势了!而且你也许还会注意到一个非常有趣的现象:JDK1.3.1的性能似乎在这里表现得没有那么突出了,在StringBuffer甚至表现得最差了。非常有趣,不是吗?

第六步,再次减少循环次数(从1000次减少为500次)


  1. public class StringTest {
  2.   public static void main(String[] args) {
  3.     String s1="This is a ";
  4.     String s2="long test string for ";
  5.     String s3="different JDK performance ";
  6.     String s4="testing.";
  7.     
  8.     
  9.     long start=System.currentTimeMillis();
  10.     String s="";
  11.     for (int i=0; i<500; i++) {
  12.       s+=s1+s2+s3+s4;
  13.     }
  14.     long end=System.currentTimeMillis();
  15.     System.out.println("Directly string contact:"+(end-start));
  16.     start=System.currentTimeMillis();
  17.     StringBuffer buffer = new StringBuffer();
  18.     for (int i=0; i<500; i++) {
  19.       buffer.append(s1);
  20.       buffer.append(s2);
  21.       buffer.append(s3);
  22.       buffer.append(s4);
  23.       String ss=buffer.toString();
  24.     }
  25.     end=System.currentTimeMillis();
  26.     System.out.println("StringBuffer contact:"+(end-start));
  27.   }
  28. }

运行结果:
test by jdk1.2.2
Directly string contact:270
StringBuffer contact:9
test by jdk1.3.1_09
Directly string contact:251
StringBuffer contact:2
test by jdk1.4.2
Directly string contact:264
StringBuffer contact:2

呵呵,JDK1.3.1的性能优势又回来了!而且和1000次的情况相比,字符串直接操作的性能差距减小了。
而且上面给出的仅仅是一个样本值,如果你多运行几次会发现如下的现象:JDK1.2.2和JDK1.3.3的运行时间在每次运行的时候都差不多,但是JDK1.4.2的StringBuffer操作的运行时间的波动就比较大了,在我的环境下的波动范围竟然在1到16之间!

故事到这里,我不想再继续做更多的测试了,我的头脑里面已经没有谁好谁坏的结论了,我们有必要总结一下我们上面实验的结果了。

总结


我感觉从上面的一些实验来看,我们可以得到如下的一些结论:
1.String的+操作和StringBuffer不存在绝对的性能问题,但是可以得到的结论是,无论在那个版本的JDK上,如果是连接的内容非常的大和多,并且代码被执行的次数很多,那么不要犹豫,使用StringBuffer,这个性能差异是很大的。如果是连接比较短小的内容,那么使用你喜欢的方式吧,它们的性能没有明显的差异。
2.不同版本的JDK对这两种操作所做的优化是不同的,总体而言JDK1.3.1对它们的优化是比较好的。

我想提醒你的是不要得出如下的结论:
1.JDK1.3.1的性能是最好的。我们的结论只是针对我们测试的问题,对于其他的问题,JDK1.3.1的表现如何就不得而知了,但是我相信JDK1.3.1的总体性能比JDK1.2.2要好一些。

我想提醒大家最后注意的事情是:
1.不要盲目相信性能优化,用一句我以前在JR的某篇文章里面看到的话说就是:只有在你知道它确实是性能瓶颈的地方才去优化。
2.JDK的不同版本对于某些在某些方面进行了一些运行和编译优化,但是这种优化是不固定的,我们不要对那些优化寄予太大的期望,最根本的还是你的代码的性能。

原文转自:http://www.ltesting.net

评论列表(网友评论仅供网友表达个人看法,并不表明本站同意其观点或证实其描述)