首页 > 编程语言 >Java字符串拼接

Java字符串拼接

时间:2023-02-19 13:59:03浏览次数:47  
标签:lang Java String StringBuilder 拼接 字符串 java append

“哥,你让我看的《Java 开发手册open in new window》上有这么一段内容:循环体内,拼接字符串最好使用 StringBuilder 的 append() 方法,而不是 + 号操作符。这是为什么呀?”三妹疑惑地问。

“+ 号操作符其实被 Java 在编译的时候重新解释了,换一种说法就是,+ 号操作符是一种语法糖,让字符串的拼接变得更简便了。”一边给三妹解释,我一边在 Intellij IDEA 中敲出了下面这段代码。

class Demo {
   public static void main(String[] args) {
       String chenmo = "沉默";
       String wanger = "王二";
       System.out.println(chenmo + wanger);
  }
}

在 Java 8 的环境下,使用 javap -c Demo.class 反编译字节码后,可以看到以下内容:

Compiled from "Demo.java"
class Demo {
 Demo();
   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 沉默
      2: astore_1
      3: ldc           #3                  // String 王二
      5: astore_2
      6: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      9: new           #5                  // class java/lang/StringBuilder
     12: dup
     13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
     16: aload_1
     17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     20: aload_2
     21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
     27: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     30: return
}

“你看,三妹,这里有一个 new 关键字,并且 class 类型为 java/lang/StringBuilder。”我指着标号为 9 的那行对三妹说,“这意味着新建了一个 StringBuilder 的对象。”

“然后看标号为 17 的这行,是一个 invokevirtual 指令,用于调用对象的方法,也就是 StringBuilder 对象的 append() 方法。”

“也就意味着把 chenmo 这个字符串添加到 StringBuilder 对象中了。”

“再往下看,标号为 21 的这行,又调用了一次 append() 方法,意味着把 wanger 这个字符串添加到 StringBuilder 对象中了。”

换成 Java 代码来表示的话,大概是这个样子:

class Demo {
   public static void main(String[] args) {
       String chenmo = "沉默";
       String wanger = "王二";
       System.out.println((new StringBuilder(String.valueOf(chenmo))).append(wanger).toString());
  }
}

“哦,原来编译的时候把“+”号操作符替换成了 StringBuilder 的 append() 方法啊。”三妹恍然大悟。

“是的,不过到了 Java 9,情况发生了一些改变,同样的代码,字节码指令完全不同了。”我说。

同样的代码,在 Java 11 的环境下,字节码指令是这样的:

Compiled from "Demo.java"
public class com.itwanger.thirtyseven.Demo {
 public com.itwanger.thirtyseven.Demo();
   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
      2: astore_1
      3: iconst_0
      4: istore_2
      5: iload_2
      6: bipush        10
      8: if_icmpge     41
     11: new           #3                  // class java/lang/String
     14: dup
     15: ldc           #4                  // String 沉默
     17: invokespecial #5                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
     20: astore_3
     21: ldc           #6                  // String 王二
     23: astore        4
     25: aload_1
     26: aload_3
     27: aload         4
     29: invokedynamic #7,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
     34: astore_1
     35: iinc          2, 1
     38: goto          5
     41: return
}

看标号为 29 的这行,字节码指令为 invokedynamic,该指令允许由应用级的代码来决定方法解析,所谓的应用级的代码其实是一个方法——被称为引导方法(Bootstrap Method),简称 BSM,BSM 会返回一个 CallSite(调用点) 对象,这个对象就和 invokedynamic 指令链接在一起。以后再执行这条 invokedynamic 指令时就不会创建新的 CallSite 对象。CallSite 其实就是一个 MethodHandle(方法句柄)的 holder,指向一个调用点真正执行的方法——此时就是 StringConcatFactory.makeConcatWithConstants() 方法。

“哥,你别再说了,再说我就听不懂了。”三妹打断了我的话。

“好吧,总之就是 Java 9 以后,JDK 用了另外一种方法来动态解释 + 号操作符,具体的实现方式在字节码指令层面已经看不到了,所以我就以 Java 8 来继续讲解吧。”

“再回到《Java 开发手册》上的那段内容:循环体内,拼接字符串最好使用 StringBuilder 的 append() 方法,而不是 + 号操作符。原因就在于循环体内如果用 + 号操作符的话,就会产生大量的 StringBuilder 对象,不仅占用了更多的内存空间,还会让 Java 虚拟机不停的进行垃圾回收,从而降低了程序的性能。”

