前言
在我们平时的开发过程中,使用最多的对象几乎就是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”等价吗
前面可以看到+
在编译器作用下都会转成 StringBuilder
的append
方法执行,所以如果抛开运行效率来说,它们其实本质是一样的。
本质一样是否就能说明它们是等价的呢?或者说能否为了方便直接用+
来连接字符串,剩下的事就交给编译器了?继续看个例子,在这个例子中有个 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:按常量递增局部变量
以上代码等价于这样来写:
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);
}
}
- 每次循环中都需要new一个
StringBuilder
对象- 执行两次
append
方法- 转换成
String
对象- 让虚拟机栈中的局部变量
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