Java8中字符串连接加号“+”和“StringBuilder#append()”

前言

  在我们平时的开发过程中,使用最多的对象几乎就是String了吧,那么自然的也涉及到了字符串的拼接。在最开始的开发经验中,我们使用最多的字符串拼接估计就是使用加号+直接将两个字符串变量拼接在一起,这样固然没什么问题,但是当我们知道了如下特性之后:

String StringBuffer StringBuilder
类是否可变 不可变(final) 可变 可变
功能介绍 每次对String的操作都会在“常量池”中生成新的String对象 任何对它指向的字符串的操作都不会
产生新的对象。每个 StringBuffer
对象都有一定的缓冲区容量,字符串
大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,自动扩容
功能与 StringBuffer相同,相比少了同步锁,执行速度更快
线程安全 线程安全 线程安全 线程不安全
使用场景推荐 单次操作或者循环外操作字符串 多线程操作字符串 单线程操作字符串

  对于字符串拼接的场景我们更多的开始优先使用StringBuilder

  其实在Java8中对于String对象使用+的这种这种拼接方式,在编译之后在部分场景+java.lang.StringBuilder#append(java.lang.String)是完全一样的,但是如果想要更加灵活更加高效的达到字符串连接的目的,还是尽量使用StringBuilder,这样代码可读性也会更高,而不是把代码交给编译器转换之后进行编译,那么我们这里就通过实际代码进行分析。

“+”编译后

看看如果我们在程序中直接使用+来连接字符串的情况,用下面一个简单的例子来说明,进行两个字符串连接操作,即s3 = s1 + s2

public class Main3 {
    public static void main(String[] args) {
        String s1 = "11111";
        String s2 = "22222";
        String s3 = s1 + s2;
    }
}

接着javap -c Main3.class看一下编译后的情况,可以看到编译器其实是对+进行了转换的,转成了 StringBuilder对象来操作了,首先使用 s1 创建 StringBuilder 对象,然后用 append方法连接 s2,最后调用toString方法完成赋值给s3。

public class com.sijibao.gyl.mallservice.test.Main3 {
  public com.sijibao.gyl.mallservice.test.Main3();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String 11111
       2: astore_1
       3: ldc           #3                  // String 22222
       5: astore_2
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_1
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      17: aload_2
      18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      24: astore_3
      25: return
}

“+”与”append”等价吗

  前面可以看到+在编译器作用下都会转成 StringBuilderappend方法执行,所以如果抛开运行效率来说,它们其实本质是一样的。

  本质一样是否就能说明它们是等价的呢?或者说能否为了方便直接用+来连接字符串,剩下的事就交给编译器了?继续看个例子,在这个例子中有个 for 循环进行字符串连接操作。

public class Main {
    public static void main(String[] args) {
        String s4 = "sfsd1fds";
        for (int i = 0; i < 11; i++) {
            s4 = s4 + i;
        }
        System.out.println(s4);
    }
}
public class com.sijibao.gyl.mallservice.test.Main {
  public com.sijibao.gyl.mallservice.test.Main();//实例化方法执行
    Code:
       0: aload_0                           // (不知原因,每个类的初始化都有从局部变量中加载引用)
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V//执行当前类的初始化
       4: return                            //不做返回

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String sfsd1fds//从字符串常量池中加载字符串"sfsd1fds"到虚拟机栈
       2: astore_1                          //将字符串"sfsd1fds"字面量的引用赋值给s4
       3: iconst_0                          //声明int常量,即for循环中的字面量:0
       4: istore_2                          //将int型字面量0赋值给int i
       5: iload_2                           //从本地变量加载int
       6: bipush        11                  //加载byte变量11,在运行过程中数字-128~127实际是byte类型,大于127时是short类型
       8: if_icmpge     36                  //if_icmp<cond>int比较成功时分支,ge即greater or equal
      11: new           #3                  // class java/lang/StringBuilder//在堆中创建一个StringBuilder对象,并将对应的引用压入到栈顶
      14: dup                               // 复制操作数堆栈上的顶部值,并将复制的值推送到操作数堆栈上(即这时候操作数栈中,栈顶和第二条数据都是上一步new出来的对象的引用)
      15: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V//调用实例方法;对超类、私有和实例初始化方法调用的特殊处理(自动弹出栈顶数据new引用,带入到实例方法中)
      18: aload_1                           //从局部变量加载引用
      19: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;//调用实例方法;对超类、私有和实例初始化方法调用的特殊处理
      22: iload_2                           //从局部变量加载int
      23: invokevirtual #6                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;//调用实例方法;对超类、私有和实例初始化方法调用的特殊处理
      26: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;//调用实例方法;对超类、私有和实例初始化方法调用的特殊处理
      29: astore_1                          //将引用存储到局部变量中
      30: iinc          2, 1                //按常量递增局部变量(i++)
      33: goto          5                   //回到代码行5重新执行循环
      36: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;//从类中获取静态字段
      39: aload_1                           //从局部变量加载引用
      40: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V//调用实例方法;对超类、私有和实例初始化方法调用的特殊处理
      43: return                            //方法执行完成,返回void
}
  • aload_<n>:从局部变量加载引用到操作数栈中(入栈)
  • ldc:从运行时常量池中获取数据,然后压入虚拟机栈中(直接声明的字符串"sfsd1fds"会首先自动先放在字符串常量池中(独立于堆之外、存在于方法区))(入栈)
  • astore_<n>:将引用存储到局部变量中(出栈)
  • iconst_<i>:声明int型常量,并添加到操作数栈(入栈)
  • istore_<n>:将int存储到局部变量中(出栈)
  • iload_<n>:从局部变量加载int(入栈)
  • bipush:加载byte常量池数据到操作数栈中(在java文件转换成class文件后,实际上-128~127的int型数据会转而使用byte来存储,大于127的数据会转换成short)(bi:binary)(入栈)
  • if_icmp<cond>:int比较成功时分支,ge即greater or equal(大于或者等于,不是>=,而是>或者==都是这个指令)(icmp:int compare)
  • new:在堆中创建一个对象:在堆中开辟内存空间并创建对象,并将对应的引用压入栈顶(推堆)(入栈)
  • dup:复制操作数堆栈上的顶部值,并将复制的值推送到操作数堆栈上(入栈)
  • invokevirtual:调用实例方法(一般在后面会有具体调用的类和方法)
  • iinc:按常量递增局部变量