更好的写法就是在循环的外部新建一个 StringBuilder 对象,然后使用 append() 方法将循环体内的字符串添加进来:

class Demo {
   public static void main(String[] args) {
       StringBuilder sb = new StringBuilder();
       for (int i = 1; i < 10; i++) {
           String chenmo = "沉默";
           String wanger = "王二";
           sb.append(chenmo);
           sb.append(wanger);
      }
       System.out.println(sb);
  }
}

 

来做个小测试。

第一个,for 循环中使用”+”号操作符。

String result = "";
for (int i = 0; i < 100000; i++) {
   result += "六六六";
}

第二个,for 循环外部新建 StringBuilder,循环体内使用 append() 方法。

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) {
   sb.append("六六六");
}

“这两个小测试分别会耗时多长时间呢?三妹你来运行下。”

“哇,第一个小测试的执行时间是 6212 毫秒,第二个只用了不到 1 毫秒,差距也太大了吧!”三妹说。

“是的,这下明白了原因吧?”我说。

“是的,哥,原来如此。”

“好了,三妹,来看一下 StringBuilder 类的 append() 方法的源码吧!”

public StringBuilder append(String str) {
   super.append(str);
   return this;
}

这 3 行代码其实没啥看的。我们来看父类 AbstractStringBuilder 的 append() 方法:

public AbstractStringBuilder append(String str) {
   if (str == null)
       return appendNull();
   int len = str.length();
   ensureCapacityInternal(count + len);
   str.getChars(0, len, value, count);
   count += len;
   return this;
}

1)判断拼接的字符串是不是 null,如果是,当做字符串“null”来处理。appendNull() 方法的源码如下:

private AbstractStringBuilder appendNull() {
   int c = count;
   ensureCapacityInternal(c + 4);
   final char[] value = this.value;
   value[c++] = 'n';
   value[c++] = 'u';
   value[c++] = 'l';
   value[c++] = 'l';
   count = c;
   return this;
}

2)获取字符串的长度。

3)ensureCapacityInternal() 方法的源码如下:

private void ensureCapacityInternal(int minimumCapacity) {
   // overflow-conscious code
   if (minimumCapacity - value.length > 0) {
       value = Arrays.copyOf(value,
               newCapacity(minimumCapacity));
  }
}

由于字符串内部是用数组实现的,所以需要先判断拼接后的字符数组长度是否超过当前数组的长度,如果超过,先对数组进行扩容,然后把原有的值复制到新的数组中。

4)将拼接的字符串 str 复制到目标数组 value 中。

str.getChars(0, len, value, count)

5)更新数组的长度 count。

“说到 StringBuilder 就必须得提一嘴 StringBuffer,两者就像是孪生双胞胎,该有的都有,只不过大哥 StringBuffer 因为多呼吸两口新鲜空气,所以是线程安全的。”我说,“它里面的方法基本上都加了 synchronized 关键字来做同步。”

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

“除了可以使用 + 号操作符,StringBuilder 和 StringBuilder 的 append() 方法,还有其他的字符串拼接方法吗?”三妹问。

“有啊,比如说 String 类的 concat() 方法,有点像 StringBuilder 类的 append() 方法。”

String chenmo = "沉默";
String wanger = "王二";
System.out.println(chenmo.concat(wanger));

可以来看一下 concat() 方法的源码。

 

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);
}

1)如果拼接的字符串的长度为 0,那么返回拼接前的字符串。

2)将原字符串的字符数组 value 复制到变量 buf 数组中。

3)把拼接的字符串 str 复制到字符数组 buf 中,并返回新的字符串对象。

“和 + 号操作符相比,concat() 方法在遇到字符串为 null 的时候,会抛出 NullPointerException,而“+”号操作符会把 null 当做是“null”字符串来处理。”

如果拼接的字符串是一个空字符串(""),那么 concat 的效率要更高一点,毕竟不需要 new StringBuilder 对象。

如果拼接的字符串非常多,concat() 的效率就会下降,因为创建的字符串对象越来越多。

String 类有一个静态方法 join(),可以这样来使用。

