1. 程式人生 > >Java總結 - String -> 這篇請使勁噴我

Java總結 - String -> 這篇請使勁噴我

  • 首先我要提前說明的一點是,這篇文章是我自己的理解,而且其中涉及了一些JVM指令,但是自己沒有學過這些東西,完全是靠自己的感覺在寫,所以我感覺本片文章會有些漏洞,因此您只可以做一個參考,我希望您發現不對的地方即使指正,非常感謝
  • 這篇是考慮再三冒死拿出來給大家看的,因為一直放在我的筆記對錯我自己完全不知道,所以孬活著不如快樂一死,接收噴,但請帶上您的理由,嘻嘻

String繼承關係

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence{
      ....
    }
  • 到這我們可以看到String類不可以被繼承,因為final修飾,所以他的方法自然不可以被重寫,然後String可以進行序列化,比較,以及他實現了CharSequence字元序列介面
  • 總結:String可序列化,可比較,emmm...是個字元序列

String的儲存實現

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence{
      //之前版本的JDK的String實現是char陣列,需要兩個位元組
      //而在之後更新的JDK中實現為byte陣列,加上一個下面的coder標誌來表示字元
      private final byte[] value;   
      //用於對value的byte進行編碼的編碼識別符號,即使用什麼編碼對value進行編碼
      //支援的編碼有LATIN1即ISO-8859-1單位元組編碼和UTF-16雙位元組編碼
      //如果value中儲存的字串都可以用LATIN1儲存,那麼coder=0,否則就使用UTF-16儲存,coder=1
      private final byte coder;
      //快取的hash值,預設為0
      private int hash;
      //數字代表編碼
      @Native static final byte LATIN1 = 0;
      @Native static final byte UTF16  = 1;
    }
  • String在JDK8中儲存的形式還是private final char value[]的,這個變化在JDK9中發生改變的,這個改變使字串能夠佔用更少的空間,因為原來實現的陣列是char,是2位元組長度,那麼在更改為byte陣列後,每個元素只有一個位元組的長度,所以節省了一半的空間(並不準確,但是肯定比之前的char節省,下面介紹)
  • 比如之前儲存how單詞,char[]陣列是這樣的

    [0][h][0][o][0][w]
    //之後byte單位元組儲存
    [h][o][w]
  • 如下圖是JDK8和JDK11中分別儲存how後的char[]陣列內的情況

markdown_img_paste_20190110120040637

markdown_img_paste_20190110120102427

  • 所以從上面看到,在儲存字母的時候,也就是單位元組可以存放一個字母的時候,儲存效率達到了最高,這時候就真的是之前char[]
    儲存的一半了,但是中國漢字不止佔一個位元組,即一個byte存不下一個漢字了,這時候還是需要2位元組去儲存的,比如JDK8和JDK11分別儲存期待a,如下圖

markdown_img_paste_20190110120251236

markdown_img_paste_20190110120417963

  • 如上圖,由於單位元組存不下漢字,所以coder編碼識別符號改為了1,即UTF-16
  • 對於coder可以這樣做一個實驗,如下

    String str = new String("xx");   //當你在構造器打斷點的時候,此時coder=0
    String str = new String("中國"); //coder=1
  • 所以從Java9 的 String 預設是使用了上述緊湊的空間佈局的
  • 這一改變,也直接影響了String.length()方法

    public int length() {
      //即如果是 LATIN-1 編碼,則右移0位.陣列長度即為字串長度.而如果是 UTF16 編碼,則右移1位,陣列長度的二分之一為字串長度
        return value.length >> coder();
    }
    byte coder() {
      //COMPACT_STRINGS預設為true
      //這個變數就代表了String一開始是否使用緊湊佈局,這個引數由JVM注入,只能通過虛擬機器引數更改
      //意思就是如果是緊湊佈局的話,那麼我們就使用coder作為返回值,coder會根據你存的string的內容變化
      //如果是False就是放棄緊湊佈局,那麼就是用雙位元組進行儲存內容
        return COMPACT_STRINGS ? coder : UTF16;
    }
  • 既然String子層儲存發生了變化,那麼相關的StringBuilderStringBuffer也發生了變化,如下是他們兩個類的父類

    abstract class AbstractStringBuilder implements Appendable, CharSequence {
        byte[] value;
        byte coder;
      }
  • 總結:String在JDK9之前使用char[]儲存資料,在JDK9開始使用byte[]儲存資料,並有一個coder標誌符,來表示資料是用哪一種編碼儲存的,以方便之後的方法進行區分對待,並且相關的String類都發生了改變

