常用的Java内部的字符串拼接方式大概有四种

  1. 使用运算符“+”
    1. 推荐使用场景:简单的字符串拼接
  2. 使用String类的concat方法
    1. 推荐使用场景:简单的字符串拼接
  3. StringBuilder类的append方法
    1. 推荐使用场景:不需要保证线程安全的场景,效率最高
  4. StringBuffer类的append方法
    1. 推荐使用场景:需要保证线程安全的场景

字符拼接效率:StringBuilder.append > StringBuffer.append > String.concat > 运算符“+”

运算符“+”拼接字符串实现

  • 本质:通过初始化一个新的StringBuilder对象,并且调用它的append方法来拼接多个字符串
  • 分析如下:

    1. /*
    2. 通过反编译下面的代码的字节码(javap -verbose YourClassName),可以了解到使用运算符“+”来拼接字符串的本质,是通过初始化一个新的StringBuilder对象,并且调用它的append方法,等价于:
    3. String str = "";
    4. str = (new StringBuilder()).append(str).append("a").toString();
    5. str = (new StringBuilder()).append(str).append("b").toString();
    6. str = (new StringBuilder()).append(str).append("c").append("d").toString();
    7. */
    8. String str = "";
    9. str = str + "a";
    10. str = str + "b";
    11. str = str + "c" + "d";
  • 优点:

    • 写法简洁,在简单的字符串拼接场景推荐直接使用
  • 缺点:

    • 每一行使用运算符“+”来拼接字符串,都会创建一个新的StringBuilder对象;若在循环中使用,占用更多内存的同时,效率还非常低

      String类的concat方法实现

  • 本质:初始化一个新的String类来存储拼接后的字符串(因为String类是以字符数组常量的方式存储它的值,因此拼接后的字符串就必须要初始化新的String来存储)

  • 源码分析如下

    1. /**
    2. * String类的concat方法源码分析:
    3. * 首先,每个String类存储字符串的方式是:以字符数组char[]的形式,存储在常量value中
    4. * 这里,我们假设原来的字符串存储常量为value1,待拼接字符串存储常量为value2
    5. * 在调用concat方法时,会根据旧字符数组(value1)和待拼接字符串(str)的长度之和创建一个新的char数组(假设为newChar1),并且将旧的字符数组(value1)拷贝到新的字符数组中(假设为newChar1)
    6. * 然后,再将待拼接字符串的存储常量(str的value2值)拷贝到字符数组newChar1后
    7. * 最后,根据拼接后的字符数组newChar1,初始化一个新的字符串返回
    8. */
    9. public String concat(String str) {
    10. int otherLen = str.length();
    11. if (otherLen == 0) {
    12. return this;
    13. }
    14. int len = value.length;
    15. char buf[] = Arrays.copyOf(value, len + otherLen);
    16. str.getChars(buf, len);
    17. return new String(buf, true);
    18. }
  • 优点:

    • 相对来说,写法也比较简洁,同样适用于简单的字符串拼接场景
    • 在简单的字符串拼接场景中,性能比使用运算符“+”号稍好一点
  • 缺点:
    • 每调用一次concat方法都会创建一个新的String对象;
    • 当在循环中使用时,占用更多内存的同时,效率还非常低;
      • 注意:
        • 当每次循环调用concat的次数较低时,效率比使用运算符“+”号稍好
        • 但当,每次循环调用concat的次数非常多时,效率不如使用运算符“+”号
          ```java // 可以尝试使用以下代码来校验运算符+号和调用concat的来比较其效率 long t1 = System.currentTimeMillis(); String str1 = “”; for (int i = 0; i < 10000; i++) { str1 = str1 + “a” + “b” + “c” + “d” + “e” + “f”; } long t2 = System.currentTimeMillis(); System.out.println(“使用加号拼接字符串效率: “ + (t2 - t1));

long t3 = System.currentTimeMillis(); String str2 = “”; for (int i = 0; i < 10000; i++) { str2 = str2.concat(“a”).concat(“b”).concat(“c”).concat(“e”).concat(“d”).concat(“f”); } long t4 = System.currentTimeMillis(); System.out.println(“使用concat拼接字符串效率: “ + (t4 - t3));

  1. <a name="IpZ0M"></a>
  2. ### StringBuilder类的append方法实现
  3. - 本质:StringBuilder类是继承了AbstractStringBuilder类,而StringBuilder的append正是调用了父类的append方法
  4. - 源码分析如下
  5. ```java
  6. /**
  7. * StringBuilder类的append方法
  8. * 它本质上是调用了父类实现的append方法,下面是父类的append具体实现
  9. */
  10. public StringBuilder append(String str) {
  11. super.append(str);
  12. return this;
  13. }
  14. /**
  15. * AbstractStringBuilder类的append方法源码分析:
  16. * 首先,AbstractStringBuilder类有两个参数:
  17. * - value参数:以字符数组char[]的形式存储字符串(变量)
  18. * - count参数:存储字符串的实际长度(注意,与value参数的长度区分开)
  19. * 其次,确定是否需要扩容旧的字符数组value的长度
  20. * - 当拼接后的字符串长度要大于value的长度时,默认扩容长度为value长度 * 2 + 2,否则为拼接后字符串的长度
  21. * 然后,判断是否需要扩容,则初始化一个新的字符数组(长度为第二步获取)newValue,将旧的value的值拷贝到新的字符数组newValue中,并重新为旧的value赋值(value = newValue;)
  22. * 最后,将待拼接的字符串的字符数组值,拷贝到value参数中
  23. */
  24. public AbstractStringBuilder append(String str) {
  25. if (str == null)
  26. return appendNull();
  27. int len = str.length();
  28. ensureCapacityInternal(count + len);
  29. str.getChars(0, len, value, count);
  30. count += len;
  31. return this;
  32. }
  33. /**
  34. * AbstractStringBuilder类的扩容源码
  35. */
  36. private void ensureCapacityInternal(int minimumCapacity) {
  37. // overflow-conscious code
  38. if (minimumCapacity - value.length > 0) {
  39. value = Arrays.copyOf(value,
  40. newCapacity(minimumCapacity));
  41. }
  42. }
  43. /**
  44. * 获取新字符数组扩容后长度源码分析:
  45. * 在做扩容时,默认新的字符数组长度为:旧的字符数组的值的长度 * 2 + 2
  46. * 这么做的理由主要目的是:减少后续的扩容次数
  47. */
  48. private int newCapacity(int minCapacity) {
  49. // overflow-conscious code
  50. int newCapacity = (value.length << 1) + 2;
  51. if (newCapacity - minCapacity < 0) {
  52. newCapacity = minCapacity;
  53. }
  54. return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
  55. ? hugeCapacity(minCapacity)
  56. : newCapacity;
  57. }
  • 优点(其实是父类AbstractStringBuilder的优点)
    • 通过校验扩容、字符数组扩容、拷贝等方法,极大地提高了字符串拼接效率
  • 缺点
    • 完全是调用了父类的方法,方法本身并没有保证线程不安全
    • 在简单的字符串拼接场景写法略显复杂

StringBuffer类的append方法实现

  • 实现方式:
    • StringBuffer类也继承了AbstractStringBuilder类,而StringBuffer的append同样调用了父类的append方法
    • 与StringBuilder的差别是,它的append方法使用了synchronized进行声明,说明它是一个线程安全的方法
  • 源码如下

    1. /**
    2. * StringBuffer类的append方法源码
    3. */
    4. public synchronized StringBuffer append(String str) {
    5. toStringCache = null;
    6. super.append(str);
    7. return this;
    8. }
  • 优点:

    • 通过校验扩容、字符数组扩容、拷贝等方法,极大地提高了字符串拼接效率(其实是父类AbstractStringBuilder的优点)
    • 保证了线程安全
  • 缺点:
    • 因为方法本身保证了线程安全,因此字符拼接效率,相比于StringBuilder稍慢
    • 在简单的字符串拼接场景写法略显复杂

参考