java基礎(chǔ)鞏固-淺析String源碼及其不可變性

字符串可以說是廣泛應(yīng)用在日常編程中,jdk從1.0就提供了String類來(lái)創(chuàng)建和操作字符串。同時(shí)它也是不可改變類(基本類型的包裝類都不可改變)的典型代表。

源碼查看(基于1.8)

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[]; //這邊只是引用并不是真正對(duì)象
    ...
}
//首先string類創(chuàng)建的對(duì)象是不可變的(一個(gè)對(duì)象在創(chuàng)建完成后不能再改變它狀態(tài),說明是不可變的,并發(fā)程序最喜歡不可變量了),
//里面最主要的成員為char類型的數(shù)組

幾個(gè)構(gòu)造方法

//空的構(gòu)造方法 例如 String a = new String(); a為""空字符
public String() {
    this.value = new char[0];
}

//帶參構(gòu)造方法 將源的hash和value賦給目標(biāo)String
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

幾個(gè)常用經(jīng)典的String類方法

1.equals
    //如果引用指向的內(nèi)存值都相等 直接返回true
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        //instanceof判斷是否屬于或子類 但Stringfinal修飾不可繼承 只考慮是否為String類型
        //上面說過String的成員為char數(shù)組,equals內(nèi)則比較char類數(shù)組元素是否一一相等 
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            //長(zhǎng)度不相等返回false
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                //從后往前單個(gè)字符判斷,如果有不相等,返回false
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

2.subString(beginIndex,endIndex)

subString這邊存在一個(gè)小插曲

在jdk1.7以前,該方法存在內(nèi)存泄漏問題。之所以存在是因?yàn)樵诖酥?,String三個(gè)參數(shù)的構(gòu)造方法是這么寫的。成員變量為這三個(gè),jdk7以后取消掉了offset和count加入了hash,雖然原來(lái)的構(gòu)造方法簡(jiǎn)潔高效但存在gc問題。所以7以后放棄了性能采取了更為保守的寫法。

    /** The value is used for character storage. */
    private final char value[];

    /** The offset is the first index of the storage that is used. */
    private final int offset;

    /** The count is the number of characters in the String. */
    private final int count;
   ...
   public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > count) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    if (beginIndex > endIndex) {
        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
    }
 //雖然這邊返回的是新的String對(duì)象,但構(gòu)造方法中還引用著原先的value
    return ((beginIndex == 0) && (endIndex == count)) ? this :
        new String(offset + beginIndex, endIndex - beginIndex, value);
    }
   ...
   String(int offset, int count, char value[]) {  
     this.value = value;  
     this.offset = offset;  
     this.count = count;  
    }  
//這邊開始this.value = value; 出現(xiàn)問題,這三個(gè)個(gè)原來(lái)為String類中的三個(gè)私有成員變量,因?yàn)檫@種實(shí)現(xiàn)還在引用原先的字符串變量value[] 通過offset(起始位置)和count(字符所占個(gè)數(shù))返回一個(gè)新的字符串,這樣可能導(dǎo)致jvm認(rèn)為最初被截取的字符串還被引用就不對(duì)其gc,如果這個(gè)原始字符串很大,就會(huì)占用著內(nèi)存,出現(xiàn)內(nèi)存泄漏等gc問題。

jdk1.7以后的寫法變化

//雖然這邊還是有offse和count參數(shù) 但不是成員變量了
private final char value[];

/** Cache the hash code for the string */
private int hash; // Default to 0
...
public String substring(int beginIndex, int endIndex) {
      if (beginIndex < 0) {
          throw new StringIndexOutOfBoundsException(beginIndex);
      }
      if (endIndex > value.length) {
          throw new StringIndexOutOfBoundsException(endIndex);
      }
      int subLen = endIndex - beginIndex; 
      if (subLen < 0) {
          throw new StringIndexOutOfBoundsException(subLen);
      }
      return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);//構(gòu)造函數(shù)參數(shù)順序也有所變化
}
...
public String(char value[], int offset, int count) {
  if (offset < 0) {
    throw new StringIndexOutOfBoundsException(offset);
  }
  if (count < 0) {
    throw new StringIndexOutOfBoundsException(count);
  }
  // Note: offset or count might be near -1>>>1.
  if (offset > value.length - count) {
    throw new StringIndexOutOfBoundsException(offset + count);
  }
    //新的不是引用之前的而是重新新建了一個(gè)。
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}

3.hashcode
public int hashCode() {
    int h = hash;
    //如果hash沒有被計(jì)算過,并且字符串不為空,則進(jìn)行hashCode計(jì)算
    if (h == 0 && value.length > 0) {
        char val[] = value;
        //計(jì)算過程
        //s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        //hash賦值
        hash = h;
    }
    return h;
}
//String重寫了Object類的hashcode方法,根據(jù)值來(lái)計(jì)算hashcode值,不過Object的該方法為native本地方法。
//該方法設(shè)計(jì)十分巧妙,它先從0判斷字符大小,如果
//hashcode其實(shí)就是散列碼 這種算法的話 同樣的字符串的值一定相等 但不同的字符串其實(shí)也有可能得到同樣的hashcode值 
n=3
i=0 -> h = 31 * 0 + val[0]  
i=1 -> h = 31 * (31 * 0 + val[0]) + val[1]
i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2]
//以字符串 "123"為例 1的的ascil碼為49 所以 "1".hashcode()的值為49,2為50...
h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2] = 31 * (31 * 49 + 50) + 51 = 48690