String初始化過程

  • new String(char[] ch)構造器開始

    //斷點程式碼
    public static void main(String[] args) {
        char[] chars = {'A', 'B'};
        String str = new String(chars);
        System.out.println(str);
    }
    //String構造器
    public String(char value[]) {
        this(value, 0, value.length, null);
    }
    //String包級別構造器
    String(char[] value, int off, int len, Void sig) {
      //這的註釋可以過一眼,等你看完下面的流程後,你就知道這是什麼作用了
        if (len == 0) {
            this.value = "".value;
            this.coder = "".coder;
            return;
        }
        //COMPACT_STRINGS預設為true,即代表啟用壓縮,即使用單位元組編碼
        if (COMPACT_STRINGS) {
          //compress裡面判斷如果char陣列存在 value > 0xFF 的值時,就返回null, 0xFF=255
          //如果內容全部小於0xFF,即代表可以全部採用單位元組編碼
          //那麼返回值就不是null,那麼直接賦值給String類中屬性就行了
            byte[] val = StringUTF16.compress(value, off, len);
            if (val != null) {
              //直接賦值給value屬性,單位元組編碼初始化完畢
                this.value = val;
                this.coder = LATIN1;
                return;
            }
        }
        //到這就代表上面遇到了不能直接單位元組編碼的String了,然後就開始採用雙位元組編碼
        this.coder = UTF16;
        //然後將要儲存的String用UTF16編碼即可,到這就初始化完畢
        this.value = StringUTF16.toBytes(value, off, len);
    }
    public static byte[] compress(char[] val, int off, int len) {
      //這個就是存放char轉到byte後的資料的臨時陣列
        byte[] ret = new byte[len];
        //內部呼叫,裡面判斷是否c>0xFF,如果都小於,就代表可以全部單位元組編碼,返回值就==len
        //如果遇到了c>0xFF的情況,那麼這個條件不會成立
        if (compress(val, off, ret, 0, len) == len) {
          //到這就代表已經儲存進了byte陣列內了,返回就可以
            return ret;
        }
        //這就代表需要儲存的String不能直接單位元組編碼
        return null;
    }
    public static int compress(char[] src, int srcOff, byte[] dst, int dstOff, int len) {
        for (int i = 0; i < len; i++) {
            char c = src[srcOff];
            //判斷char中的每個是否value > 0xFF
            if (c > 0xFF) {
              //如果發現c>0xFF那麼len賦值為0,跳出,所以len返回值也為0,所以會造成上層判斷為false
                len = 0;
                break;
            }
            //如果不遇到break,說明要儲存的String,可以直接用單位元組編碼,迴圈完成了,即char也儲存到了byte了
            dst[dstOff] = (byte)c;
            //指標++
            srcOff++;
            dstOff++;
        }
        return len;
    }
  • JDK8中的初始化

    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
  • 到這一個String的基本的構造過程就寫完了,這一部分掰扯了好半天,全是自己的理解,如果有分析的不對,請及時指正
  • 總結:已經拋棄了JDK8中的系統拷貝(2位元組),轉而使用字元編碼來區別初始化(1位元組 or 2位元組)

