String源碼分析

       String是我們其中用的最多的一個類,但是我們有很多細節我們可能沒有去深深去研究,這裏主要通過閱讀源碼去了解這個類。瞭解這個類我們分三個步驟去研究:

        1.String類信息,修飾的類關鍵字以及實現的接口,繼承的類,實現的接口實現了什麼樣的規範,父類主要完成什麼方法;     

        2.基本類屬性,包括靜態屬性,以及普通的屬性;

        3.方法,可以瞭解這個類的用法,實現避免一些不必要的坑。

1.類屬性

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

       首先我們看看到String類被final這個關鍵字修飾了,說明String是不可以繼承的。這個時候我們就會有疑問這裏爲什麼用final修飾,其實簡單來講就是兩點:安全和效率。安全,首先這個基本類提供給用戶使用,就是希望這個類不輕易的而改變,容易出現不同的問題,還有點String還有native方法,不暴露成員變量。final關鍵字詳解

        然後看這個String繼承的接口:java.io.Serializable, Comparable<String>, CharSequence。Serializable這個接口被繼承的類可以被序列化以及反序列化; CharSequence主要提供一些對字符串序列一些只讀的方法規範,例如,獲取字符串長度,以及索引值的字符等;Comparable<String>主要定義一個類的大小比較規範。我們可以大概看下實現方式:

 public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }

      我們可以熊源碼中總結以下結論:1.先按照字符串優先逐個比較字符大小,如果遇到不一樣的字符,就直接判斷兩個不同的字符,直到最小串結束;2.然後比較兩個字符串的長度

     2.成員屬性

     從源碼中我們可以看到四個屬性,兩個靜態屬性:serialVersionUID,serialPersistentFields ,這兩個屬性和繼承的Serializable的接口有關係,這個serialVersionUID主要用來在JVM會把傳來的字節流中的serialVersionUID與本地相應實體(類)的serialVersionUID進行比較,如果相同就認爲是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常(InvalidCastException)
 /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];
       還有兩個普通的成員hash以及private final char value[]。首先了解一下這個hash的使用,這個hash值的只有在兩個地方纔會初始化,一個是被其他的String構造出新的String時候;還有一個是在第一次調用hashCode方法的時候纔會“懶加載”生成。
 public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
       我們這裏有一個疑問就是爲什麼要以31作爲一個權值呢??詳情可以看下這個博客String爲啥用31作爲權值
       然後我們主要看下value這個值被final修飾,也就意味着一旦String對象生成,value指向就是不可變的,這裏的不可變不是絕對的不可變,我們可以通過反射修改內存數據:
        final String  a="ab";
        Field value = String.class.getDeclaredField("value");
        value.setAccessible(true);
        char[] o = (char[])value.get(a);
        o[0]='b';
        o[1]='A';
        System.out.println(a);

       除下使用反射去改變內存情況,,一個String生成就不會發生改變了,因此subString等一些寫操作基本都是new一個新的String對象(返回的String和this的一樣時返回本身),

       3.String內存分配

      其他的方法就不一一介紹,接下來介紹一下String和虛擬機的內存模型的關係。其實要你有了解過jvm(jdk1.7),jvm對String創建進行一個優化,就是使用字符串常量池,String使用一個字符串的時候,jvm首先會檢查運行時常量區,是否有String對象,如果就返回這個對象的引用,否則實例化並且把該String放入字符串常量區,結論:在字符串常量區,不可能出現兩份及以上的同樣的String實例。這樣有什麼好處呢?這樣的可以大大減少程序使用的內存。我們知道String是不變的(引用不可變),一份同樣的String沒有必要創建兩份,變這樣既減少了對象創建的時間以及使用的內存。接下來通過幾String創建分析內存的分佈。


public class Test {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String s="test";
        char test[]={'t','e','s','t'};
        String te="te";
        String st="st";
        String s1=new String(s);
        String s2="te"+"st";
        String s3="te"+new String("st");
        String s4=new String(test);
        String s5=te+st;
        if (s==s1){
            System.out.println("s==s1");
        }
        if (s==s2){
            System.out.println("s==s2");
        }
        if (s==s3){
            System.out.println("s==s3");
        }
        if (s==s4){
            System.out.println("s==s4");
        }
        if (s==s5){
            System.out.println("s==s5");
        }

