string+和stringbuffer的速度比较

 

复制代码
public class Main{
    
    public static void main(String[] args){
        /*   1   */
        String string = "a" + "b" + "c";
        /*   2   */
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");
        string = stringBuffer.toString();
    }
    
}
复制代码

  当时大部分的新手猿友都表示,stringbuffer快于string+。惟有群里一位有工做经验的猿友说,是string+的速度快。这让LZ意识到,工做经验确实不是白积累的,一个小问题就看出来了。java

  这里确实string+的写法要比stringbuffer快,是由于在编译这段程序的时候,编译器会进行常量优化,它会将a、b、c直接合成一个常量abc保存在对应的class文件当中。LZ当时在群里贴出了编译后的class文件的反编译代码,以下。编程

复制代码
public class Main
{
  public static void main(String[] args)
  {
    String string = "abc";

    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append("a");
    stringBuffer.append("b");
    stringBuffer.append("c");
    string = stringBuffer.toString();
  }
}
复制代码

  能够看出,在编译这个java文件时,编译器已经直接进行了+运算,这是由于a、b、c这三个字符串都是常量,是能够在编译期由编译器完成这个运算的。假设咱们换一种写法。小程序

复制代码
public class Main{
    
    public static void main(String[] args){
        /*   1   */
        String a = "a";
        String b = "b";
        String c = "c";
        String string = a + b + c;
        /*   2   */
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(a);
        stringBuffer.append(b);
        stringBuffer.append(c);
        string = stringBuffer.toString();
    }
    
}
复制代码

  此处的答案貌似应该是stringbuffer更快,由于此时a、b、c都是对象,编译器已经没法在编译期进行提早的运算优化了。安全

  可是,事实真的是这样的吗?多线程

  其实答案依然是第一种写法更快,也就是string+的写法更快,这一点可能会有猿友比较疑惑。这个缘由是由于string+实际上是由stringbuilder完成的,而通常状况下stringbuilder要快于stringbuffer,这是由于stringbuilder线程不安全,少了不少线程锁的时间开销,所以这里依然是string+的写法速度更快。app

  尽管LZ已经解释了缘由,不过可能仍是有猿友依然不太相信,那么下面咱们来写一个测试程序。编程语言

复制代码
public class Main
{
  public static void main(String[] args)
  {
    String a = "a";
    String b = "b";
    String c = "c";
    long start = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
         String string = a + b + c;
         if (string.equals("abc")) {}
    }
    System.out.println("string+ cost time:" + (System.currentTimeMillis() - start) + "ms");
    start = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(a);
        stringBuffer.append(b);
        stringBuffer.append(c);
        String string = stringBuffer.toString();
        if (string.equals("abc")) {}
    }
    System.out.println("stringbuffer cost time:" + (System.currentTimeMillis() - start) + "ms");
  }
}
复制代码

  咱们每一个进行了1亿次,咱们会看到string+居然真的快于stringbuffer,是否是瞬间被毁了三观,咱们来看下结果。函数

  答案已经很显然,string+居然真的比stringbuffer要快。这里其实仍是编译器捣的鬼,string+事实上是由stringbuilder完成的。咱们来看一下这个程序的class文件内容就能够看出来了。测试

  因为文件太长,因此LZ是分开截的图。能够看到,里面有两次stringbuilder的append方法调用,三次stringbuffer的append方法调用。stringbuilder只有两次append方法的调用,是由于在建立stringbuilder对象的时候,第一个字符串也就是a对象已经被当作构造函数的参数传入了进去,所以就少了一次append方法。优化

  不过请各位猿友不要误会,这里stringbuilder之因此比stringbuffer快,是由于少了锁同步的开销,而不是由于少了一次append方法,缘由看下面这段stringbuilder类的源码就知道了。

    public StringBuilder(String str) {
    super(str.length() + 16);
    append(str);
    }

  能够看到,实际上带有string参数的构造方法,依然是使用的append方法,所以stringbuilder其实也进行了三次append方法的调用。

  看到这里,估计有的猿友就该奇怪了,这么看的话,彷佛string+的速度比stringbuffer更快,难道之前的认识都错误了?

  答案固然是否认的,咱们来看下面这个小程序,你就看出来差异有多大了。

