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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

java容器类2:Map及HashMap深入解读

發(fā)布時(shí)間:2023/12/10 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java容器类2:Map及HashMap深入解读 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

Java的編程過程中經(jīng)常會和Map打交道,現(xiàn)在我們來一起了解一下Map的底層實(shí)現(xiàn),其中的思想結(jié)構(gòu)對我們平時(shí)接口設(shè)計(jì)和編程也有一定借鑒作用。(以下接口分析都是以jdk1.8源碼為參考依據(jù))

1. Map

An object that maps keys to values. A map cannot contain duplicate keys; each key can map to at most one value.

Map提供三種訪問數(shù)據(jù)的方式: 鍵值集、數(shù)據(jù)集、數(shù)據(jù)-映射,對應(yīng)下表中的標(biāo)記為黃色的三個(gè)接口。public interface Map<K, V>

方法名描述
void clear()從此映射中移除所有映射關(guān)系(可選操作)。
boolean containsKey(Object key)如果此映射包含指定鍵的映射關(guān)系,則返回 true。
boolean containsValue(Object value)如果此映射將一個(gè)或多個(gè)鍵映射到指定值,則返回 true。
Set<Map.Entry<K,V>> entrySet()返回此映射中包含的映射關(guān)系的 Set 視圖。
boolean equals(Object o)比較指定的對象與此映射是否相等。
V get(Object key)返回指定鍵所映射的值;如果此映射不包含該鍵的映射關(guān)系,則返回 null。
int hashCode()返回此映射的哈希碼值。
boolean isEmpty()如果此映射未包含鍵-值映射關(guān)系,則返回 true。
Set<K> keySet()返回此映射中包含的鍵的 Set 視圖。
V put(K key, V value)將指定的值與此映射中的指定鍵關(guān)聯(lián)(可選操作)。
void putAll(Map<? extends K,? extends V> m)從指定映射中將所有映射關(guān)系復(fù)制到此映射中(可選操作)。
V remove(Object key)如果存在一個(gè)鍵的映射關(guān)系,則將其從此映射中移除(可選操作)。
int size()返回此映射中的鍵-值映射關(guān)系數(shù)。
Collection<V> values()返回此映射中包含的值的 Collection 視圖。

在Java8中的Map有增添了一些新的接口不在上述表格之中,這里不一一列舉。

這里涉及到一個(gè)靜態(tài)內(nèi)部接口:Map.Entry<K,V> ,用于存儲一個(gè)鍵值對,該接口中設(shè)置set和get鍵值和value值的接口。

所以Map中存儲數(shù)據(jù)都是以這種Entry為數(shù)據(jù)單元存儲的。

2. AbatractMap

AbstractMap中增加了兩個(gè)非常重要的成員變量:

transient Set<K> keySet;
transient Collection<V> values;

通過這兩個(gè)成員變量,我們已經(jīng)知道Map是如何存儲數(shù)據(jù)的了:鍵值存入keySet中,value存入values中。(由于Map需要保證鍵值的唯一性所以選擇Set作為鍵值的存儲結(jié)構(gòu),而Value則對此沒有任何要求所以選擇Collection作為存儲結(jié)構(gòu))

AbstractMap實(shí)現(xiàn)了Map中的部分接口,都是通過調(diào)用接口:Set<Entry<K,V>> entrySet() 實(shí)現(xiàn)的,而該接口的具體實(shí)現(xiàn)卻留給了具體的子類。以下代碼列出了equal()方法的具體實(shí)現(xiàn):

public boolean equals(Object o) {if (o == this)return true;if (!(o instanceof Map))return false;Map<?,?> m = (Map<?,?>) o;if (m.size() != size())return false;try {Iterator<Entry<K,V>> i =

entrySet().

iterator();while (i.hasNext()) {Entry<K,V> e = i.next();K key = e.getKey();V value = e.getValue();if (value == null) {if (!(m.get(key)==null && m.containsKey(key)))return false;} else {if (!value.equals(m.get(key)))return false;}}} catch (ClassCastException unused) {return false;} catch (NullPointerException unused) {return false;}return true;}

