<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                今天我會聊聊日常使用的字符串,別看它似乎很簡單,但其實字符串幾乎在所有編程語言里都是個特殊的存在,因為不管是數量還是體積,字符串都是大多數應用中的重要組成。 今天我要問你的問題是,理解 Java 的字符串,String、StringBuffer、StringBuilder 有什么區別? ## 典型回答 String 是 Java 語言非常基礎和重要的類,提供了構造和管理字符串的各種基本邏輯。它是典型的 Immutable 類,被聲明成為 final class,所有屬性也都是 final 的。也由于它的不可變性,類似拼接、裁剪字符串等動作,都會產生新的 String 對象。由于字符串操作的普遍性,所以相關操作的效率往往對應用性能有明顯影響。 StringBuffer 是為解決上面提到拼接產生太多中間對象的問題而提供的一個類,我們可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本質是一個線程安全的可修改字符序列,它保證了線程安全,也隨之帶來了額外的性能開銷,所以除非有線程安全的需要,不然還是推薦使用它的后繼者,也就是 StringBuilder。 StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 沒有本質區別,但是它去掉了線程安全的部分,有效減小了開銷,是絕大部分情況下進行字符串拼接的首選。 ## 考點分析 幾乎所有的應用開發都離不開操作字符串,理解字符串的設計和實現以及相關工具如拼接類的使用,對寫出高質量代碼是非常有幫助的。關于這個問題,我前面的回答是一個通常的概要性回答,至少你要知道 String 是 Immutable 的,字符串操作不當可能會產生大量臨時字符串,以及線程安全方面的區別。 如果繼續深入,面試官可以從各種不同的角度考察,比如可以: * 通過 String 和相關類,考察基本的線程安全設計與實現,各種基礎編程實踐。 * 考察 JVM 對象緩存機制的理解以及如何良好地使用。 * 考察 JVM 優化 Java 代碼的一些技巧。 * String 相關類的演進,比如 Java 9 中實現的巨大變化。 * … 針對上面這幾方面,我會在知識擴展部分與你詳細聊聊。 ## 知識擴展 1\. 字符串設計和實現考量 我在前面介紹過,String 是 Immutable 類的典型實現,原生的保證了基礎線程安全,因為你無法對它內部數據進行任何修改,這種便利甚至體現在拷貝構造函數中,由于不可變,Immutable 對象在拷貝時不需要額外復制數據。 我們再來看看 StringBuffer 實現的一些細節,它的線程安全是通過把各種修改數據的方法都加上 synchronized 關鍵字實現的,非常直白。其實,這種簡單粗暴的實現方式,非常適合我們常見的線程安全類實現,不必糾結于 synchronized 性能之類的,有人說“過早優化是萬惡之源”,考慮可靠性、正確性和代碼可讀性才是大多數應用開發最重要的因素。 為了實現修改字符序列的目的,StringBuffer 和 StringBuilder 底層都是利用可修改的(char,JDK 9 以后是 byte)數組,二者都繼承了 AbstractStringBuilder,里面包含了基本操作,區別僅在于最終的方法是否加了 synchronized。 另外,這個內部數組應該創建成多大的呢?如果太小,拼接的時候可能要重新創建足夠大的數組;如果太大,又會浪費空間。目前的實現是,構建時初始字符串長度加 16(這意味著,如果沒有構建對象時輸入最初的字符串,那么初始值就是 16)。我們如果確定拼接會發生非常多次,而且大概是可預計的,那么就可以指定合適的大小,避免很多次擴容的開銷。擴容會產生多重開銷,因為要拋棄原有數組,創建新的(可以簡單認為是倍數)數組,還要進行 arraycopy。 前面我講的這些內容,在具體的代碼書寫中,應該如何選擇呢? 在沒有線程安全問題的情況下,全部拼接操作是應該都用 StringBuilder 實現嗎?畢竟這樣書寫的代碼,還是要多敲很多字的,可讀性也不理想,下面的對比非常明顯。 ~~~ String strByBuilder = new StringBuilder().append("aa").append("bb").append("cc").append ("dd").toString(); String strByConcat = "aa" + "bb" + "cc" + "dd"; ~~~ 其實,在通常情況下,沒有必要過于擔心,要相信 Java 還是非常智能的。 我們來做個實驗,把下面一段代碼,利用不同版本的 JDK 編譯,然后再反編譯,例如: ~~~ public class StringConcat { public static String concat(String str) { return str + “aa” + “bb”; } } ~~~ 先編譯再反編譯,比如使用不同版本的 JDK: ~~~ ${JAVA_HOME}/bin/javac StringConcat.java ${JAVA_HOME}/bin/javap -v StringConcat.class ~~~ JDK 8 的輸出片段是: ~~~ 0: new #2 // class java/lang/StringBuilder 3: dup 4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V 7: aload_0 8: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 11: ldc #5 // String aa 13: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 16: ldc #6 // String bb 18: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; ~~~ 而在 JDK 9 中,反編譯的結果就會有點特別了,片段是: ~~~ // concat method 1: invokedynamic #2, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String; // ... // 實際是利用了 MethodHandle, 統一了入口 0: #15 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; ~~~ 你可以看到,非靜態的拼接邏輯在 JDK 8 中會自動被 javac 轉換為 StringBuilder 操作;而在 JDK 9 里面,則是體現了思路的變化。Java 9 利用 InvokeDynamic,將字符串拼接的優化與 javac 生成的字節碼解耦,假設未來 JVM 增強相關運行時實現,將不需要依賴 javac 的任何修改。 在日常編程中,保證程序的可讀性、可維護性,往往比所謂的最優性能更重要,你可以根據實際需求酌情選擇具體的編碼方式。 2\. 字符串緩存 我們粗略統計過,把常見應用進行堆轉儲(Dump Heap),然后分析對象組成,會發現平均 25% 的對象是字符串,并且其中約半數是重復的。如果能避免創建重復字符串,可以有效降低內存消耗和對象創建開銷。 String 在 Java 6 以后提供了 intern() 方法,目的是提示 JVM 把相應字符串緩存起來,以備重復使用。在我們創建字符串對象并調用 intern() 方法的時候,如果已經有緩存的字符串,就會返回緩存里的實例,否則將其緩存起來。一般來說,JVM 會將所有的類似“abc”這樣的文本字符串,或者字符串常量之類緩存起來。 看起來很不錯是吧?但實際情況估計會讓你大跌眼鏡。一般使用 Java 6 這種歷史版本,并不推薦大量使用 intern,為什么呢?魔鬼存在于細節中,被緩存的字符串是存在所謂 PermGen 里的,也就是臭名昭著的“永久代”,這個空間是很有限的,也基本不會被 FullGC 之外的垃圾收集照顧到。所以,如果使用不當,OOM 就會光顧。 在后續版本中,這個緩存被放置在堆中,這樣就極大避免了永久代占滿的問題,甚至永久代在 JDK 8 中被 MetaSpace(元數據區)替代了。而且,默認緩存大小也在不斷地擴大中,從最初的 1009,到 7u40 以后被修改為 60013。你可以使用下面的參數直接打印具體數字,可以拿自己的 JDK 立刻試驗一下。 ~~~ -XX:+PrintStringTableStatistics ~~~ 你也可以使用下面的 JVM 參數手動調整大小,但是絕大部分情況下并不需要調整,除非你確定它的大小已經影響了操作效率。 ~~~ -XX:StringTableSize=N ~~~ Intern 是一種**顯式地排重機制**,但是它也有一定的副作用,因為需要開發者寫代碼時明確調用,一是不方便,每一個都顯式調用是非常麻煩的;另外就是我們很難保證效率,應用開發階段很難清楚地預計字符串的重復情況,有人認為這是一種污染代碼的實踐。 幸好在 Oracle JDK 8u20 之后,推出了一個新的特性,也就是 G1 GC 下的字符串排重。它是通過將相同數據的字符串指向同一份數據來做到的,是 JVM 底層的改變,并不需要 Java 類庫做什么修改。 注意這個功能目前是默認關閉的,你需要使用下面參數開啟,并且記得指定使用 G1 GC: ~~~ -XX:+UseStringDeduplication ~~~ 前面說到的幾個方面,只是 Java 底層對字符串各種優化的一角,在運行時,字符串的一些基礎操作會直接利用 JVM 內部的 Intrinsic 機制,往往運行的就是特殊優化的本地代碼,而根本就不是 Java 代碼生成的字節碼。Intrinsic 可以簡單理解為,是一種利用 native 方式 hard-coded 的邏輯,算是一種特別的內聯,很多優化還是需要直接使用特定的 CPU 指令,具體可以看相關[源碼](http://hg.openjdk.java.net/jdk/jdk/file/44b64fc0baa3/src/hotspot/share/classfile/vmSymbols.hpp),搜索“string”以查找相關 Intrinsic 定義。當然,你也可以在啟動實驗應用時,使用下面參數,了解 intrinsic 發生的狀態。 ~~~ -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining // 樣例輸出片段 180 3 3 java.lang.String::charAt (25 bytes) @ 1 java.lang.String::isLatin1 (19 bytes) ... @ 7 java.lang.StringUTF16::getChar (60 bytes) intrinsic ~~~ 可以看出,僅僅是字符串一個實現,就需要 Java 平臺工程師和科學家付出如此大且默默無聞的努力,我們得到的很多便利都是來源于此。 我會在專欄后面的 JVM 和性能等主題,詳細介紹 JVM 內部優化的一些方法,如果你有興趣可以再深入學習。即使你不做 JVM 開發或者暫時還沒有使用到特別的性能優化,這些知識也能幫助你增加技術深度。 3.String 自身的演化 如果你仔細觀察過 Java 的字符串,在歷史版本中,它是使用 char 數組來存數據的,這樣非常直接。但是 Java 中的 char 是兩個 bytes 大小,拉丁語系語言的字符,根本就不需要太寬的 char,這樣無區別的實現就造成了一定的浪費。密度是編程語言平臺永恒的話題,因為歸根結底絕大部分任務是要來操作數據的。 其實在 Java 6 的時候,Oracle JDK 就提供了壓縮字符串的特性,但是這個特性的實現并不是開源的,而且在實踐中也暴露出了一些問題,所以在最新的 JDK 版本中已經將它移除了。 在 Java 9 中,我們引入了 Compact Strings 的設計,對字符串進行了大刀闊斧的改進。將數據存儲方式從 char 數組,改變為一個 byte 數組加上一個標識編碼的所謂 coder,并且將相關字符串操作類都進行了修改。另外,所有相關的 Intrinsic 之類也都進行了重寫,以保證沒有任何性能損失。 雖然底層實現發生了這么大的改變,但是 Java 字符串的行為并沒有任何大的變化,所以這個特性對于絕大部分應用來說是透明的,絕大部分情況不需要修改已有代碼。 當然,在極端情況下,字符串也出現了一些能力退化,比如最大字符串的大小。你可以思考下,原來 char 數組的實現,字符串的最大長度就是數組本身的長度限制,但是替換成 byte 數組,同樣數組長度下,存儲能力是退化了一倍的!還好這是存在于理論中的極限,還沒有發現現實應用受此影響。 在通用的性能測試和產品實驗中,我們能非常明顯地看到緊湊字符串帶來的優勢,**即更小的內存占用、更快的操作速度**。 今天我從 String、StringBuffer 和 StringBuilder 的主要設計和實現特點開始,分析了字符串緩存的 intern 機制、非代碼侵入性的虛擬機層面排重、Java 9 中緊湊字符的改進,并且初步接觸了 JVM 的底層優化機制 intrinsic。從實踐的角度,不管是 Compact Strings 還是底層 intrinsic 優化,都說明了使用 Java 基礎類庫的優勢,它們往往能夠得到最大程度、最高質量的優化,而且只要升級 JDK 版本,就能零成本地享受這些益處。 ## 一課一練 關于今天我們討論的題目你做到心中有數了嗎?限于篇幅有限,還有很多字符相關的問題沒有來得及討論,比如編碼相關的問題。可以思考一下,很多字符串操作,比如 getBytes()/[String](https://docs.oracle.com/javase/9/docs/api/java/lang/String.html#String-byte:A-)?(byte\[\] bytes) 等都是隱含著使用平臺默認編碼,這是一種好的實踐嗎?是否有利于避免亂碼?
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看