扩展引用:javap—跟在new指令屁股后面的dup指令

以上代码等价于这样来写:

public class Main {
    public static void main(String[] args) {
        String s4 = "sfsd1fds";
        for (int i = 0; i < 11; i++) {
            StringBuilder s4SB = new StringBuilder();
            s4SB.append(s4);
            s4SB.append(i);
            s4 = s4SB.toString();
        }
        System.out.println(s4);
    }
}
  1. 每次循环中都需要new一个StringBuilder对象
  2. 执行两次append方法
  3. 转换成String对象
  4. 虚拟机栈中的局部变量s4指向新的地址(闲置之前的字面量,使其在后续被当作垃圾而回收)

友好写法

把事情都丢给编译器是不友好的,为了能让程序执行更加高效,最好是我们自己来控制StringBuilder的实例,比如下面,只创建一个 StringBuilder实例,后面用append方法连接字符串。

public class Main2 {
    public static void main(String[] args) {
        StringBuilder s4 = new StringBuilder("1");
        for (int i = 0; i < 10; i++) {
            s4.append(i);
        }
        System.out.println(s4);
    }
}
public class com.sijibao.gyl.mallservice.test.Main2 {
  public com.sijibao.gyl.mallservice.test.Main2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/StringBuilder//在堆中创建一个StringBuilder对象,并将创建对象的引用压入栈顶
       3: dup                               // 复制一份栈顶数据压入栈中
       4: ldc           #3                  // String 1//从运行时常量池中加载数据,并压入栈中
       6: invokespecial #4                  // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V // StringBuilder的实例化方法
       9: astore_1                          // 将StringBuilder的引用存如到局部变量中
      10: iconst_0                          // 声明int类型常量0
      11: istore_2                          // 将int类型数据赋值即:int i = 0
      12: iload_2                           // 加载局部变量i到操作数栈中
      13: bipush        10                  // java.lang.Byte.ByteCache 数据10入栈
      15: if_icmpge     30                  // for循环中的逻辑比较是否满足
      18: aload_1                           // 装载变量s4到操作数栈中
      19: iload_2                           // 转载变量i到操作数栈中
      20: invokevirtual #5                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;//执行append方法
      23: pop                               // 弹出顶部操作数栈中的值
      24: iinc          2, 1                // i自增
      27: goto          12                  // 跳转代码行12
      30: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;获取静态变量System.out
      33: aload_1                           // 装载变量s4到操作数栈中
      34: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      37: return
}

以上结论仅针对Java8,在Java9之后的版本中其字节码比较简洁,使用的是命令invokedynamic

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Java概述 何为编程 编程就是让计算机为解决某个问题而使用某种程序设计语言编写程序代码,并最终得到结果的过程。 ...
    极课编程阅读 10,195评论 0 40
  • 数据的传递 1.实参的数目、数据类型和次序必须和所调用的方法声明的形式参数列表匹配。 2.return 语句...
    仙人掌_b340阅读 1,924评论 0 0
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 12,718评论 28 53
  • 人工智能是什么?什么是人工智能?人工智能是未来发展的必然趋势吗?以后人工智能技术真的能达到电影里机器人的智能水平吗...
    ZLLZ阅读 9,369评论 0 5
  • 首先介绍下自己的背景: 我11年左右入市到现在,也差不多有4年时间,看过一些关于股票投资的书籍,对于巴菲特等股神的...
    瞎投资阅读 11,041评论 3 8