访问量 ...
访客数 ...
总文章数 185 篇
博客已运行 1921 天

字符串拼接暗战

2025.03.18

五大拼接方案的技术对比

先说结论,如果在处理大量字符串的时候就需要注意性能和耗时,以下是耗时比较(短->长):StringBuilder<StringBuffer<StringUtils.join<concat<+

方法名称 特点 适用场景 注意事项
StringBuilder 非线程安全/动态扩容/内存预分配 循环体/高频拼接/超大文本处理 多线程场景禁用
StringBuffer 线程安全/同步锁/性能损耗 多线程并发拼接 非并发场景性能浪费
String.concat() 原生方法/仅限两个字符串连接 简单双字符串合并 连续调用产生中间对象
+操作符 语法糖/编译优化 静态拼接/少量动态拼接 循环内使用触发性能灾难
StringUtils.join 空值处理/依赖第三方库 复杂集合拼接/已有项目集成 引入额外Jar包依赖

因为String类是不可变的,所以所谓字符串拼接,本质都是重新生成一个新的字符串。以下是字符串的几种拼接的方式详细说明。

方案一:原生concat方法

使用concat方法连接字符串:

String str2 = "123".concat("456");
System.out.println(str2);

作为String类自带的拼接方法,concat()直接操作JVM底层字符数组实现两个字符串的合并。 其源码通过数组拷贝完成操作:

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) return this;
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

该方法适用于简单双字符串合并场景,但连续调用会产生中间对象,需谨慎处理高频拼接操作。

方案二:运算符"+“连接字符串

+运算符的语法糖特性使其在编译阶段触发智能优化。对于静态字符串拼接:

String s = "Java" + "实战";  // 编译优化为"Java实战"

编译器直接合并字符串常量。但当存在变量时:

String s1 = "Java";
String s2 = s1 + "实战";  // 转换为StringBuilder操作

此时编译器自动生成StringBuilder处理流程,但循环场景下会产生重复对象。

方案三:StringBuilder/StringBuffer连接字符串

StringBuilder(非线程安全)与StringBuffer(线程安全)采用动态数组策略,初始容量16并支持自动扩容(新容量=原容量*2+2)。两者的核心差异体现在同步机制。

StringBuffer关键方法实现:

public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

StringBuilder则无同步锁机制,这使得单线程场景下其性能比StringBuffer提升约15%-20%。

方案四:StringUtils.join连接字符串

Java8引入的String.join方法统一了集合与数组的拼接处理:

String[] arr = {"A", "B", "C"};
List<String> list = Arrays.asList(arr);
String s3 = String.join("|", arr);    // A|B|C
String s4 = String.join("-", list);   // A-B-C

该方法内部使用StringJoiner实现,适合处理预定义分隔符的集合拼接场景。

方案五:第三方工具扩展

例如,apache.commonsStringUtils是Apache Common Lang库中的一个工具类,提供了许多字符串处理的方法,包括join方法,用于将数组或集合以指定的分隔符拼接起来。

// Java8提供的String.join方法和`apache.commons`方法相似.
// 主要作用是将数组或集合以某拼接符拼接到一起形成新的字符串
String str9 = StringUtils.join(new String[]{str, "456", "789"});

该工具类具备空值处理能力,但需要引入外部依赖包。

底层机制深度解密

深入解析“+”拼接原理

拼接字符串最简单的方式就是直接使用符号”+“来拼接,其实“+”是Java提供的一个语法糖。 语法糖,也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。 语法糖让程序更加简洁,有更高的可读性。

为了探究“+”拼接的原理,用javap -verbose命令进行反编译看一下:

public static void main(String[] args) {
    String str1 = "123";
    String str2 = "456";
    String str3 = str1 + str2;
    System.out.println(str3);
}
// ...
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: ldc           #2                  // String 123
         2: astore_1
         3: ldc           #3                  // String 456
         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: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        28: aload_3
        29: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        32: return
      // ...

可以看到在字符串拼接过程中,是将String转成了StringBuilder后,使用append方法进行拼接字符串处理的,最后在去调用StringBuildertoString方法进行返回。 在JDK5之后,使用的是StringBuilder,在JDK5之前使用的是StringBuffer

知道了“+”底层之后,如果在循环中,使用“+”拼接字符串,会创建大量的StringBuilder对象,造成内存的浪费。 所以对于大量字符串拼接操作,推荐使用StringBuilderStringBuffer以提高性能。

// 不推荐
public class StringConcatExample {
    public static void main(String[] args) {
        String result = "";
        for (int i = 0; i < 10000; i++) {
            result += i;
        }
        System.out.println(result.length());
    }
}

// 推荐
public class StringBuilderExample {
  public static void main(String[] args) {
    StringBuilder result = new StringBuilder();
    for (int i = 0; i < 10000; i++) {
      result.append(i);
    }
    System.out.println(result.length());
  }
}

StringBuffer与StringBuilder深度解密

StringbuilderStringBuffer是字符串拼接的两种实现类,它们有相同的父类:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;

    // ...
}

StringBufferStringBuilder是 Java 提供的用于处理字符串的类,二者的主要区别在于线程安全性和性能方面。拿append方法举例:

  • StringBuilder.append方法
        @Override
        public StringBuilder append(String str) {
            super.append(str);
            return this;
        }
    
  • StringBuffer.append方法
      @Override
      public synchronized StringBuffer append(String str) {
          toStringCache = null;
          super.append(str);
          return this;
      }
    

由此可以看出StringBuilderStringBuffer原理是相似的,最大的区别就是StringBuffer是线程安全的,原因是用了synchronized修饰。 StringBuffer是线程安全的,因为它的方法都是同步的,这意味着它是安全的,可以在多线程环境中使用。 由于其线程安全性,每个方法调用都有同步开销,所以StringBuffer的性能比StringBuilder稍慢。

String类不同的是,StringBufferStringBuilder类的对象能够被多次的修改,并且不产生新的未使用对象。 如果你需要一个可修改的字符串,应该使用StringBuffer或者StringBuilder,但是会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的String对象被创建出来。

通过StringBuilderappend()方式添加字符串的效率,要远远高于String的字符串拼接方法。 在实际开发中还可以进行优化,StringBuilder的空参构造器,默认的字符串容量是16,如果需要存放的数据过多,容量就会进行扩容,我们可以设置默认初始化更大的长度,来减少扩容的次数。 如果我们能够确定,前前后后需要添加的字符串不高于某个限定值,那么建议使用构造器创建一个阈值的长度。