心心念念的HashMap,了解一下

? ? ? ? ? HashMap底層實(shí)現(xiàn)

目錄索引

1.java中Map相關(guān)結(jié)構(gòu)體系

2.HashMap源碼中的細(xì)節(jié)

3.JDK1.8中對(duì)HashMap的改進(jìn)

4.高并發(fā)下的HashMap


1 Map結(jié)構(gòu)體系簡(jiǎn)單介紹

? ? ? ? ? ? 上文ArrayList相關(guān)介紹中有提到j(luò)ava中除了單個(gè)元素成“列”存儲(chǔ)的列表容器以外归敬,還提供了存儲(chǔ)? key-value 成對(duì)格式的對(duì)象容器真屯。Map的實(shí)現(xiàn)類有HashMap衷蜓、Hashtable取试、LinkedHashMap和TreeMap纫骑,類繼承關(guān)系如下圖所示:


圖片來源于Java 8系列之重新認(rèn)識(shí)HashMap

注:圖中LinkedHashMap少了一個(gè)字母‘h’? 哈哈以现,摳細(xì)節(jié)

2.1 HashMap定義

? ? ? ? ? HashMap 繼承了 AbstractMap 抽象類實(shí)現(xiàn)了Map接口隔显,用于存儲(chǔ)Key-Value鍵值對(duì)的集合项鬼,它的主干是一個(gè)存儲(chǔ)Entry對(duì)象(1.8中改為了Node)的數(shù)組侦厚,初始值都被設(shè)置為null.耻陕。當(dāng)每次插入一個(gè)新的Entry對(duì)象,首先根據(jù)該對(duì)象的 key 值計(jì)算一個(gè) hashcode 刨沦,然后通過一個(gè) hash 函數(shù)計(jì)算出該元素應(yīng)該放在 Entry 應(yīng)該放在哪個(gè)桶中诗宣。


圖片來源于Java 8系列之重新認(rèn)識(shí)HashMap -

? ? ? ? 如上圖所示,整個(gè)? HashMap 相當(dāng)于一個(gè) table,每個(gè) table 由若干個(gè)桶組成想诅,每個(gè)桶中存儲(chǔ)一個(gè)單向鏈表召庞,在JDK 1.8中某個(gè)桶中的鏈表長(zhǎng)度超過 8時(shí),將會(huì)把該桶中的元素構(gòu)建為一顆紅黑樹来破。

2.2 HashMap源碼中的一些細(xì)節(jié)

? ? ? ? ? 每次想在HashMap中存儲(chǔ)一個(gè)鍵值對(duì)時(shí)篮灼,第一步先創(chuàng)建一個(gè)Entry(java8中改成了Node)對(duì)象,然后將該對(duì)象鍵的 hashcode 值通過 hash 函數(shù)映射計(jì)算出該 Entry 應(yīng)該存儲(chǔ)在HashMap中的具體位置下標(biāo)徘禁;通常情況下 诅诱,多個(gè)不同的對(duì)象可能會(huì)計(jì)算出相同的hashcode 值,這樣就會(huì)出現(xiàn)散列沖突送朱,為了解決沖突娘荡,具有相同hashcode值的不同對(duì)象會(huì)通過鏈表的形式存儲(chǔ)在同一個(gè)索引上,當(dāng)一個(gè)索引的長(zhǎng)度超過8時(shí),會(huì)把鏈表轉(zhuǎn)換為紅黑樹骤菠。關(guān)于紅黑樹是個(gè)大專題在這里不瞎談它改,改天單獨(dú)認(rèn)真聊。

put? 時(shí)值的注意的一些細(xì)節(jié):? 為什么要求hashMap的默認(rèn)長(zhǎng)度以及擴(kuò)? ? 容后的 Node桶的個(gè)數(shù)必須是 2 的次冪

? ? ? 簡(jiǎn)單的說是為了避免出現(xiàn)過多的hash沖突商乎,舉個(gè)例子說明? 假設(shè) key = "Apple"的hashcode值為:? 1111 1111 1111 1111 1111 0000 1110 1010? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? &

