研究String不可變特性時遇到奇怪現(xiàn)象

研究String不可變特性時遇到的問題分析

背景

三年前在學(xué)習(xí)String相關(guān)的概念知識的時候,看到了Java中String的不可變特性沉填,說的是String對象一旦生成就不會變更市框,其他所有的操作實際上都是重新生成了新的String對象霞扬,然后我用反射機制做了一個demo,出現(xiàn)了一些令我迷惑的現(xiàn)象枫振,當(dāng)時還去了segmentfault網(wǎng)站上寫了個提問喻圃。

image.png

有幾個答主給出了一些見解還是很深刻的,最近重新復(fù)習(xí)的時候粪滤,又仔細追蹤了一下這個問題斧拍,現(xiàn)在有了一點點的初步設(shè)想,不過只是猜測杖小,我目前還沒找到相關(guān)的涉及資料來支撐這個想法肆汹。我這邊目前用到的JDK版本是1.8.0_351,先上代碼:

代碼

String str1 = "String";
String str2 = "Strong";
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char [])field.get(str1);
value[3] = 'o';

這段代碼邏輯就是為了替換String對象內(nèi)的value值予权,因為String的不可變特性昂勉,所以常規(guī)方案肯定不行,這里就采用了一個反射的方式來強行改變了String內(nèi)部value這個屬性存儲的值扫腺,這么改動之后硼啤,我發(fā)現(xiàn)了一些比較奇怪的現(xiàn)象:

問題

輸出str1和str2:

System.out.println(str1); //Strong
System.out.println(str2);//Strong

因為用反射機制把str1的內(nèi)容給換成了str2的內(nèi)容了,所以str1現(xiàn)在也是Strong斧账。

比較str1和str2:

System.out.println(str1 == str2); // false
System.out.println(str1.equals(str2)); // true

這個結(jié)果谴返,腦補了如下這張圖,str1和str2在聲明的時候咧织,由于是字面量創(chuàng)建嗓袱,在創(chuàng)建字符串對象的時候會去字符串常量池里面看看是否已有對應(yīng)的對象,如果沒有的話习绢,還會在常量池里面加入對應(yīng)的緩存記錄渠抹。

后續(xù)如果再有同樣的字符串,直接從常量池里面獲取對應(yīng)String對象的地址返回即可闪萄。

image.png

而String中梧却,本身重寫了equals方法:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i]) // 可以看到是逐個字符對比value屬性的內(nèi)容
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

而str1內(nèi)部的value已經(jīng)通過反射調(diào)整到了"Strong"內(nèi)容,因此這里比較之后败去,結(jié)果就為true了放航。

直接輸出字符串"String"、"Strong":

System.out.println("String"); //Strong
System.out.println("Strong"); //Strong

前面說了圆裕,因為聲明str1和str2的時候广鳍,采用的是字面量創(chuàng)建荆几,因此會將其放入到常量池中,因此這時候直接使用字符串字面量的話赊时,默認使用的就是前面創(chuàng)建str1吨铸、str2時在常量池中緩存的那個,所以對于這里的"String"字符串祖秒,就相當(dāng)于有一個匿名的變量指向它诞吱,但是因為字符串常量池的緣故,所以該匿名變量所指向的地址就是前面str1指向的地址竭缝。

所以它等價于前面直接輸出str1房维、str2的那兩句代碼。

至于println的輸出歌馍,就需要追蹤一下改方法的源碼內(nèi)部調(diào)用了:

仔細追蹤了下println()這個方法內(nèi)部:

調(diào)用鏈是這樣:println() ----> print() ---> write() ---> textOut.write(String) ---> write(String str, int off, int len)

public void write(String str, int off, int len) throws IOException {
    ......
    str.getChars(off, (off + len), cbuf, 0);
    write(cbuf, 0, len);
}

這個里面的str.getChars()實際內(nèi)部就是利用System的arraycopy方法把字符串內(nèi)部的value數(shù)組拷貝到cbuf數(shù)組中握巢,然后寫出cbuf數(shù)組內(nèi)的數(shù)據(jù)晕鹊。

所以可以發(fā)現(xiàn)它實際上輸出的仍舊是String對象內(nèi)部value的值松却,前面的代碼中因為使用了反射,將該字符串對應(yīng)的內(nèi)部值變更了溅话,所以這里也就跟著一并變化了晓锻。

輸出HashCode

System.out.println(str1.hashCode()); //-1808112969
System.out.println(str2.hashCode()); //-1808112969
System.out.println(System.identityHashCode(str1)); //939047783
System.out.println(System.identityHashCode(str2)); //1237514926

前面兩行的輸出內(nèi)容是一樣的,這說明此時str1和str2調(diào)用對應(yīng)的hashCode方法得到的結(jié)果是一樣飞几,這是因為String類重寫的hashCode方法砚哆,這里可以跟蹤進入重寫的hashCode內(nèi)部邏輯上去看看:

public int hashCode() {
    int h = hash; // hash默認是0
    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;
}

該方法上的注釋已經(jīng)說明了計算方法:

Returns a hash code for this string. The hash code for a String object is computed as

s[0]31^(n-1) + s[1]31^(n-2) + ... + s[n-1]

可以看到,String對象的hash值計算和內(nèi)部的value數(shù)組緊密相關(guān)屑墨,此時因為使用反射躁锁,修改了str1內(nèi)部value數(shù)組內(nèi)的內(nèi)容,因此hashCode計算的結(jié)果str1和str2是一樣的卵史,這個沒毛病战转。

至于下面兩行調(diào)用的 System.identityHashCode() 方法,這個方法它是一個native方法以躯,根據(jù)注釋上的內(nèi)容:

Returns the same hash code for the given object as would be returned by the default method hashCode(), whether or not the given object's class overrides hashCode()槐秧。