String中的常用方法實現

  • 使用方法就不多說了,來看一下他們的實現:substring,,replace

    //擷取字串
    public String substring(int beginIndex, int endIndex) {
         int length = length();
         //檢查是否越界
         checkBoundsBeginEnd(beginIndex, endIndex, length);
         //擷取的長度
         int subLen = endIndex - beginIndex;
         if (beginIndex == 0 && endIndex == length) {
             return this;
         }
         //根據編碼來區分擷取字元,注意他們的方法是!!newString!!,所以不用跟進去也知道他是建立一個新的子串
         return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen)
                           : StringUTF16.newString(value, beginIndex, subLen);
     }
    
    //替換字串
    public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
          //StringLatin1這裡面的方法有點長,如下
            String ret = isLatin1() ? StringLatin1.replace(value, oldChar, newChar)
                                    : StringUTF16.replace(value, oldChar, newChar);
            if (ret != null) {
                return ret;
            }
        }
        return this;
    }
    //因為return可以直接返回結果,所以我們直接看返回就好了,下面是精簡後的程式,詳細程式我看不懂..嘻嘻
    //看到都是newString返回的
    public static String replace(byte[] value, char oldChar, char newChar) {
        if (canEncode(oldChar)) {
            ...
            if (i < len) {
                ...
                    return new String(buf, LATIN1);
                } else {
                   ...
                    return new String(buf, UTF16);
                }
            }
        }
        return null; // for string to return this;
    }
  • 所以到這我們可以看到返回的是一個新的子串,而並非對原來的String做任何的改變,這也可以作為String是immutable的證據
  • 總結:String的任何操作都是返回一個新的子串,而並非對原來的String做任何修改,String物件一旦被建立就是固定不變的了,對String物件的任何改變都不影響到原物件,相關的任何change操作都會生成新的物件

常量池

  • 這部分內容我不知道怎麼去驗證,所以是參考網上的帖子,文末有引用說明
  • 字串在我們程式設計中使用是很多的,所以如果不引入一個機制,那麼除了不重複的字串快取外,重複的部分那麼將是無用的,所以這個機制就是常量池,在java中常量池可以保證池中一個字串僅且只有這一個,不會出現第二個備份,所以這也就很好的解決了重複字串的問題
  • 當我們建立一個字串的時候,Java會先去執行緒池中尋找,如果有就返回這個字串,否則就新建一個放入池中,當然這個操作是排除new String()操作的,僅支援直接可以能夠判斷出變數值的狀態,比如下面

    //可以直接得到變數的值
    String str1 = "1";
    String str2 = "1";
    System.out.println(str1 == str2);  //true
    //下面是不能直接得到值的,比如new String()
    String str1 = "1";
    String str2 = new String("1");
    System.out.println(str1 == str2); //false
  • 對於下面這種情況,剛開始還不理解為啥是true,感覺這個只能執行期才能獲取值啊,應該是false啊,但是卻不是,後來想了一下,因為getNum方法中是return "1";也相當於在建立物件,需要到池中搜索,結果發現有1,所以比較會返回true,這只是我的猜測,我還不知道怎麼去證實,如果不對請指正,謝謝
  • 補充:雖然getNum比較返回為true,但是隻是證明是常量池中的一個物件,而這個方法依舊是執行時才會知道其返回值的,即編譯器無法確定他的具體值

    public void test() {
        String str1 = "1";
        String str2 = getNum();
        System.out.println("1" == str1);  //true
        System.out.println("1" == str2);  //true
        System.out.println(str1 == str2);  //true
    }
    private String getNum(){
        return "1";  //我的證實是將這裡改為new String("1"),上面會返回false,所以得出上面的結論
    }
  • 到這就需要提到兩個概念:靜態常量池,動態常量池

    • 靜態常量池.即*.class檔案中的常量池,class檔案中的常量池不僅僅包含字串(數字)字面量,還包含類,方法的資訊,佔用class檔案絕大部分空間
    • 執行時常量池,則是jvm虛擬機器在完成類裝載操作後,將class檔案中的常量池載入到記憶體中,並儲存在方法區中,我們常說的常量池,就是指方法區中的執行時常量池
  • 提到上面兩個概念,可以解決這個問題:上面說建立字串時會在常量池中尋找,那麼new String("1")為啥不等於String str = "1"呢?

    • new操作其實是建立了一個真正的物件,這個我們都知道,所以這個new出來的物件1一定會在堆記憶體,我們之前也證實了不管從常用方法還是常量池機制都保證了不會有重複的字串,所以這的唯一可能就是new出來的物件是引用常量池中的物件的,如果常量池中沒有這個物件,new操作就會先在常量池中新建一個常量,然後再引用他,如下圖