詳細(xì)算法參考這里

String的不可變性

//這邊可能存在一個(gè)疑問 s對(duì)象是否發(fā)生了改變
String s = "hello";
s = "world";
//String不可變不是在原內(nèi)存地址上修改數(shù)據(jù),而是重新指向一個(gè)新對(duì)象,新地址,所以這里的hello對(duì)象并沒有被改變。
//同樣的類似replace方法源碼中
  public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);//這邊返回的是新的一個(gè)String對(duì)象 而不改變?cè)瓉?lái)的對(duì)象
            }
        }
        return this; //如果都一樣 就指向同一個(gè)對(duì)象了
    }
String對(duì)象不可變

String對(duì)象為什么不可變

String用final修飾是為了防止被繼承進(jìn)而破壞,而讓String對(duì)象不可變主要也是為了安全。

這里原來(lái)我有一個(gè)誤區(qū),僅僅認(rèn)為說final修飾了String類,所以String對(duì)象不可變,想法很天真,,final修飾類的話主要是讓類不可被繼承,final修飾基本類型變量不能對(duì)基本類型對(duì)象重新賦值,但對(duì)于引用類型的變量(如數(shù)組),它保存的只是一個(gè)引用,final只需保證這個(gè)引用的地址不改變,即一直引用同一個(gè)對(duì)象即可,但是這個(gè)對(duì)象還是能改變的。

比如 final int[] arr = {1};
arr[0] = 3; //改變

arr = new int[]{3}; //因?yàn)檫@個(gè)是個(gè)新的數(shù)組 這樣改變了數(shù)組的地址 編譯不通過 除非去掉final

//String類源碼中的成員變量 (jdk1.8)
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
String.png

思考

1.String對(duì)象是否真的不可變

可以通過反射改變
String的成員變量為final修飾,就是初始化之后不可改變,但是這幾個(gè)成員中value比較特殊,因?yàn)樗莻€(gè)引用變量而不是真正的對(duì)象,value[]是final修飾的,也就是說不能再指向其他數(shù)組對(duì)象,但是可以改變數(shù)組內(nèi)部的結(jié)構(gòu)來(lái)改變。
注:簡(jiǎn)單來(lái)說就是final修飾數(shù)組,指定數(shù)組所指向的內(nèi)存空間固定,數(shù)組內(nèi)部值還能改。因?yàn)閿?shù)組是引用類型,內(nèi)存地址是不可改變的。
實(shí)例代碼
public static void testReflection() throws Exception {
    String s = "Hello,World"; 
    System.out.println("s = " + s); //Hello World  
    //獲取String類中的value字段
    Field valueOfString = String.class.getDeclaredField("value");    
    //改變value屬性的訪問權(quán)限
    valueOfString.setAccessible(true);    
    //獲取s對(duì)象上的value屬性的值
    char[] value = (char[]) valueOfString.get(s);   
    //改變value所引用的數(shù)組中的第5個(gè)字符
    value[5] = '_';  
    System.out.println("s = " + s);  //Hello_World
}

String為什么要設(shè)計(jì)成不可變

1.允許String對(duì)象緩存hashcode:
Java中String對(duì)象的哈希碼被頻繁地使用, 比如在hashMap 等容器中。字符串不變性保證了hash碼的唯一性,因此可以放心地進(jìn)行緩存。
2.安全性
String被許多的Java類(庫(kù))用來(lái)當(dāng)做參數(shù),例如 網(wǎng)絡(luò)連接地址URL,文件路徑path,還有反射機(jī)制所需要的String參數(shù)等, 假若String不是固定不變的,將會(huì)引起各種安全隱患。
對(duì)于一個(gè)方法而言,參數(shù)是為該方法提供信息的,而不是讓方法改變自己。
3.字符串常量池的需要
字符串常量池(String pool, String intern pool, String保留池) java堆內(nèi)存放了一個(gè)特殊的區(qū)域用于常量池, 當(dāng)創(chuàng)建一個(gè)String對(duì)象時(shí),假如此字符串值已經(jīng)存在于常量池中,則不會(huì)創(chuàng)建一個(gè)新的對(duì)象,而是引用已經(jīng)存在的對(duì)象。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類相關(guān)的語(yǔ)法,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法,線程的語(yǔ)...
    子非魚_t_閱讀 34,823評(píng)論 18 399
  • 一、Java 簡(jiǎn)介 Java是由Sun Microsystems公司于1995年5月推出的Java面向?qū)ο蟪绦蛟O(shè)計(jì)...
    子非魚_t_閱讀 4,635評(píng)論 1 44
  • 一句話 就能輕易被原諒 誰(shuí)的慈悲 一個(gè)不留神 就能再次被放逐 誰(shuí)的清脆 一下諾言 就能瘋狂被嘲笑 誰(shuí)的從前 誰(shuí)人 ...
    焚膏繼乆閱讀 286評(píng)論 0 2
  • 在時(shí)光悄然無(wú)聲的蠕動(dòng)著,太多的承諾從指縫中的溜走,所以的愿望都不再?gòu)?qiáng)烈了,就像天上的星星,一顆一課隕落。慢慢地,就...
    糖玉嬋糖閱讀 288評(píng)論 0 1
  • 聽對(duì)”話”------你的職場(chǎng)會(huì)開掛 一 小A的公司是市里比較體面的公司,薪酬高、福利好,工作相對(duì)寬松。 9月份小...
    愷愷媽閱讀 190評(píng)論 0 1

友情鏈接更多精彩內(nèi)容