复制代码
public class Main
{
  public static void main(String[] args)
  {
    String a = "a";
    long start = System.currentTimeMillis();
    String string = a;
    for (int i = 0; i < 100000; i++) {
         string += a;
    }
    if (string.equals("abc")) {}
    System.out.println("string+ cost time:" + (System.currentTimeMillis() - start) + "ms");
    start = System.currentTimeMillis();
    StringBuffer stringBuffer = new StringBuffer();
    for (int i = 0; i < 100000; i++) {
        stringBuffer.append(a);
    }
    if (stringBuffer.toString().equals("abc")) {}
    System.out.println("stringbuffer cost time:" + (System.currentTimeMillis() - start) + "ms");
  }
}
复制代码

  这个程序与刚才的程序有着细微的差异,可是结果却会让你大跌眼镜。咱们来看结果输出。

  看到这个结果是否是直接给跪了,效率差了这么多?这仍是LZ将循环次数降到了10万,而不是1亿,由于1亿次LZ跑了好久也没跑完,LZ等不急了,0.0。

  形成这种状况的缘由,咱们看两个程序的区别就看出来了。第一个循环1亿次的程序,不论是string+仍是stringbuffer都是在循环体里构造的字符串,最重要的是string+是由一个语句构造而成的,所以此时string+其实和stringbuffer实际运行的方式是同样的,只不过string+是使用的stringbuilder而已。

  而对于上面这个10万次循环的程序,stringbuffer就不用说了,实际运行的方式很明显。而对于string+,它将会创造10万个stringbuilder对象,每一次循环体的发生,都至关于咱们新建了一个stringbuilder对象,将string对象做为构造函数的参数,并进行一次append方法和一次toString方法。

  由上面几个小程序咱们能够看出,在string+写成一个表达式的时候(更准确的说,是写成一个赋值语句的时候),效率其实比stringbuffer更快,但若是不是这样的话,则效率会明显低于stringbuffer。咱们来再写一个程序证明这一点。

  为了避免会致使编译失败,咱们将循环次数减为1万次,不然会超出文件的最大长度,咱们先来看看刚才的程序改成1万次循环的结果。

  能够看到,在1万次的循环下,依然能够看到效率上的明显差别,这个差距已经足够咱们观察了。如今咱们就改一种写法,它会让string+的效率提升到stringbuffer的速度,甚至更快。

  这里咱们是将1万次字符串的拼接直接写成了一个表达式,那个a+a+...表达式一共是1万个(是LZ使用循环打印出来贴到代码处的),能够看到,此时string+的速度已经超过了stringbuffer。

  所以LZ给各位猿友一个建议,若是是有限个string+的操做,能够直接写成一个表达式的状况下,那么速度其实与stringbuffer是同样的,甚至更快,所以有时候不必就几个字符串操做也要建个stringbuffer(若是中途拼接操做的字符串是线程间共享的,那么也建议使用stringbuffer,由于它是线程安全的)。可是若是把string+的操做拆分红语句去进行的话,那么速度将会指数倍降低。

  总之,咱们大部分时候的宗旨是,若是是string+操做,咱们应该尽可能在一个语句中完成。若是是没法作到,而且拼接动做不少,好比数百上千成万次,则必须使用stringbuffer,不能用string+,不然速度会很慢。

 

Java的方法参数传递方式

 

  这个问题的引入是当时LZ在群里问了这样一个问题,就是Java的方法参数传递是值传递仍是引用传递?对于基本类型和对象来讲,都会发生什么状况?

  这道题大部分猿友仍是说的不错的,包括群里的新手猿友。答案是Java只有值传递,由于Java只有值传递,所以在改变形参的值的时候,实参是不会所以而改变的。这一点从下面这个小程序能够很明显的看出来。

复制代码
public class Main
{
  public static void main(String[] args)
  {
      int a = 2;
      Object object = new Object();
      System.out.println(a + ":" + object);
      change(a, object);
      System.out.println(a + ":" + object);
  }
  
  public static void change(int a,Object object){
      a = 1;
      object = new Object();
  }
}
复制代码

  咱们在方法当中改变形参的值,以后再次输出两个实参的值,会发现它们无任何变化。

  这就足以说明Java只有值传递了,不管是对象仍是基本类型,改变形参的值不会反应到实参上面去,这也正是值传递的奥义所在。

  对于基本类型来讲,这一点比较明显,不过对于对象来说,不少猿友会有误解。认为咱们在方法里改变形参对象属性的值,是会反映到实参上面去的,所以部分猿友认为这就是引用传递。

  首先LZ要强调的是,上面也说了,咱们只是改变形参对象属性的值,反映到实参上面去的,而不是真的改变了实参的值,也就是说实参引用的对象依然是原来的对象,只不过对象里的属性值改变了而已。

  针对上面这一点,咱们使用下面这个程序来讲明。

复制代码
public class Main
{
  public static void main(String[] args)
  {
      int a = 2;
      Entity entity = new Entity();
      entity.a = 100;
      System.out.println(a + ":" + entity);
      System.out.println(entity.a);
      change(a, entity);
      System.out.println(a + ":" + entity);
      System.out.println(entity.a);
  }
  
  public static void change(int a,Entity entity){
      a = 1;
      entity.a = 200;
  }
}

class Entity{
    int a;
}
复制代码

  咱们在方法里改变了entity对象的属性值为200,咱们来看一下结果。

  能够看到,实参对象的值依然没有改变,只是属性值变了而已,所以这依旧是值传递的范围。为了说明这个区别,咱们来看下真正的引用传递。因为Java当中不存在引用传递,所以LZ借用C/C++来让各位看下真正的引用传递是什么效果。

复制代码
 1 #include <stdio.h>
 2 
 3 class Entity{
 4 public:
 5     int a;
 6     Entity(){};
 7 };
 8 
 9 void change(int &a,Entity *&entity);
10 
11 int main(){
12     int a = 2;
13     Entity *entity = new Entity();
14     printf("%d:%p\n",a,entity);
15     change(a, entity);
16     printf("%d:%p\n",a,entity);
17 }
18 
19  void change(int &a,Entity *&entity){
20     a = 1;
21     entity = new Entity();
22 }
复制代码

  LZ尽可能保持和Java的第一个程序是同样的结构,只不过C/C++中没有现成的Object对象,所以这里使用Entity对象代替,这样便于各位猿友理解。咱们来看下结果,结果会发现引用传递的时候,在方法里改变形参的值会直接反应到实参上面去。

  能够看到,在引用传递的时候,不管是基本类型,仍是对象类型,实参的值都发生了变化,这里才是真正的引用传递。固然了,LZ对C/C++的理解很是有限,不过毋庸置疑的是,真正的引用传递应该是相似上述的现象,也就是说实参会因形参的改变而改变的现象,而这显然不是咱们Java程序当中的现象。

  所以,结论就是Java当中只有值传递,可是这并不影响咱们在方法中改变对象参数的属性值。

  

文章小结

 

  咱们平时多了解一些语言的特性确实是有不少好处的,这会潜移默化的影响咱们编码的质量。但愿各位猿友在遇到这种问题的时候也本身多写写代码,看看本身的理解对不对,在这样的过程当中进步会很快,尤为是在初次接触一个编程语言的时候。