<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          教妹學(xué) Java 第 37 講:字符串拼接

          共 17168字,需瀏覽 35分鐘

           ·

          2021-06-13 02:14

          “哥,你讓我看的《Java 開發(fā)手冊》上有這么一段內(nèi)容:循環(huán)體內(nèi),拼接字符串最好使用 StringBuilder 的 append() 方法,而不是 + 號操作符。這是為什么呀?”三妹疑惑地問。

          “好的,三妹,哥來慢慢給你講。”我回答。

          三妹能在學(xué)習(xí)的過程中不斷地發(fā)現(xiàn)問題,讓我感到非常的開心。其實很多時候,我們不應(yīng)該只是把知識點記在心里,還應(yīng)該問一問自己,到底是為什么,只有邁出去這一步,才能真正的成長起來。

          “+ 號操作符其實被 Java 在編譯的時候重新解釋了,換一種說法就是,+ 號操作符是一種語法糖,讓字符串的拼接變得更簡便了。”一邊給三妹解釋,我一邊在 Intellij IDEA 中敲出了下面這段代碼。

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

          在 Java 8 的環(huán)境下,使用 javap -c Demo.class 反編譯字節(jié)碼后,可以看到以下內(nèi)容:

          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 關(guān)鍵字,并且 class 類型為 java/lang/StringBuilder。”我指著標(biāo)號為 9 的那行對三妹說,“這意味著新建了一個 StringBuilder 的對象。”

          “然后看標(biāo)號為 17 的這行,是一個 invokevirtual 指令,用于調(diào)用對象的方法,也就是 StringBuilder 對象的 append() 方法。”

          “也就意味著把 chenmo 這個字符串添加到 StringBuilder 對象中了。”

          “再往下看,標(biāo)號為 21 的這行,又調(diào)用了一次 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,情況發(fā)生了一些改變,同樣的代碼,字節(jié)碼指令完全不同了。”我說。

          同樣的代碼,在 Java 11 的環(huán)境下,字節(jié)碼指令是這樣的:

          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
          }

          看標(biāo)號為 29 的這行,字節(jié)碼指令為 invokedynamic,該指令允許由應(yīng)用級的代碼來決定方法解析,所謂的應(yīng)用級的代碼其實是一個方法——被稱為引導(dǎo)方法(Bootstrap Method),簡稱 BSM,BSM 會返回一個 CallSite(調(diào)用點) 對象,這個對象就和 invokedynamic 指令鏈接在一起。以后再執(zhí)行這條 invokedynamic 指令時就不會創(chuàng)建新的 CallSite 對象。CallSite 其實就是一個 MethodHandle(方法句柄)的 holder,指向一個調(diào)用點真正執(zhí)行的方法——此時就是 StringConcatFactory.makeConcatWithConstants() 方法。

          “哥,你別再說了,再說我就聽不懂了。”三妹打斷了我的話。

          “好吧,總之就是 Java 9 以后,JDK 用了另外一種方法來動態(tài)解釋 + 號操作符,具體的實現(xiàn)方式在字節(jié)碼指令層面已經(jīng)看不到了,所以我就以 Java 8 來繼續(xù)講解吧。”

          “再回到《Java 開發(fā)手冊》上的那段內(nèi)容:循環(huán)體內(nèi),拼接字符串最好使用 StringBuilder 的 append() 方法,而不是 + 號操作符。原因就在于循環(huán)體內(nèi)如果用 + 號操作符的話,就會產(chǎn)生大量的 StringBuilder 對象,不僅占用了更多的內(nèi)存空間,還會讓 Java 虛擬機(jī)不同的進(jìn)行垃圾回收,從而降低了程序的性能。”

          更好的寫法就是在循環(huán)的外部新建一個 StringBuilder 對象,然后使用 append() 方法將循環(huán)體內(nèi)的字符串添加進(jìn)來:

          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 循環(huán)中使用”+”號操作符。

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

          第二個,for 循環(huán)外部新建 StringBuilder,循環(huán)體內(nèi)使用 append() 方法。

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

          “這兩個小測試分別會耗時多長時間呢?三妹你來運(yùn)行下。”

          “哇,第一個小測試的執(zhí)行時間是 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,如果是,當(dāng)做字符串“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));
              }
          }

          由于字符串內(nèi)部是用數(shù)組實現(xiàn)的,所以需要先判斷拼接后的字符數(shù)組長度是否超過當(dāng)前數(shù)組的長度,如果超過,先對數(shù)組進(jìn)行擴(kuò)容,然后把原有的值復(fù)制到新的數(shù)組中。

          4)將拼接的字符串 str 復(fù)制到目標(biāo)數(shù)組 value 中。

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

          5)更新數(shù)組的長度 count。

          “說到 StringBuilder 就必須得提一嘴 StringBuffer,兩者就像是孿生雙胞胎,該有的都有,只不過大哥 StringBuffer 因為多呼吸兩口新鮮空氣,所以是線程安全的。”我說,“它里面的方法基本上都加了 synchronized 關(guān)鍵字來做同步。”

          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)將原字符串的字符數(shù)組 value 復(fù)制到變量 buf 數(shù)組中。

          3)把拼接的字符串 str 復(fù)制到字符數(shù)組 buf 中,并返回新的字符串對象。

          我一行一行地給三妹解釋著。

          “和 + 號操作符相比,concat() 方法在遇到字符串為 null 的時候,會拋出 NullPointerException,而“+”號操作符會把 null 當(dāng)做是“null”字符串來處理。”

          如果拼接的字符串是一個空字符串(""),那么 concat 的效率要更高一點,畢竟不需要 new StringBuilder 對象。

          如果拼接的字符串非常多,concat() 的效率就會下降,因為創(chuàng)建的字符串對象越來越多。

          “還有嗎?”三妹似乎對字符串拼接很感興趣。

          “有,當(dāng)然有。”

          String 類有一個靜態(tài)方法 join(),可以這樣來使用。

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

          第一個參數(shù)為字符串連接符,比如說:

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

          輸出結(jié)果為:王二-太特么-有趣了

          來看一下 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 循環(huán)把可變參數(shù)添加了進(jìn)來,最后調(diào)用 toString() 方法返回 String。

          “實際的工作中,org.apache.commons.lang3.StringUtilsjoin() 方法也經(jīng)常用來進(jìn)行字符串拼接。”

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

          該方法不用擔(dān)心 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();
          }

          內(nèi)部使用的仍然是 StringBuilder。

          “好了,三妹,關(guān)于字符串拼接的知識點我們就講到這吧。注意 Java 9 以后,對 + 號操作符的解釋和之前發(fā)生了變化,字節(jié)碼指令已經(jīng)不同了,等后面你學(xué)了字節(jié)碼指令后我們再詳細(xì)地講一次。”我說。

          “嗯,哥,你休息吧,我把這些例子再重新跑一遍。”三妹說。

          ---未完待續(xù),期待下集---

          點擊「閱讀原文」可直達(dá)《教妹學(xué)Java》專欄的在線閱讀地址!

          瀏覽 52
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久日网站 | 一级少妇A片在线观看浪莎八Av | 91精品国际 | 一级操逼网站 | 97caocao |