String chenmo = "沉默";
String wanger = "王二";
String cmower = String.join("", chenmo, wanger);
System.out.println(cmower);

第一个参数为字符串连接符,比如说:

String message = String.join("-", "王二", "太特么", "有趣了");

输出结果为:王二-太特么-有趣了

来看一下 join 方法的源码:

public static String join(CharSequence delimiter, CharSequence... elements) {
   Objects.requireNonNull(delimiter);
   Objects.requireNonNull(elements);
   // Number of elements not likely worth Arrays.stream overhead.
   StringJoiner joiner = new StringJoiner(delimiter);
   for (CharSequence cs: elements) {
       joiner.add(cs);
  }
   return joiner.toString();
}

里面新建了一个叫 StringJoiner 的对象,然后通过 for-each 循环把可变参数添加了进来,最后调用 toString() 方法返回 String。

“实际的工作中,org.apache.commons.lang3.StringUtilsjoin() 方法也经常用来进行字符串拼接。”

String chenmo = "沉默";
String wanger = "王二";
StringUtils.join(chenmo, wanger);

该方法不用担心 NullPointerException。

StringUtils.join(null)            = null
StringUtils.join([])              = ""
StringUtils.join([null])          = ""
StringUtils.join(["a", "b", "c"]) = "abc"
StringUtils.join([null, "", "a"]) = "a"

来看一下源码:

public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) {
   if (array == null) {
       return null;
  }
   if (separator == null) {
       separator = EMPTY;
  }

   final StringBuilder buf = new StringBuilder(noOfItems * 16);

   for (int i = startIndex; i < endIndex; i++) {
       if (i > startIndex) {
           buf.append(separator);
      }
       if (array[i] != null) {
           buf.append(array[i]);
      }
  }
   return buf.toString();
}

内部使用的仍然是 StringBuilder。

“好了,三妹,关于字符串拼接的知识点我们就讲到这吧。注意 Java 9 以后,对 + 号操作符的解释和之前发生了变化,字节码指令已经不同了,等后面你学了字节码指令后我们再详细地讲一次。”我说。

标签:lang,Java,String,StringBuilder,拼接,字符串,java,append
From: https://www.cnblogs.com/chaosssock/p/17134637.html

相关文章

  • Java字符串分割
    “哥,我感觉字符串拆分没什么可讲的呀,直接上String类的split()方法不就可以了!”三妹毫不客气地说。“假如你真的这么觉得,那可要注意了,事情远没这么简单。”我微笑着说......
  • 漏洞分析-log4j RCE-JAVA篇
    0x00原理分析log4j的介绍:log4j是java打印输出日志的一个API,只要引入了log4j的jar包或者是在xml配置文件内配置好log4j即可输入java运行时产生的日志内容,一般用于记录网......
  • 【LeeCode】剑指 Offer 58 - II. 左旋转字符串
    【题目描述】字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回......
  • Java入门博客
    开始准备 新建一个file->new->project->emptyproject   新建一个file->new->module->java 打开projectstructure,修改这两处   修改注释的颜色 ......
  • Java基础知识点(带返回值方法的定义和调用
    一:带返回值方法的定义方法的返回值其实就是方法运行的最终结果。如果要在调用处根据方法的结果,去编写另外一段逻辑,为了在调用处拿到方法的结果,就需要定义带返回值的方法。eg......
  • LeetCode-53. 最大子数组和(Java)
    一、前言:......
  • Java:使用thumbnailator实现图片压缩处理
    thumbnailator可以实现图片的压缩、旋转、添加水印文档https://github.com/coobird/thumbnailatorhttps://github.com/coobird/thumbnailator/wiki/Examples依赖<!--......
  • Java内存
    系统中的堆、栈和数据结构堆、栈系统中的堆、栈和数据结构堆、栈不是一个概念。可以说系统中的堆、栈是真实的内存物理区,数据结构中的堆、栈是抽象的数据存储结构。数......
  • java序列化反序列化
    序列化概述序列化:将数据结构或对象转换成二进制字节流的过程反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程TCP/IP四层模型transient......
  • 剑指 Offer 34. 二叉树中和为某一值的路径(java解题)
    目录1.题目2.解题思路3.数据类型功能函数总结4.java代码1.题目给你二叉树的根节点root和一个整数目标和targetSum,找出所有从根节点到叶子节点路径总和等于给......