markdown_img_paste_20190110201235483

  • 對應如下程式碼段
String str = new String("1");
String n = "1";
System.out.println(str == n);  //false
  • 到這就可以看出了,比較str = n,其一次指向完全不同,所以返回false
  • 那怎麼證明new String真的是建立了一個物件一個常量呢 ?(一個new 物件在堆,一個在常量池),我們使用到了javap -verbose 輸出附加資訊

    • 首先如下空實現
    public static void main(String[] args) {}
    • javap一下,只擷取有用的部分
    public class com.qidai.Tests
    //...
    Constant pool:    //常量池出現,其中沒有我們定義的字元,因為是空實現哈哈
       #1 = Methodref          #3.#17         // java/lang/Object."<init>":()V
       #2 = Class              #18            // com/qidai/Tests
       #3 = Class              #19            // java/lang/Object
       #4 = Utf8               <init>
       #5 = Utf8               ()V
       #6 = Utf8               Code
       #7 = Utf8               LineNumberTable
       #8 = Utf8               LocalVariableTable
       #9 = Utf8               this
      #10 = Utf8               Lcom/qidai/Tests;
      #11 = Utf8               main
      #12 = Utf8               ([Ljava/lang/String;)V
      #13 = Utf8               args
      #14 = Utf8               [Ljava/lang/String;
      #15 = Utf8               SourceFile
      #16 = Utf8               Tests.java
      #17 = NameAndType        #4:#5          // "<init>":()V
      #18 = Utf8               com/qidai/Tests
      #19 = Utf8               java/lang/Object
    {
      public com.qidai.Tests();
      //...
      public static void main(java.lang.String[]);  //main方法開始
        Code:
          stack=0, locals=1, args_size=1
             0: return   //無實現直接返回
    }
    • 然後我們在main中加入程式碼
    String string = new String("MyConstantString");
    • 編譯一下再javap看一下
    public class com.qidai.Tests
    Constant pool:
       #1 = Methodref          #6.#22         // java/lang/Object."<init>":()V
       #2 = Class              #23            // java/lang/String
       #3 = String             #24            // MyConstantString
       #4 = Methodref          #2.#25         // java/lang/String."<init>":(Ljava/lang/String;)V
       #5 = Class              #26            // com/qidai/Tests
       #6 = Class              #27            // java/lang/Object
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               LocalVariableTable
      #12 = Utf8               this
      #13 = Utf8               Lcom/qidai/Tests;
      #14 = Utf8               main
      #15 = Utf8               ([Ljava/lang/String;)V
      #16 = Utf8               args
      #17 = Utf8               [Ljava/lang/String;
      #18 = Utf8               string
      #19 = Utf8               Ljava/lang/String;
      #20 = Utf8               SourceFile
      #21 = Utf8               Tests.java
      #22 = NameAndType        #7:#8          // "<init>":()V
      #23 = Utf8               java/lang/String
      #24 = Utf8               MyConstantString      //!!!!!!!!!!!類檔案中出現了~~~~~
      #25 = NameAndType        #7:#28         // "<init>":(Ljava/lang/String;)V
      #26 = Utf8               com/qidai/Tests
      #27 = Utf8               java/lang/Object
      #28 = Utf8               (Ljava/lang/String;)V
    {
      public static void main(java.lang.String[]);
        Code:
          stack=3, locals=2, args_size=1  //因為有實現了,所以沒有直接返回
             0: new           #2                  // class java/lang/String
             3: dup
             4: ldc           #3                  // String MyConstantString !!!!!!!!!!!!
             6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
             9: astore_1
            10: return
    }
    • 果然證明了我們說的問題,就是new String()的時候會建立一個物件和一個常量,至此我們就算驗證結束了,但是我們還可以驗證一個其他的問題:不是說常量池不重複的嘛,那麼我們再定義一個一樣資料的String呢?,所以我們現在main方法中就有兩行內容了,如下
    String string = new String("MyConstantString");
    String constant = "MyConstantString";
    • 編譯此類然後javap檢視
    public class com.qidai.Tests
    Constant pool:
       #1 = Methodref          #6.#23         // java/lang/Object."<init>":()V
       #2 = Class              #24            // java/lang/String
       #3 = String             #25            // MyConstantString
       #4 = Methodref          #2.#26         // java/lang/String."<init>":(Ljava/lang/String;)V
       #5 = Class              #27            // com/qidai/Tests
       #6 = Class              #28            // java/lang/Object
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               LocalVariableTable
      #12 = Utf8               this
      #13 = Utf8               Lcom/qidai/Tests;
      #14 = Utf8               main
      #15 = Utf8               ([Ljava/lang/String;)V
      #16 = Utf8               args
      #17 = Utf8               [Ljava/lang/String;
      #18 = Utf8               string       //常量池中會加入Strig引用變數??
      #19 = Utf8               Ljava/lang/String;
      #20 = Utf8               constant     //常量池中會加入Strig引用變數??
      #21 = Utf8               SourceFile
      #22 = Utf8               Tests.java
      #23 = NameAndType        #7:#8          // "<init>":()V
      #24 = Utf8               java/lang/String
      #25 = Utf8               MyConstantString  //僅有一個~~~
      #26 = NameAndType        #7:#29         // "<init>":(Ljava/lang/String;)V
      #27 = Utf8               com/qidai/Tests
      #28 = Utf8               java/lang/Object
      #29 = Utf8               (Ljava/lang/String;)V
    {
      public com.qidai.Tests();
      public static void main(java.lang.String[]);
        Code:
          stack=3, locals=3, args_size=1
             0: new           #2                  // class java/lang/String
             3: dup
             4: ldc           #3                  // String MyConstantString
             6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V 初始化方法?
             9: astore_1
            10: ldc           #3                  // String MyConstantString  這應該就是咱們定義的constant了
            12: astore_2
            13: return
    }
    • 因為自己並沒有看過深入理解Java虛擬機器所以對上面也是一知半解,我們用javap證明了他確實會只儲存一個常量,但是我們看到常量池中會加入Strig引用變數??這個問題我還不知道怎麼回答,如果您知道此問題,請評論告訴我,謝謝,暫且記住常量池中會出現引用變數吧,畢竟他是真實存在的,但我們現在分析的只是class類檔案,而JVM中的動態常量池中應該會把他去掉,但這個全域性的常量池中應該會有一個與常量池儲存引用的一個機制,要麼怎麼找到常量池中的物件呢?我感覺這個在class檔案中的常量池中出現只是在描述這個類資訊,也就是說有點定義的意思,這是自己理解的,別信...我說真的...自己沒把握...
  • 好了知道了這些內容,我們還需要知道JVM在編譯的時候會進行編譯優化的,比如巨集變數的替換,比如上面的可確定的變數直接寫為確定值了,比如

    public static void main(String[] args) {
        String str = "1"+"2"+"3";
        String method = getNum();
    }
    private static String getNum() {return "1";}
    • 編譯檢視class檔案,IDEA就可以直接點選生成的class檔案進行檢視
    public static void main(String[] args) {
        String str = "123";  //直接替換為可確定值
        String method = getNum(); //這是不可確定的
    }
    private static String getNum() {return "1";}
  • 好了知道了會進行編譯優化的話,我們來看幾個例項

    public static void main(String[] args) {
        String s0= "helloworld"; //直接常量池 helloworld
        String s1= new String("helloworld"); //堆helloworld+引用常量池helloworld
        //javap看是hello和一個world常量
        String s2= "hello" + new String("world");
        System.out.println("===========test4============");
        //s0常量引用不等於s1的堆引用
        System.out.println( s0==s1 ); //false
        //s0的常量引用不等於s2的堆引用和常量引用
        System.out.println( s0==s2 ); //false
        //s1不等於s2,因為s2生成了兩個一個hello和一個world
        System.out.println( s1==s2 ); //false
    }
    • 如上一切都很正常,唯獨s2比較特殊,javap檢視constant pool中有三個常量:helloworld,world,hello\u0001,這個\u0001有人知道是什麼東西嗎??連線符?
  • 早期版本的常量池是放入永生代的,但是永生代是大小有限制的,所以在之後版本中將常量池放入了堆中,避免了永久代沾滿的問題,甚至永久代在JDK8中被替換為METASpace元資料區替代了
  • 總結:String的常量池保證只有一個唯一的字串,不會發生重複,並且newString操作是先去判斷常量池中是否有常量,有則引用,否則建立並且JVM會自動編譯優化,將可以直接確定下來的值替換掉原來的值