假設(shè)table的Node桶的個(gè)數(shù) length 為10:? ? 0000 0000 0000 0000 0000 0000 0000 1010

? 計(jì)算結(jié)果是:? ? ? 0000 0000 0000 0000 0000 0000 0000 1010

? ? ? ? ? 如果下一個(gè)要存入的 Node 對(duì)象的key的 hashcode 最低四位是: 1111? 那么和 1010 取 & 運(yùn)算結(jié)果仍然是 1010央拖,明顯 hashcode 的最后四位值不相同,結(jié)果經(jīng)過 hash 后卻獲得了相同的索引鹉戚;還有一個(gè)問題就是在上面的例子中鲜戒,有些索引值是永遠(yuǎn)不會(huì)出現(xiàn)的:比如1111 0101 0001 0100? ? ? 這樣會(huì)導(dǎo)致更多的hash沖突不說還會(huì)導(dǎo)致某些索引值所在的位置永遠(yuǎn)不會(huì)存入元素,這些都是我們不希望看到的抹凳。然后把問題回到起點(diǎn)遏餐,發(fā)生更多的hash沖突的原因是table中的node桶個(gè)數(shù)的二進(jìn)制低位上有 0,這樣取 & 運(yùn)算 0 所在的位置當(dāng)然不會(huì)計(jì)算出 1赢底,為了解決這個(gè)問題java設(shè)計(jì)師將Node 桶個(gè)數(shù)規(guī)定為 2 的次冪失都,這樣 hashcode 值和 length-1 取 & 運(yùn)算就會(huì)使得每次計(jì)算出的索引值都和 hashcode 的低位值一致(上面例子中的最后四位)柏蘑。 并且不會(huì)出現(xiàn)某些索引被遺忘的情況,繼續(xù)上面的例子:

? ? ? ? key = "Apple"的 hashcode 值為:? ? ? ? ? 1111 1111 1111 1111 1111 0000 1110 1010

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? &

假設(shè)table的Node桶的個(gè)數(shù)length-1為15: 0000 0000 0000 0000 0000 0000 0000 1111

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 計(jì)算結(jié)果是:? ? ? ? 0000 0000 0000 0000 0000 0000 0000 1010?

如果下一個(gè)要存入的Node對(duì)象key的hashcode最后四位是: 1111 那么和 1010 取 & 運(yùn)算結(jié)果是1111粹庞,這樣就很大程度上減少了hash沖突咳焚,同時(shí)也不會(huì)出現(xiàn)某些桶不會(huì)存入Node對(duì)象的情況 JDK1.7中的hash函數(shù)就是這樣實(shí)現(xiàn)的,但是在JDK 1.8 中又做了進(jìn)一步的改進(jìn)庞溜,假設(shè)有兩個(gè)Node對(duì)象的 hashcode 值計(jì)算出來低16位完全相同革半,但是高 16 位值不同,當(dāng)table的桶個(gè)數(shù)較小的時(shí)候這兩個(gè)對(duì)象的hashcode值直接和length - 1取 &流码,求得的索引是相同的又官,但是如果在取 & 之前,將兩個(gè)對(duì)象的hashcode值右移16位漫试,再和原來的 hashcode 值取異或六敬,這樣就會(huì)使得原來相同的低16 變?yōu)椴煌又?length-1 取&商虐,求得的索引值是不相同的兩個(gè)數(shù)觉阅,很好地解決了這種沖突,但是這樣也會(huì)有不好的情況秘车,就是可能會(huì)把之前低 16 不相等 hashcode 值變?yōu)橄嗟鹊溆拢贿^出現(xiàn)的幾率會(huì)很小。

JDK 1.8 hash 函數(shù)


putVal 的部分源碼

putVal 中 的片段說明叮趴,雖然沒有了1.7 中 的 indexOf 方法單獨(dú)確定下標(biāo)割笙,但是在做插入判斷的時(shí)候依然是 和 length-1 做 & 運(yùn)算

*? if ((p = tab[i = (n - 1) & hash]) == null)

舉個(gè)例子說明一下前面說到的“不足”:

? hashcode值1:1111 1111 1111 1010 1111 0000 1110 1010

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ^

? >>>16? ? 0000 0000 0000 0000 1111 1111 1111 1010

? 異或結(jié)果? 0000 0000 0000 0000 1111 1111 1111 0000

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? &?

? length-1:? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 1111

hash? 結(jié)果:? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 0000

*? hashcode值2:1111 1111 1111 0101 1111 0000 1110 0101

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ^

? ? ? ? ? ? ? ? >>>16? ? 0000 0000 0000 0000 1111 1111 1111 0101

? ? ? ? ? ? ? ? ? 異或結(jié)果? 1111 1111 1111 1111 0000 1111 0001 0000

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? &

? ? ? ? ? length-1 :? ? ? ? ? ? ? ? ? ? ? ? ? ? 1111? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

? ? ? ? ? hash 結(jié)果:? ? ? ? ? ? ? ? ? ? ? ? ? 0000? ? ? ? ? ? ? ?

如上面的例子所示,本來不相同兩個(gè) hashcode 值眯亦,卻計(jì)算出了相同的下標(biāo)伤溉;不過這種情況出現(xiàn)的概率是很小的,總得來說每一次改進(jìn)都只是更好的一種優(yōu)化權(quán)衡妻率,而不是直接消除 hash 沖突乱顾,因?yàn)閔ash沖突只能盡量避免而不能完全消除。不然鏈地址法就完全沒有用武之地了宫静。

接下來談?wù)則able中很重要的一個(gè)字段 threshold:

? ? ? ? threshold? = Load factor負(fù)載因子(默認(rèn)值是0.75)* length;引入負(fù)載因子的目的是用它來衡量一個(gè) table 到底能裝多滿走净,負(fù)載因子很大則說明內(nèi)存空間被充分利用,由于查找效率和table中的桶個(gè)數(shù)有關(guān)孤里,table? 裝的越滿伏伯,則查找效率越低;相反如果負(fù)載因子很小則說明一個(gè)table中有很多的空桶捌袜,這樣會(huì)造成內(nèi)存空間浪費(fèi)说搅,除非在對(duì)內(nèi)存空間和執(zhí)行效率把控特別到位的時(shí)候才可以適當(dāng)調(diào)整默認(rèn)值 0.75,否則慎改虏等。

put具體步驟:(摘自Java 8系列之重新認(rèn)識(shí)HashMap -


putVal流程圖

①.判斷鍵值對(duì)數(shù)組table[i]是否為空或?yàn)閚ull弄唧,否則執(zhí)行resize()進(jìn)行擴(kuò)容适肠;

②.根據(jù)鍵值key計(jì)算hash值得到插入的數(shù)組索引i,如果table[i]==null候引,直接新建節(jié)點(diǎn)添加迂猴,轉(zhuǎn)向⑥,如果table[i]不為空背伴,轉(zhuǎn)向③;

③.判斷table[i]的首個(gè)元素是否和key一樣峰髓,如果相同直接覆蓋value傻寂,否則轉(zhuǎn)向④,這里的相同指的是hashCode以及equals携兵;

④.判斷table[i] 是否為treeNode疾掰,即table[i] 是否是紅黑樹,如果是紅黑樹徐紧,則直接在樹中插入鍵值對(duì)静檬,否則轉(zhuǎn)向⑤;

⑤.遍歷table[i]并级,判斷鏈表長(zhǎng)度是否大于8拂檩,大于8的話把鏈表轉(zhuǎn)換為紅黑樹,在紅黑樹中執(zhí)行插入操作嘲碧,否則進(jìn)行鏈表的插入操作稻励;遍歷過程中若發(fā)現(xiàn)key已經(jīng)存在直接覆蓋value即可;

⑥.插入成功后愈涩,判斷實(shí)際存在的鍵值對(duì)數(shù)量size是否超多了最大容量threshold望抽,如果超過,進(jìn)行擴(kuò)容履婉。

JDK1.8 中 的擴(kuò)容優(yōu)化:

? ? ? 首先會(huì)創(chuàng)建原table兩倍大小的新table煤篙,然后將舊數(shù)組中的元素通過再hash插入到新的數(shù)組中,與1.7 不同的是在移動(dòng)舊元素時(shí)1.8中會(huì)根據(jù)元素在舊數(shù)組中索引位 判斷出該元素在新數(shù)組中應(yīng)該插入的位置而不需要再次 hash毁腿,因?yàn)樾聰?shù)組的長(zhǎng)度是原數(shù)組的 2 倍辑奈,所以假如再次進(jìn)行? hash 變化的只是第三步和 length-1 做 & 運(yùn)算的時(shí)候,由于 length-1 比之前多了一位 1狸棍,多出來的一位和 hashcode 高低位異或后對(duì)應(yīng)的那一位要么是 0 要么是 1 身害; 如果是 0,那么該元素應(yīng)該插入到與舊數(shù)組索引值相同的位置草戈,如果是 1塌鸯,那么該元素應(yīng)該插入到在舊數(shù)組索引值加上舊數(shù)組的總長(zhǎng)度的位置。這樣就很好的避免了再次求 hash 值唐片,只需要查看原來的 hash 值中異或后多出來的一位時(shí) 0 還是 1丙猬。舉例見下圖:

JDK1.8 hash 原理圖

3? 最后聊聊 HashMap 的線程不安全

大家都知道在多線程環(huán)境下 HashMap 是線程不安全的涨颜,最可怕的是如果兩個(gè)線程同時(shí)并發(fā)執(zhí)行同一個(gè) HashMap 時(shí)有可能形成一個(gè)隱藏起來的環(huán),就像一個(gè)定時(shí)炸彈一樣茧球,在某個(gè)時(shí)刻隨時(shí)爆發(fā)庭瑰。下面舉個(gè)例子,簡(jiǎn)單說明一下形成換的過程:

下圖是JDK 1.7 中擴(kuò)容時(shí)將舊數(shù)組中的元素移動(dòng)到新數(shù)組的具體過程抢埋,從源碼中可以看出每移動(dòng)一個(gè)元素都要重新 hash 求得在新數(shù)組中的索引值弹灭。插入時(shí)若索引沖突將會(huì)按頭插法插入到對(duì)應(yīng)桶的鏈表中

新舊更替

? ? ? ? ? 現(xiàn)在模擬一個(gè)并發(fā)場(chǎng)景,有兩個(gè)線程同時(shí)對(duì)一個(gè) HashMap 對(duì)象進(jìn)行擴(kuò)容操作揪垄,線程一新建一個(gè)長(zhǎng)度為兩倍的新 table穷吮,接下來線程二搶奪CPU同樣新建了一個(gè)新 table;然后線程一繼續(xù)獲得CPU執(zhí)行到 transfer 方法中的 next = e.next 時(shí)線程二再次搶奪了CPU執(zhí)行完整個(gè)轉(zhuǎn)移操作饥努。此時(shí)兩線程的狀態(tài)如下圖所示:

第一階段

線程二執(zhí)行完了轉(zhuǎn)移操作捡鱼,線程一獲得CPU,由于線程一上次執(zhí)行到了 next = e.next酷愧,所以此時(shí)繼續(xù)執(zhí)行驾诈,當(dāng)前為線程一的第一次循環(huán):

e = 3? ? next? = 7? ? e.next = newTable[i] = null? ? newTable[i] = e = 3? ? ? ? ? e = next = 7

第一次循環(huán)后的狀態(tài)

注意:原先的舊數(shù)組已經(jīng)被線程一置空了,所以線程一指向的 e = 3 和 next = 7 都已經(jīng)被移動(dòng)到了線程二創(chuàng)建的新 table 中溶浴。

接下來執(zhí)行第二次循環(huán):

e = 7? ? ? next = 3? ? e.next = newTable[i] = 3? ? ? newTable[i] = 7? ? e = next = 3


第二次循環(huán)后的狀態(tài)

第三循環(huán)見證奇跡:

e = 3? ? next = null? ? e.next = newTable[i] = 7? ? newTable[i] = 3? e = null? 插入完畢


最后一次循環(huán)后的狀態(tài)