為給定對象返回與默認方法 hashCode() 返回的相同的哈希碼,無論給定對象的類是否覆蓋 hashCode()

可以看到忧设,它最終調(diào)用的就是Object里的hashCode方法刁标,也就是說:這里通過identityHashCode方法調(diào)用,實際上是繞過了String自身的 hashCode方法址晕,轉(zhuǎn)而直接使用了Object的hashCode方法膀懈。

JDK8中默認的hashCode計算方案是Marsaglia’s xor-shift 隨機數(shù)生成法,它是跟線程狀態(tài)有關(guān)谨垃。

可參考 openjdk 源碼:

// sychronizer.cpp
static inline intptr_t get_next_hash(Thread * Self, oop obj) {
  intptr_t value = 0 ;
  if (hashCode == 0) {
     // This form uses an unguarded global Park-Miller RNG,
     // so it's possible for two threads to race and generate the same RNG.
     // On MP system we'll have lots of RW access to a global, so the
     // mechanism induces lots of coherency traffic.
     value = os::random() ;
  } else
  if (hashCode == 1) {
     // This variation has the property of being stable (idempotent)
     // between STW operations.  This can be useful in some of the 1-0
     // synchronization schemes.
     intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
     value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
  } else
  if (hashCode == 2) {
     value = 1 ;            // for sensitivity testing
  } else
  if (hashCode == 3) {
     value = ++GVars.hcSequence ;
  } else
  if (hashCode == 4) {
     value = cast_from_oop<intptr_t>(obj) ;
  } else {
     // Marsaglia's xor-shift scheme with thread-specific state
     // This is probably the best overall implementation -- we'll
     // likely make this the default in future releases.
     unsigned t = Self->_hashStateX ;
     t ^= (t << 11) ;
     Self->_hashStateX = Self->_hashStateY ;
     Self->_hashStateY = Self->_hashStateZ ;
     Self->_hashStateZ = Self->_hashStateW ;
     unsigned v = Self->_hashStateW ;
     v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
     Self->_hashStateW = v ;
     value = v ;
  }

  value &= markOopDesc::hash_mask;
  if (value == 0) value = 0xBAD ;
  assert (value != markOopDesc::no_hash, "invariant") ;
  TEVENT (hashCode: GENERATE) ;
  return value;
}

JDK8默認走的是最后的else分支吏砂,這里出現(xiàn)了_hashStateX 撵儿、_hashStateY、_hashStateZ狐血、_hashStateW淀歇。

而在thread.cpp中:

// thread-specific hashCode stream generator state - Marsaglia shift-xor form
_hashStateX = os::random() ;
_hashStateY = 842502087 ;
_hashStateZ = 0x8767 ;    // (int)(3579807591LL & 0xffff) ;
_hashStateW = 273326509 ;

可以發(fā)現(xiàn),這種算法實際上就是基于一個隨機值外加三個確定值經(jīng)過一系列運算后得到的一個數(shù)字匈织。

同時為了提升性能浪默,JDK會將對象的hashCode值進行緩存,將對象第一次計算后的 hash 值緩存起來缀匕,下次再獲取時無需重新計算纳决,直接從緩存處獲取。

對于String類乡小,它內(nèi)部本身就設(shè)定了一個hash屬性用于緩存字符串對象的hash值:

private int hash; // Default to 0

而對于native方法計算的hash值阔加,可以參考sychronizer.cpp中的FastHashCode:

intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
    ......
    if (mark->is_neutral()) {
        hash = mark->hash();              // this is a normal header
        if (hash) {                       // if it has hash, just return it
          return hash;
        }
        ......
    }
    ......
}

緩存的做法比較適合在不可變化的對象上使用,比如:String满钟。對于存在變動的胜榔,就最好重寫 hashcode 方法。

在研究String的這個hashCode問題時湃番,發(fā)現(xiàn)了一些相關(guān)的知識內(nèi)容夭织,準備專門開一篇介紹hashCode相關(guān)研究過程的總結(jié)記錄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末吠撮,一起剝皮案震驚了整個濱河市尊惰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌泥兰,老刑警劉巖弄屡,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鞋诗,居然都是意外死亡膀捷,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門师脂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來担孔,“玉大人,你說我怎么就攤上這事吃警「馄” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵酌心,是天一觀的道長拌消。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么墩崩? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任氓英,我火速辦了婚禮,結(jié)果婚禮上鹦筹,老公的妹妹穿的比我還像新娘铝阐。我一直安慰自己,他們只是感情好铐拐,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布徘键。 她就那樣靜靜地躺著,像睡著了一般遍蟋。 火紅的嫁衣襯著肌膚如雪吹害。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天虚青,我揣著相機與錄音它呀,去河邊找鬼。 笑死棒厘,一個胖子當(dāng)著我的面吹牛纵穿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播绊谭,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼政恍,長吁一口氣:“原來是場噩夢啊……” “哼汪拥!你這毒婦竟也來了达传?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤迫筑,失蹤者是張志新(化名)和其女友劉穎宪赶,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體脯燃,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡搂妻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了辕棚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片欲主。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖逝嚎,靈堂內(nèi)的尸體忽然破棺而出扁瓢,到底是詐尸還是另有隱情,我是刑警寧澤补君,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布引几,位于F島的核電站,受9級特大地震影響挽铁,放射性物質(zhì)發(fā)生泄漏伟桅。R本人自食惡果不足惜敞掘,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望楣铁。 院中可真熱鬧玖雁,春花似錦、人聲如沸盖腕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赊堪。三九已至面殖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間哭廉,已是汗流浹背脊僚。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留遵绰,地道東北人辽幌。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像椿访,于是被迫代替她去往敵國和親乌企。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容