String 字符串的拼接
+ 号拼接
通过 +
号拼接是最常见的拼接方式了。
String jeremy = "Jeremy";
String tsai = "Tsai";
String jeremytsai = jeremy + tsai;
观察字节码
L0
LINENUMBER 12 L0
LDC "Jeremy"
ASTORE 1
L1
LINENUMBER 13 L1
LDC "Tsai"
ASTORE 2
L2
LINENUMBER 14 L2
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 3
我们不难发现, String str = a + b
被JDK编译器在编译字节码的时候帮我们优化成为了以下语句:
String jeremytsai = new StringBuilder().append(jeremy).append(tsai);
区别与C++的运算符重载,这仅仅是JDK内部优化的语法糖,Java本身没有运算符重载之说。但是要注意这种语法糖, 只对于同一行才有效,例如:
s = hello + world + jeremy + tsai;
注: 他们都是变量。
若将其拆分
s = hello;
s += world;
s += jeremy;
s += tsai;
查看字节码:
L5
LINENUMBER 15 L5
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 5
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 5
L6
LINENUMBER 16 L6
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 5
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 3
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 5
L7
LINENUMBER 17 L7
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 5
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 4
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 5
可以看到,每次拼接都会创建一个StringBuilder。所以说这只是语法糖,不能算符号重载,稍加不注意就行影响效率,因为我们日常编写不可能用+
号拼接很长的串,中间还有涉及业务逻辑。
String.concat(String str) 方法
concat方法是String给我们提供的拼接字符串的方法。
String jeremy = "Jeremy";
String tsai = "Tsai";
String jeremytsai = jeremy.concat(tsai);
源码描述如下:
将指定的字符串连接到该字符串的末尾。
如果参数字符串的长度为0,则返回此String对象。
否则,返回一个String对象,该对象表示一个字符序列,该字符序列是此String对象表示的字符序列与参数字符串表示的字符序列的串联。
源码
public String concat(String str) {
int otherLen = str.length(); // 获取参数字符串长度
if (otherLen == 0) {
return this; // 参数长度为0,返回自身
}
int len = value.length; //获取自身长度
char buf[] = Arrays.copyOf(value, len + otherLen); // 得到一个包含当前字符序列,长度为 // 两者之和的字符数组
str.getChars(buf, len); // 从当前字符序列长度开始,将参数的字符序列写入buf字符数组
return new String(buf, true); // 创建新的String对象并返回。
}
跟描述一样。
StringBuilder 拼接字符串
String jeremy = "jeremy";
String tsai = "tsai";
String jeremytsai = new StringBuilder().append(jeremy).append(tsai).toString();
查看字节码
L0
LINENUMBER 13 L0
LDC "jeremy"
ASTORE 1
L1
LINENUMBER 14 L1
LDC "tsai"
ASTORE 2
L2
LINENUMBER 15 L2
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 3
可以看出,与 +
号拼接一致。
String 字符串拼接效率对比
先知道一点,String在Java中是不可变对象,因此每次拼接都是生成新的String对象,为了解决频繁的内存开辟消耗资源,才有了StringBuilder
类。在+
拼接过程中,JDK默认优化成为StringBuilder以提高运行效率。
但是这里又出现了一个问题,当且仅有两个字符串拼接生成一个新的字符串,这个默认优化的优势就体现不出来了。因为本来只需要三份String空间,默认优化StringBuilder的情况下,还需要一份StringBuilder的空间,多开辟了一份空间,肯定会对性能有所影响,为了验证这一猜想,简单写了一个小程序测试.
// 定义要拼接的数组
String jeremy = "Jeremy";
String tsai = "Tsai";
// 然后分别记录各个拼接的耗时
String jeremytsai1 = jeremy + tsai;
String jeremytsai2 = jeremy.concat(tsai);
结果如我所料:
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的执行时间为:100纳秒 concat拼接 jeremytsai 的执行时间为:200纳秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的执行时间为:200纳秒 concat拼接 jeremytsai 的执行时间为:100纳秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的执行时间为:100纳秒 concat拼接 jeremytsai 的执行时间为:200纳秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的执行时间为:100纳秒 concat拼接 jeremytsai 的执行时间为:200纳秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的执行时间为:200纳秒 concat拼接 jeremytsai 的执行时间为:100纳秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的执行时间为:0纳秒 concat拼接 jeremytsai 的执行时间为:100纳秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的执行时间为:100纳秒 concat拼接 jeremytsai 的执行时间为:300纳秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的执行时间为:100纳秒 concat拼接 jeremytsai 的执行时间为:100纳秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的执行时间为:100纳秒 concat拼接 jeremytsai 的执行时间为:0纳秒
在100000次拼接JeremyTsai中,+/StringBuilder快的次数为: 29510,concat快的次数为:70490
循环拼接
循环拼接是一种特殊的拼接,其形式一般为:
String 结果;
for(循环条件) {
String 中间量;
// 计算
结果 += 中间量
}
在这种情况下,JDk的默认优化就显得很笨拙了,例如:
String jeremytsai = "";
for (int i = 0; i < 100; i++) {
jeremytsai += "JeremyTsai\n";
}
查看源码
L0
LINENUMBER 15 L0
LDC ""
ASTORE 1
L1
LINENUMBER 16 L1
ICONST_0
ISTORE 2
L2
FRAME APPEND [java/lang/String I]
ILOAD 2
BIPUSH 100
IF_ICMPGE L3
L4
LINENUMBER 17 L4
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
LDC "JeremyTsai\n"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ASTORE 1
L5
LINENUMBER 16 L5
IINC 2 1
GOTO L2
可以看出,+
号的默认优化使得每个循环体内部都要new一个新的StringBuilder进行拼接,这会大大降低性能。同理,concat也一样,每次拼接会生成新的String对象,会频繁开辟空间,效率不高。
故,在循环体中的字符串拼接推荐使用StringBuilder
StringBuilder jeremytsai = new StringBuilder();
for (int i = 0; i < 100; i++) {
jeremytsai.append("jeremy").append("tsai\n");
}
字符串拼接总结
在非循环体中的字符串拼接,若只是两个字符串拼接,推荐使用concat
。
多字符或循环体中拼接字符串优先使用StringBuilder
,提高效率,还能链式编程。不要过于依赖+
号拼接的语法糖,但是简单拼接还是推荐使用的。毕竟能省很多代码量。