        Field valueField = String.class.getDeclaredField("value");
        valueField.setAccessible(true);//查看value的分配方式
        char[] sValue = (char[]) valueField.get(s);
        char[] s1Value = (char[]) valueField.get(s1);
        char[] s2Value = (char[]) valueField.get(s2);
        char[] s3Value = (char[]) valueField.get(s3);
        char[] s4Value = (char[]) valueField.get(s4);
        char[] s5Value = (char[]) valueField.get(s5);
        if (sValue==s1Value){
            System.out.println("s1value is same");
        }
        if (sValue==s2Value){
            System.out.println("s2value is same");
        }
        if (sValue==s3Value){
            System.out.println("s3value is same");
        }
        if (sValue==s4Value){
            System.out.println("s4value is same");
        }
        if (sValue==s5Value){
            System.out.println("s5value is same");
        }
    }
}

  運行結果:

s==s2
s1value is same
s2value is same

        結果對比結果:s和s2指向同一個字符串常量,s,s1,s2的char value[]指向同一片內存地址;s1,s3,s4,s5都和s都引用不相等。      

       這裏我們就看String  這個“+”是怎麼起作用的,我們可以通過反編譯(jad -o -a -s d.java Test.class)得到我們想要的代碼(已經刪除字節碼註釋):

public class Test
{

    public Test()
    {
    }

    public static void main(String args[])
        throws NoSuchFieldException, IllegalAccessException
    {
        String s = "test";
        char test[] = {
            't', 'e', 's', 't'
        };
        String te = "te";
        String st = "st";
        String s1 = new String(s);
        String s2 = "test";
        String s3 = (new StringBuilder()).append("te").append(new String("st")).toString();
        String s4 = new String(test);
        String s5 = (new StringBuilder()).append(te).append(st).toString();
        if(s == s1)
            System.out.println("s==s1");
        if(s == s2)
            System.out.println("s==s2");
        if(s == s3)
            System.out.println("s==s3");
        if(s == s4)
            System.out.println("s==s4");
        if(s == s5)
            System.out.println("s==s5");
        Field valueField = java/lang/String.getDeclaredField("value");
        valueField.setAccessible(true);
        char sValue[] = (char[])(char[])valueField.get(s);
        char s1Value[] = (char[])(char[])valueField.get(s1);
        char s2Value[] = (char[])(char[])valueField.get(s2);
        char s3Value[] = (char[])(char[])valueField.get(s3);
        char s4Value[] = (char[])(char[])valueField.get(s4);
        char s5Value[] = (char[])(char[])valueField.get(s5);
        if(sValue == s1Value)
            System.out.println("s1value is same");
        if(sValue == s2Value)
            System.out.println("s2value is same");
        if(sValue == s3Value)
            System.out.println("s3value is same");
        if(sValue == s4Value)
            System.out.println("s4value is same");
        if(sValue == s5Value)
            System.out.println("s5value is same");
    }
}
       1. 1.s!=s1  s.value==s1.value

        s是常量區String對象的引用,s1是使用new在堆裏面生成的對象引用,所以s!=s1;而s.value==s1.value相等是因爲構造方法是一個淺拷貝:

public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

       因爲我們看String源代碼可以看到數據底層的value是指向同一個char[]實例的,相當於多實例出一個String對象,但是用了同一個底層數據,對資源是一種浪費,所以平時儘量減少這種方式使用

      2. 1.s!=s2  s.value==s2.value &s!=s5 s.value==s5.value&s!=s3 s.value==s3.value

       分析:我們看編譯之後字節碼就可以發現,在編譯期間jvm就可以直接優化s2直接引用常量區的字符串常量:

               String s2="te"+"st";優化爲String s2="test"

       而使用引用被使用"+"重載:

               String s3="te"+new String("st")

               String s3 = (new StringBuilder()).append("te").append(new String("st")).toString();


               String s5=te+st

               String s5 = (new StringBuilder()).append(te).append(st).toString();

       對於s3和s5在編譯期間不能不確定性,所以沒有被編譯器優化,所以使用的是StringBuilder,通過StringBuilder源碼可以發現toString方法也是直接在堆上直接創建一個新的String對象,所以導致String以及屬性value內存都不相等。

@Override //StringBuilder.toString()
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

     2. 1.s!=s4  s.value==s4.value

     這個看源碼就可以知道String對象以及velue內存是直接從堆裏面分配的

  public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
    public static char[] copyOf(char[] original, int newLength) {
        char[] copy = new char[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

       總結:由以上實驗我們可以看到,不同的String構造,內存分配地址是不同的。編譯期間確定的String是分配在常量區的,運行期間初始化是分配在堆上的,從char[]初始化以及+拼接生成的String對象以及內部value是分配至堆上的。字符串常量區有且僅有一份字符串,不會出現重複的字符串,但是堆上可能存在多份一樣的字符串。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章