研究String不可變特性時遇到的問題分析
背景
三年前在學(xué)習(xí)String相關(guān)的概念知識的時候,看到了Java中String的不可變特性沉填,說的是String對象一旦生成就不會變更市框,其他所有的操作實際上都是重新生成了新的String對象霞扬,然后我用反射機制做了一個demo,出現(xiàn)了一些令我迷惑的現(xiàn)象枫振,當(dāng)時還去了segmentfault網(wǎng)站上寫了個提問喻圃。
有幾個答主給出了一些見解還是很深刻的,最近重新復(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對象的地址返回即可闪萄。
而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é)記錄。