intern

  • 這個是一個可以擴充常量池內常量的個數的方法,即new String的時候,他會去建立一個常量在常量池,本文之前都是這麼說的,但是在網上的帖子中說到這個建立常量池的動作是lazy的,所以堆中有了物件而不一定常量池中也會有,即字串字面量會進入到當前類的執行時常量池,不會進入全域性的字串常量池,所以這個方法其實是觸發lazy機制,使其將資料放入常量池,下面有一篇美團的分享貼,可以看一下,自己不太理解就不多比比了
  • intern是顯示排重機制,但是每次呼叫就很麻煩,在jdk8u20推出了G1 GC下的字串排重,他是通過將相同資料的字串指向同一份資料來做到的,是JVM底層改變,並不涉及API的修改
  • G1 GC排重預設是關閉的,需要指定

    -XX:+UseStringDeduplication
  • 總結:可以擴充常量池內常量的個數,在Java8特定版本後,JVM就會幫我們做這件事

String,StringBuffer,StringBuilder

  • String是java語言非常基礎和重要的類,提供了構造和管理字串的各種基本邏輯,他是典型的immutable類,被宣告成為final class,所有屬性也都是final的,由於它的不可變現,類似拼接裁剪動作都會產生新的String物件
  • StringBuffer是解決拼接太多造成很多String物件的類,本質是一個執行緒安全的可修改字元序列,他保證了執行緒安全,但同時帶來了額外的效能開銷
  • StringBuilder是jdk5新增的,和StringBuffer類似,只是這個不是執行緒安全的
  • String是Immutable類的典型實現,他保證了執行緒安全,因為無法對內部資料進行更改
  • StringBuffer顯示的一些細節,他的執行緒安全是通過把各種修改資料的方法加上sync實現的,
  • 為了實現修改字元序列的目的,StringBuffer和StringBuilder底層都是利用可修改的陣列,二者都集成了AbstractStringBuilder,之間的區別僅僅是方法是否加了sync
  • 內部陣列的大小的實現是:構造時初始字串長度加16,所以可以根據自己的需要建立合適的大小
  • 在java8中字串的拼接操作會轉換為StringBuilder操作,而java9提供了StringConcatFactory,作為統一入口

有價值的參考貼