日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

java

哈希存储 java_Java容器系列之HashMap的存储

發(fā)布時(shí)間:2023/12/20 java 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 哈希存储 java_Java容器系列之HashMap的存储 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

Java容器系列之HashMap

概要

本文將結(jié)合Java源碼總結(jié)HashMap的存儲(chǔ)結(jié)構(gòu)及其擴(kuò)容策略,并根據(jù)這些特點(diǎn)給出使用HashMap的最佳實(shí)踐。

本文不再介紹HashMap的基本使用,有需要的請(qǐng)先學(xué)習(xí)下Java容器的基礎(chǔ)知識(shí)。

存儲(chǔ)結(jié)構(gòu)

HashMap的核心問題是如何保證讀寫的速度?答案是使用Key對(duì)象的Hash值來合理存儲(chǔ)對(duì)象。我們知道,每個(gè)java對(duì)象都有其默認(rèn)的hashCode()方法,也就是每個(gè)對(duì)象都有一個(gè)int值與之對(duì)應(yīng)。那么如何利用這個(gè)int值來快速存儲(chǔ)對(duì)象呢?

1.按照hash值找坑位

假設(shè)定義了一個(gè)長度為8的HashMap,那么HashMap會(huì)創(chuàng)建一個(gè)長度為8的數(shù)組作為容器來存放鍵值對(duì)(在HashMap內(nèi)部使用了Node對(duì)象來存儲(chǔ))。也就是說,這個(gè)數(shù)組的元素是Node對(duì)象。那么,此時(shí)有8個(gè)坑位可以用來放置元素。我們按照J(rèn)ava的數(shù)組index標(biāo)號(hào),也就是從0到7分別標(biāo)號(hào)。

Key對(duì)象的Hash值我們是能夠得到的,此時(shí)如果把這個(gè)Hash值來對(duì)8取模,自然是從0到7的一個(gè)分布了。故根據(jù)Key對(duì)象的Hash值,我們可以快速找到該Key放在了數(shù)組的哪個(gè)坑位,避免的數(shù)組的輪詢。

總結(jié)下,坑位的取得方式,簡單的說就是先得到Key的HashCode,然后把這個(gè)值對(duì)HashMap的長度取模。

此處有一個(gè)問題,如果兩個(gè)不同的Hash值,取模的結(jié)果相等怎么辦?比如: 如果Key1的hash值是7,Key2的hash值是15,此時(shí)它們對(duì)8取模,都是得到7,不可能把這兩個(gè)對(duì)象放在一個(gè)坑位。

2.當(dāng)不同的Key得到同一個(gè)坑位后,按照鏈表或者紅黑樹存儲(chǔ)

JDK8之前,使用了鏈表來存儲(chǔ)hash取模后一樣的元素。而JDK8開始使用了鏈表和紅黑樹相結(jié)合的方式來存儲(chǔ)。

鏈表存儲(chǔ)

查看Node的定義,可以看到它包含了一個(gè)next屬性。

static class Node implements Map.Entry {

final int hash;

final K key;

V value;

Node next;

......

}

正是利用這個(gè)next,實(shí)現(xiàn)了鏈表的存儲(chǔ)。此時(shí),從HashMap取元素的時(shí)候,首先找到坑位,然后遍歷坑位中的鏈表,逐一判斷Key對(duì)象的hashCode是否相等,而且Key對(duì)象的equals方法是否返回true。當(dāng)且僅當(dāng)hashCode相等,而且equals方法返回true時(shí),才認(rèn)為找到了key對(duì)應(yīng)的元素,并返回該Node對(duì)象中包含的value對(duì)象。

總結(jié)下,一個(gè)坑位下掛載了若干個(gè)元素,這些元素有以下特點(diǎn)。

1)同一坑位下的鏈表中,各個(gè)元素的Key對(duì)象Hash值取模后的值都是一樣的。

2)鏈表的先后順序按照進(jìn)入HashMap的時(shí)間順序排列,新元素被放入坑位,并且其next指向原來在坑位中的元素。

3)這個(gè)順序是可能變化的,比如擴(kuò)容的時(shí)候。這樣也就不難理解為什么HashMap無法保證元素的排序了。

顯然,這樣有個(gè)缺點(diǎn),就是如果這個(gè)鏈表很長,那邊取元素的性能會(huì)隨著鏈表的拉長而變差,而且是越來越差。所以,JDK8開始,引入了紅黑樹的存儲(chǔ)方式。當(dāng)某個(gè)坑位下的鏈表的長度小于8的時(shí)候,還是鏈表存儲(chǔ);否則,變換成紅黑樹存儲(chǔ)。

紅黑樹存儲(chǔ)