3. HashMap

public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable除了繼承了AbstractMap中HashMap中的兩個(gè)成員變量以外,又增加了如下幾個(gè)成員變量:transient Set<Map.Entry<K,V>> entrySet;transient Node<K,V>[] table;transient int size;transient int modCount;作為table存儲的基本類型,Node類的源碼如下:

View Code

Node是HashMap的一個(gè)內(nèi)部類,實(shí)現(xiàn)了Map.Entry接口,本質(zhì)是就是一個(gè)映射(鍵值對)。

建議看HashMap源碼前了解一些散列表(HashTable)的基礎(chǔ)知識:http://www.cnblogs.com/NeilZhang/p/5651492.html

包括:散列函數(shù)、碰撞處理、負(fù)載因子等。

3.1 hash值計(jì)算

static final int hash(Object key) { //jdk1.8 & jdk1.7int h;// h = key.hashCode() 為第一步 取hashCode值// h ^ (h >>> 16) 為第二步 高位參與運(yùn)算return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

首先獲取key值的hash值(每個(gè)類都有計(jì)算hash值的方法),然后將該hash值的高16位異或低16位即得到散列值。

3.2 hash散列函數(shù)

?????? 通過hash函數(shù)可以得到key值對應(yīng)的hash值,那么如何通過該hash將key散列到hashtale中呢?下面再介紹一個(gè)函數(shù):

對應(yīng)的運(yùn)算如下所示:length為table的長度(通常選擇2^n)

static int indexFor(int h, int length) { //jdk1.7的源碼,jdk1.8沒有這個(gè)方法,但是實(shí)現(xiàn)原理一樣的return h & (length-1); //第三步 取模運(yùn)算 }

這里的取模運(yùn)算等于 hash%length ,然而&運(yùn)算比%運(yùn)算的效率更高。

3.3 碰撞算法:HashTable+鏈表+紅黑樹

當(dāng)hash散列函數(shù)對不同的值散列到table的同一個(gè)位置該如何處理?何時(shí)需要擴(kuò)容table的大小,分配一個(gè)更大容量的table?

下面這張網(wǎng)絡(luò)上流行的圖基本解釋了當(dāng)發(fā)生碰撞時(shí)的處理辦法,

1、HashMap的主要存儲為HashTable

2、當(dāng)散列到的位置已經(jīng)有元素存在時(shí),通過鏈表將當(dāng)前元素鏈接到table中的元素后面

3、當(dāng)鏈表長度太長(默認(rèn)超過8)時(shí),鏈表就轉(zhuǎn)換為紅黑樹,利用紅黑樹快速增刪改查的特點(diǎn)提高HashMap的性能。

紅黑樹的相關(guān)知識可以參考:算法導(dǎo)論 第三部分——基本數(shù)據(jù)結(jié)構(gòu)——紅黑樹

3.4 hashtable的擴(kuò)容

這里先列出了HashMap源碼中的幾個(gè)常量:

/*** 默認(rèn)hashtable的長度 16*/static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/*** hashtable的最大長度*/static final int MAXIMUM_CAPACITY = 1 << 30;/*** hashtable的默認(rèn)負(fù)載因子*/static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** 當(dāng)Hashtable中鏈表長度大于該值時(shí),將鏈表轉(zhuǎn)換成紅黑樹*/static final int TREEIFY_THRESHOLD = 8;

HashMap構(gòu)造函數(shù)可以傳入table的初始大小和負(fù)載因子的大小:

public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold =

tableSizeFor(initialCapacity);

}

這里有一個(gè)很巧妙牛逼的tableSizeFor算法:返回一個(gè)大于等于且最接近 cap 的2的冪次方整數(shù),如給定9,返回2的4次方16。它的具體實(shí)現(xiàn)(全部通過位運(yùn)算完成):

/*** Returns a power of two size for the given target capacity.*/static final int tableSizeFor(int cap) {int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}

那么關(guān)鍵的問題,什么時(shí)候會增大table的容量呢?原來table中的Node如何重新散列到新的table中?下面圍繞這兩個(gè)問題展開:

HashMap中有個(gè)成員變量 : threshhold,當(dāng)table中存放的node個(gè)數(shù)大于該值時(shí)就會調(diào)用resize()函數(shù),給table重新分配一個(gè)2倍的容量的數(shù)組(具體可能涉及很多邊界問題),并且將原來table中的元素重新散列到擴(kuò)容的新表中(個(gè)人猜想這過程應(yīng)該是非常耗時(shí)的,所以為了避免HashTable不斷擴(kuò)容的操作,使用者可以在構(gòu)造函數(shù)的時(shí)候預(yù)先設(shè)置一個(gè)較大容量的table)。

那么這個(gè)threshhold的值時(shí)如何計(jì)算的呢?

1、構(gòu)造函數(shù)的時(shí)候賦值: this.threshold = tableSizeFor(initialCapacity);

2、resize()的時(shí)候 threshold也會隨著table容量的翻倍而翻倍。

3、threshold 的初始值: DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY

這里有個(gè)疑問: 通過HashMap()和HashMap(int,int)兩種構(gòu)造函數(shù)得得到的threshold值計(jì)算方法不同,前一種永遠(yuǎn)是table.length * 0.75 第二種是通過tableSizeFor(cap)計(jì)算所得,為table.length 這時(shí)負(fù)載因子似乎失去了意義?

HashTable重新散列:

當(dāng)重新分配了一個(gè)table時(shí),需要將原來table中的Node重新散列到新的table中。源碼中針對hashtable、鏈表、紅黑樹中節(jié)點(diǎn)分別作了處理。

1. 如果是table中的值(next為null):直接映射到大的table中,剛看的時(shí)候沒理解為什么不需要判斷如果新位置已經(jīng)有元素怎么辦?

這里不需要考慮大的table中該節(jié)點(diǎn)已經(jīng)有Node了,比如和value | 1111 的元素只有一個(gè)(table中不是鏈表),那么 value | 11111 的元素也一定只有一個(gè)。(1111為擴(kuò)容前table長度減1,11111位擴(kuò)容后table長度減1)

在擴(kuò)充HashMap的時(shí)候,不需要像JDK1.7的實(shí)現(xiàn)那樣

2、 如果是鏈表中的值,則重新散列后他們可能有兩種不同的值(增加了一個(gè)異或位),需要重新散列到兩個(gè)位置。

java1.8 重新計(jì)算hash,只需要看看原來的hash值新增的那個(gè)bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,HashMap的源碼真的有太多精妙的地方了。

3、如果是紅黑樹中的節(jié)點(diǎn),重新散列后的值也可能出現(xiàn)兩種,需要對紅黑數(shù)進(jìn)行操作,重新散列(這一塊沒有具體看源碼)。

resize()函數(shù)源碼:

View Code

3.5 put方法分析

????? 介紹了上面的這么多下面分析put函數(shù)就不是那么難了:

JDK1.8HashMap的put方法源碼如下:

1 public V put(K key, V value) {2 // 對key的hashCode()做hash3 return putVal(hash(key), key, value, false, true);4 }56 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,7 boolean evict) {8 Node<K,V>[] tab; Node<K,V> p; int n, i;9 // 步驟①:tab為空則創(chuàng)建 10 if ((tab = table) == null || (n = tab.length) == 0) 11 n = (tab = resize()).length; 12 // 步驟②:計(jì)算index,并對null做處理 13 if ((p = tab[i = (n - 1) & hash]) == null) 14 tab[i] = newNode(hash, key, value, null); 15 else { 16 Node<K,V> e; K k; 17 // 步驟③:節(jié)點(diǎn)key存在,直接覆蓋value 18 if (p.hash == hash && 19 ((k = p.key) == key || (key != null && key.equals(k)))) 20 e = p; 21 // 步驟④:判斷該鏈為紅黑樹 22 else if (p instanceof TreeNode) 23 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 24 // 步驟⑤:該鏈為鏈表 25 else { 26 for (int binCount = 0; ; ++binCount) { 27 if ((e = p.next) == null) { 28 p.next = newNode(hash, key,value,null);//鏈表長度大于8轉(zhuǎn)換為紅黑樹進(jìn)行處理 29 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 30 treeifyBin(tab, hash); 31 break; 32 }// key已經(jīng)存在直接覆蓋value 33 if (e.hash == hash && 34 ((k = e.key) == key || (key != null && key.equals(k)))) break; 36 p = e; 37 } 38 } 39 40 if (e != null) { // existing mapping for key 41 V oldValue = e.value; 42 if (!onlyIfAbsent || oldValue == null) 43 e.value = value; 44 afterNodeAccess(e); 45 return oldValue; 46 } 47 }48 ++modCount; 49 // 步驟⑥:超過最大容量 就擴(kuò)容 50 if (++size > threshold) 51 resize(); 52 afterNodeInsertion(evict); 53 return null; 54 }

?

HashMap實(shí)際使用中注意點(diǎn):

當(dāng)HashMap的key值為自定義類型時(shí),需要重寫它的?equals() 和?hashCode() 兩個(gè)函數(shù)才能得到期望的結(jié)果。如下例所示:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

public class PhoneNumber

{

????private int prefix; //區(qū)號

????private int phoneNumber; //電話號

?

????public PhoneNumber(int prefix, int phoneNumber)

????{

????????this.prefix = prefix;

????????this.phoneNumber = phoneNumber;

????}

?

????@Override

????public boolean equals(Object o)

????{

????????if(this == o)

????????{

????????????return true;

????????}

????????if(!(o instanceof PhoneNumber))

????????{

????????????return false;

????????}

????????PhoneNumber pn = (PhoneNumber)o;

????????return pn.prefix == prefix && pn.phoneNumber == phoneNumber;

????}

?

????@Override

????public int hashCode()

????{

????????int result = 17;

????????result = 31 * result + prefix;

????????result = 31 * result + phoneNumber;

????????return result;

????}

}

這里有個(gè)疑問: 為什么在put() 一個(gè)元素時(shí),不直接調(diào)用equals() 判斷集合中是否存在相同的元素,而是先調(diào)用 hashCode() 看是否有相同hashCode() 元素再通過equal進(jìn)行確認(rèn)?

答: 這里是從效率的方面考慮的,一個(gè)集合中往往有大量的元素如果一個(gè)個(gè)調(diào)用equals比較必然效率很低。如果兩個(gè)元素相同他們的hashCode必然相等(反之不成立),先調(diào)用hashCode可以過濾大部分元素。

?

HashMap與ArrayMap的區(qū)別

??????? 由于HashMap在擴(kuò)容時(shí)需要重建hash table 是一件比較耗時(shí)的操作,為了優(yōu)化性能Androd的系統(tǒng)中提供了ArrayMap,當(dāng)容量較小時(shí)ArrayMap的性能更優(yōu)。

?????? ArrayMap使用的是數(shù)組存放key值和value值,擴(kuò)容時(shí)只需要重建一個(gè)size*2的數(shù)組讓后將之前的數(shù)據(jù)拷貝進(jìn)去,再新添新數(shù)據(jù)。但是ArrayMap也有缺點(diǎn): 它在每次put數(shù)據(jù)時(shí),如果這個(gè)key值map中不存在,那么都可能會涉及到數(shù)組的拷貝操作。

????? HashMap每次put、delete操作(不涉及擴(kuò)容或者容量重新分配)耗時(shí)較小,但是擴(kuò)容操作時(shí)較耗時(shí)。

????? ArrayMap每次put、delete操作耗時(shí),但是擴(kuò)容操作不那么耗時(shí)。

參考: http://www.cnblogs.com/NeilZhang/p/5657265.html http://www.importnew.com/20386.html ArrayMap :https://blog.csdn.net/hp910315/article/details/48634167

夢想不是浮躁,而是沉淀和積累

總結(jié)

以上是生活随笔為你收集整理的java容器类2:Map及HashMap深入解读的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

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