e 最終指向了null 就這樣悄無聲息的轉(zhuǎn)移完成了乍迄,然而如上圖所示索引值為 3 的桶中的鏈表出現(xiàn)了一個(gè)環(huán),3 指向了 7 ====》7 指向了3戳葵;這樣在以后某個(gè)時(shí)刻當(dāng)調(diào)用get方法時(shí)就乓,查找的元素索引值剛好是3,但是該元素在鏈表中不存在拱烁,這樣執(zhí)行g(shù)et的線程將會(huì)在一個(gè)環(huán)中查找一個(gè)不存在的生蚁,永不停歇直到世界末日,哈哈戏自。

好了邦投,今天就先寫到這,關(guān)于HashMap還有很多值得聊的地方擅笔,下次再說志衣。

晚安,牧羊十二


民謠很窮猛们,一支煙就訴盡了一生
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末念脯,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子弯淘,更是在濱河造成了極大的恐慌药薯,老刑警劉巖楣号,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件授滓,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡借嗽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門转培,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恶导,“玉大人,你說我怎么就攤上這事浸须〔沂伲” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵删窒,是天一觀的道長(zhǎng)缤沦。 經(jīng)常有香客問我,道長(zhǎng)易稠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任包蓝,我火速辦了婚禮驶社,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘测萎。我一直安慰自己亡电,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布硅瞧。 她就那樣靜靜地躺著份乒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪腕唧。 梳的紋絲不亂的頭發(fā)上或辖,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音枣接,去河邊找鬼颂暇。 笑死,一個(gè)胖子當(dāng)著我的面吹牛但惶,可吹牛的內(nèi)容都是我干的耳鸯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼膀曾,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼县爬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起添谊,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤财喳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后碉钠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纲缓,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡卷拘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了祝高。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片栗弟。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖工闺,靈堂內(nèi)的尸體忽然破棺而出乍赫,到底是詐尸還是另有隱情,我是刑警寧澤陆蟆,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布雷厂,位于F島的核電站,受9級(jí)特大地震影響叠殷,放射性物質(zhì)發(fā)生泄漏改鲫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一林束、第九天 我趴在偏房一處隱蔽的房頂上張望像棘。 院中可真熱鬧,春花似錦壶冒、人聲如沸缕题。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烟零。三九已至,卻和暖如春咸作,著一層夾襖步出監(jiān)牢的瞬間锨阿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來泰國打工记罚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留群井,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓毫胜,卻偏偏與公主長(zhǎng)得像书斜,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子酵使,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

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

  • 在一個(gè)方法內(nèi)部定義的變量都存儲(chǔ)在棧中荐吉,當(dāng)這個(gè)函數(shù)運(yùn)行結(jié)束后,其對(duì)應(yīng)的棧就會(huì)被回收口渔,此時(shí)样屠,在其方法體中定義的變量將不...
    Y了個(gè)J閱讀 4,417評(píng)論 1 14
  • HashMap 是 Java 面試必考的知識(shí)點(diǎn),面試官從這個(gè)小知識(shí)點(diǎn)就可以了解我們對(duì) Java 基礎(chǔ)的掌握程度。網(wǎng)...
    野狗子嗷嗷嗷閱讀 6,667評(píng)論 9 107
  • 摘要 HashMap是Java程序員使用頻率最高的用于映射(鍵值對(duì))處理的數(shù)據(jù)類型痪欲。隨著JDK(Java Deve...
    周二倩你一生閱讀 1,250評(píng)論 0 5
  • 其實(shí)我們?cè)趯W(xué)校學(xué)習(xí)得課程是一種被動(dòng)學(xué)習(xí)的行為習(xí)慣悦穿,我們被老師得語音推著走,被老師的作業(yè)推著走业踢,經(jīng)常是沒有老師就不學(xué)...
    _均幻閱讀 166評(píng)論 0 1
  • 好久不見了栗柒,今天的畫來自漫畫三眼哮天錄,有興趣的朋友可以去看看哦知举,強(qiáng)力安利∩ω∩
    柚子露哈牛閱讀 1,667評(píng)論 5 3