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,提高效率,还能链式编程。不要过于依赖+号拼接的语法糖,但是简单拼接还是推荐使用的。毕竟能省很多代码量。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