紅黑樹能夠保證存取元素的速度不會(huì)隨著元素?cái)?shù)量的增加而迅速惡化(時(shí)間復(fù)雜度為lg(n))。具體這里不展開講,紅黑樹的資料還是很多的。

查看JDK8的HashMap源碼,其putVal方法如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

boolean evict) {

Node[] tab; Node p; int n, i;

if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

if ((p = tab[i = (n - 1) & hash]) == null) // 取模使用的是長度減1再跟hash位與操作

tab[i] = newNode(hash, key, value, null);

else {

Node e; K k;

if (p.hash == hash &&

((k = p.key) == key || (key != null && key.equals(k))))

e = p;

else if (p instanceof TreeNode)

e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);

else {

for (int binCount = 0; ; ++binCount) {

if ((e = p.next) == null) {

p.next = newNode(hash, key, value, null);

if (binCount >= TREEIFY_THRESHOLD - 1) //達(dá)到8則轉(zhuǎn)換成樹

treeifyBin(tab, hash);// 鏈表轉(zhuǎn)換成紅黑樹

break;

}

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

break;

p = e;

}

}

if (e != null) { // existing mapping for key

V oldValue = e.value;

if (!onlyIfAbsent || oldValue == null)

e.value = value;

afterNodeAccess(e);

return oldValue;

}

}

++modCount;

if (++size > threshold)

resize();

afterNodeInsertion(evict);

return null;

}

擴(kuò)容策略

HashMap在使用的時(shí)候并沒有人會(huì)關(guān)注其大小,看上去這個(gè)容器是無限大小的,反正有東西就往里面放,至于里面放了多少,還能放多少,沒人關(guān)心。這完全得益于其自動(dòng)化的擴(kuò)容機(jī)制。HashMap內(nèi)部定義了一套擴(kuò)容的機(jī)制,以應(yīng)對(duì)元素的擴(kuò)張。

1.HashMap的擴(kuò)容閾值

當(dāng)往HashMap中放入元素的時(shí)候,如果HashMap中的元素個(gè)數(shù)達(dá)到某個(gè)閾值時(shí),會(huì)觸發(fā)擴(kuò)容操作。這個(gè)閾值由loadFactor屬性控制的,這個(gè)值是個(gè)小數(shù)(默認(rèn)值是0.75),閾值等于map的整體容量乘以loadFactor。如此做的目的是確保坑位的量足夠,讓元素能夠足夠的分散。擴(kuò)容后的容量一般是翻倍,而且容量都是2的冪,比如2,4,8,16,32,64.......這樣做的目的是為了方便取模的操作(取模的操作很巧妙,會(huì)再開一文)。

2.HashMap的擴(kuò)容方法

擴(kuò)容的操作通常是增加容量后,重新安置各個(gè)元素。擴(kuò)容后,元素的放置都重新打亂了,等于是重新生成了一次HashMap。所以,盡量減少擴(kuò)容是比較明智的。

最佳實(shí)踐

使用HashMap的時(shí)候,如果能夠預(yù)估容器的大小,那么在初始化的時(shí)候就指定size的大小。這樣可以盡量減少容器的擴(kuò)容操作,提高程序運(yùn)行效率。下例中,我們?cè)诔跏蓟痬ap的時(shí)候,指定了其size為100。如下所示:

Map map = new HashMap(100);

雖然代碼中定義的長度是100,但是實(shí)際的長度是大于100的最小的2的n次方的值,有點(diǎn)繞口。舉例如下:

如果指定了50,那么實(shí)際長度是2的6次方,64

如果指定了100,那么實(shí)際長度是2的7次方,128

以此類推。。。

2.HashMap不是線程安全的,需要注意多線程的同步問題。特別是可能導(dǎo)致死循環(huán)的問題(在HashMap擴(kuò)容時(shí),多線程的情況下使用HashMap可能會(huì)導(dǎo)致死循環(huán))。如果需要保證線程安全,建議使用HashMap+Collections工具類的組合來做,而不是使用Hashtable。如下所示:

Map map = new HashMap();

Map synMap = Collections.synchronizedMap(map);

synMap.put("key1", "value1");

當(dāng)然,Hashtable也是線程安全的,但是該類的同步控制粒度比較大,跟上面比性能更低些,而且該類也是逐步被廢棄的態(tài)勢在發(fā)展,少用吧。

線程安全是消耗性能的,所以能回避線程安全問題就盡量回避,這是上上策。

3.如果還想要保證順序,可以考慮使用LinkedHashMap來實(shí)現(xiàn)。它是在HashMap的基礎(chǔ)上增加了一組保存先后順序的雙向鏈表。可以按照LRU(最不常用的放到最后,越常用的越在前面),或者按照放入容器的順序排列。

總結(jié)

以上是生活随笔為你收集整理的哈希存储 java_Java容器系列之HashMap的存储的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。