java基礎進階篇(四)_HashMap------【java源碼棧】


一.前言

  HashMap也是我們使用非常多的Collection,它是基於哈希表的 Map 接口的實現,以key-value的形式存在。在HashMap中,key-value總是會當做一個整體來處理,系統會根據hash算法來來計算key-value的存儲位置,我們總是可以通過key快速地存、取value。

二.特點和常見問題

  • 非線程安全
  • hashMap的映射不是有序的
  • key、value都可以為null
  • key 不能重復

二.接口定義

  查看源碼. HashMap實現了Map接口,繼承AbstractMap。

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

三.初始化構造函數

  HashMap提供了三個構造函數:

1.HashMap();

  構造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap。

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 * 一個空的構造方法,默認初始化容量16,負載因子0.75
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

  負載因子: loadFactor是map的負載因子,要大於0,且是非無窮大的數字.

  有這樣一個公式:initailCapacity*loadFactor=HashMap的容量.

  比如,泛型是String,String 類型的空的HashMap.(泛型: 集合內元素的類型)

HashMap map1 = new HashMap<String,String>();

2.HashMap(int initialCapacity);

  構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

  比如初始一個容量為5的hashMap集合.

HashMap map2 = new HashMap<String,String>(5);
System.out.println(map2.size());// 輸出0, 此時數組容量為5, 但內部元素為0.

3.HashMap(int initialCapacity, float loadFactor);

  構造一個帶指定初始容量和加載因子的空 HashMap。

  默認負載因子是0.75, HashMap提供可修改的負載因子的構造方法.

HashMap map2 = new HashMap<String,String>(5,0.5f);

四.HashMap內部結構

  數據結構的物理存儲結構只有兩種:順序存儲結構和鏈式存儲結構(像棧,隊列,樹,圖等是從邏輯結構去抽象的,映射到內存中,也這兩種物理組織形式),在數組中根據下標查找某個元素,一次定位就可以達到,哈希表利用了這種特性,哈希表的主干就是數組。

  簡單概括主干是數組, 每個數組元素內容部是鏈表.

  源碼如下:

public HashMap(int initialCapacity, float loadFactor) {
	//初始容量不能<0
	if (initialCapacity < 0)
	throw new IllegalArgumentException("Illegal initial capacity: "
	+ initialCapacity);
	//初始容量不能 > 最大容量值,HashMap的最大容量值為2^30
	if (initialCapacity > MAXIMUM_CAPACITY)
	initialCapacity = MAXIMUM_CAPACITY;
	//負載因子不能 < 0
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
	throw new IllegalArgumentException("Illegal load factor: "
	+ loadFactor);

	// 計算出大於 initialCapacity 的最小的 2 的 n 次方值。
	int capacity = 1;
	while (capacity < initialCapacity)
	capacity <<= 1;

	this.loadFactor = loadFactor;
	//設置HashMap的容量極限,當HashMap的容量達到該極限時就會進行擴容操作
	threshold = (int) (capacity * loadFactor);
	//初始化table數組
	table = new Entry[capacity];
	init();
}

  從源碼中可以看出,每次新建一個HashMap時,都會初始化一個table數組。table數組的元素為Entry節點。

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash;

    /**
     * Creates new entry.
     */
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    .......
}

  其中Entry為HashMap的內部類,它包含了鍵key、值value、下一個節點next,以及hash值,這是非常重要的,正是由於Entry才構成了table數組的項為鏈表。

  隨着HashMap中元素的數量越來越多,發生碰撞的概率就越來越大,所產生的鏈表長度就會越來越長,這樣勢必會影響HashMap的速度,為了保證HashMap的效率,系統必須要在某個臨界點進行擴容處理。該臨界點在當HashMap中元素的數量等於table數組長度*加載因子。但是擴容是一個非常耗時的過程,因為它需要重新計算這些數據在新table數組中的位置並進行復制處理。所以如果我們已經預知HashMap中元素的個數,那么預設元素的個數能夠有效的提高HashMap的性能。

五.HashMap的存儲分析

  源碼如下:

public V put(K key, V value) {
    //當key為null,調用putForNullKey方法,保存null與table第一個位置中,這是HashMap允許為null的原因
    if (key == null)
        return putForNullKey(value);
    //計算key的hash值
    int hash = hash(key.hashCode());                  ------(1)
        //計算key hash 值在 table 數組中的位置
        int i = indexFor(hash, table.length);             ------(2)
        //從i出開始迭代 e,找到 key 保存的位置
        for (Entry<K, V> e = table[i]; e != null; e = e.next) {
            Object k;
            //判斷該條鏈上是否有hash值相同的(key相同)
            //若存在相同,則直接覆蓋value,返回舊value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;    //舊值 = 新值
                e.value = value;
                e.recordAccess(this);
                return oldValue;     //返回舊值
            }
        }
    //修改次數增加1
    modCount++;
    //將key、value添加至i位置處
    addEntry(hash, key, value, i);
    return null;
}

  分析下源碼中保存數據的過程為:

  首先判斷key是否為null,若為null,則直接調用putForNullKey方法。

  若不為空則先計算key的hash值,然后根據hash值搜索在table數組中的索引位置,如果table數組在該位置處有元素,則通過比較是否存在相同的key,若存在則覆蓋原來key的value,否則將該元素保存在鏈頭(最先保存的元素放在鏈尾)。

  若table在該處沒有元素,則直接保存。

  HashMap中不存在相同的Key, 先看迭代處。此處迭代原因就是為了防止存在相同的key值,若發現兩個hash值(key)相同時,HashMap的處理方式是用新value替換舊value,這里並沒有處理key,這就解釋了HashMap中沒有兩個相同的key。

  再來分析下添加一個新key-value 的內部實現:

void addEntry(int hash, K key, V value, int bucketIndex) {
    //獲取bucketIndex處的Entry
    Entry<K, V> e = table[bucketIndex];
    //將新創建的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry 
    table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
    //若HashMap中元素的個數超過極限了,則容量擴大兩倍
    if (size++ >= threshold)
        resize(2 * table.length);
}

  這是一個非常優雅的設計。系統總是將新的Entry對象添加到bucketIndex處。如果bucketIndex處已經有了對象,那么新添加的Entry對象將指向原有的Entry對象,形成一條Entry鏈,但是若bucketIndex處沒有Entry對象,也就是e==null,那么新添加的Entry對象指向null,也就不會產生Entry鏈了

六.HashMap的讀取分析

  相對於HashMap的存而言,取就顯得比較簡單了。通過key的hash值找到在table數組中的索引處的Entry,然后返回該key對應的value即可。

public V get(Object key) {
    // 若為null,調用getForNullKey方法返回相對應的value
    if (key == null)
        return getForNullKey();
    // 根據該 key 的 hashCode 值計算它的 hash 碼  
    int hash = hash(key.hashCode());
    // 取出 table 數組中指定索引處的值
    for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
        Object k;
        //若搜索的key與查找的key相同,則返回相對應的value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}

  能夠根據key快速的取到value除了和HashMap的數據結構密不可分外,還和Entry有莫大的關系,在前面就提到過,HashMap在存儲過程中並沒有將key,value分開來存儲,而是當做一個整體key-value來處理的,這個整體就是Entry對象。同時value也只相當於key的附屬而已。在存儲的過程中,系統根據key的hashcode來決定Entry在table數組中的存儲位置,在取的過程中同樣根據key的hashcode取出相對應的Entry對象。

七.常用方法

1.put(K key, V value)

  將鍵(key)/值(value)映射存放到Map集合中。若key已經存在,則更新替換掉舊值.

HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("key1", "val01");
map1.put("key2", "val02");
System.out.println(map1.get("key1"));
System.out.println(map1.get("b"));
map1.put("key1", "updated");
System.out.println(map1.get("key1"));

輸出結果

val01
null
updated

2.putAll(Map<? extends K, ? extends V> m)

  合並集合,把參數集合中的元素合並到原來集合中, 若參數集合中的key 與原來集合中有重復, 則更新覆蓋原來集合對應key 的value.

// 兩個map具有不同的key
HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "A");
HashMap<String, String> map2 = new HashMap<String, String>();
map2.put("2", "B");
map2.put("3", "C");
map1.putAll(map2);
System.out.println(map1);

// 兩個map具有重復的key
HashMap<String, String> map3 = new HashMap<String, String>();
map3.put("1", "A");
HashMap<String, String> map4 = new HashMap<String, String>();
map4.put("1", "B");
map4.put("3", "C");
map3.putAll(map4);
System.out.println(map3);

輸出結果:

{1=A, 2=B, 3=C}
{1=B, 3=C}

2.get(Object key)

  返回指定鍵所映射的值,沒有該key對應的值則返回 null。

HashMap<String,String> map2=new HashMap<String,String>();
map2.put("key1","val01");
map2.put("key2", "val02");
System.out.println(map2.get("key1"));
System.out.println(map2.get("key2"));

  輸出結果:

val01
val02

3.size()

  返回Map集合中數據數量。

HashMap<String,String> map2=new HashMap<String,String>();
map2.put("key1","val01");
map2.put("key2", "val02");

System.out.println(map2.size());

  輸出結果:

2

4.clear()

  清空Map集合

HashMap<String,String> map3=new HashMap<String,String>();
map3.put("key1","val01");
map3.put("key2", "val02");
System.out.println(map3.size());
map3.clear();
System.out.println(map3.size());

  輸出結果:

2
0

5.isEmpty ()

  判斷Map集合中是否有數據,如果沒有則返回true,否則返回false; 觀察源碼,僅判斷元素個數是否為0, 不能判斷null.

public boolean isEmpty() {
    return size == 0;
}

6.remove(Object key)

  刪除Map集合中鍵為key的數據並返回其所對應value值。

HashMap<String,String> map4=new HashMap<String,String>();
map4.put("key1","val01");
map4.put("key2", "val02");
System.out.println(map4.remove("key1"));
System.out.println(map4.size());

  輸出結果:

val01
1

7.values()

  返回Map集合中所有value組成的以Collection數據類型格式數據。

HashMap<String,String> map4=new HashMap<String,String>();
map4.put("key1","val01");
map4.put("key2", "val02");
System.out.println(map4.values());

  輸出結果:

[val01, val02]

8.keySet()

  返回Map集合中所有key組成的Set集合

HashMap<String,String> map4=new HashMap<String,String>();
map4.put("key1","val01");
map4.put("key2", "val02");
System.out.println(map4.keySet());

  輸出結果:

[key1, key2]

9.containsKey(Object key)

  判斷集合中是否包含指定鍵,包含返回 true,否則返回false.

HashMap<String,String> map4=new HashMap<String,String>();
map4.put("key1","val01");
map4.put("key2", "val02");
System.out.println(map4.containsKey("key1"));
System.out.println(map4.containsKey("key3"));

  輸出結果:

true
false

10.containsValue(Object value)

  判斷集合中是否包含指定值,包含返回 true,否則返回false。

HashMap<String,String> map4=new HashMap<String,String>();
map4.put("key1","val01");
map4.put("key2", "val02");
System.out.println(map4.containsValue("val01"));
System.out.println(map4.containsValue("val03"));

  輸出結果:

true
false

八.HashMap 的java8 新特性

1.特性

  • 非線程安全

  • hashMap的映射不是有序的

  • key、value都可以為null

    HashMap<String, String> map1 = new HashMap<String, String>();
    map1.put(null, null);
    System.out.println(map1);
    

  輸出結果:

{null=null}

2.V replace(K key, V value)

  替換指定key 的value, 並返回替換前的value.

HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");
System.out.println(map1);

System.out.println(map1.replace("1", "aReplace"));
System.out.println(map1);

  輸出結果

{1=a, 2=b, 3=c}
a

3.boolean replace(key, oldValue, newValue)

  用newValue 替換指定key 的oldValue; 當key 不存在 | 對應key的value 和oldValue 不相等 則返回false;

  源碼如下:

@Override
public boolean replace(K key, V oldValue, V newValue) {
    Node<K,V> e; V v;
    if ((e = getNode(hash(key), key)) != null &&
        ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
        e.value = newValue;
        afterNodeAccess(e);
        return true;
    }
    return false;
}

  例子:

HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");
// key 不存在
System.out.println(map1.replace("4", "bb", "newValue"));
// 參數oldValue 和對應key 的值不相等
System.out.println(map1.replace("2", "bb", "newValue"));
System.out.println(map1);
// 條件滿足
System.out.println(map1.replace("2", "b", "newValue"));
System.out.println(map1);

  輸出結果:

false
false
{1=a, 2=b, 3=c}
true
{1=a, 2=newValue, 3=c}

4.void replaceAll(function)

  function, java8的流式寫法, 個人感覺個replaceAll 沒一毛錢關系, function由我們自定義的.

HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");

map1.replaceAll((k,v) -> {
    System.out.println(k);
    System.out.println(v);
    return null;
});

  (k,v) k v 對應HashMap 中的key 和value. 內部是一次遍歷操作.

  輸出結果:

1a2b3c

5.putIfAbsent(key, value)

  如果傳入的key 已經存在, 則返回存在key 的value, 不進行更新替換value;

  如果傳入的key 不存在, 則和put 作用相同, 添加新的key 和value; 並且固定返回null;

HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");
System.out.println("執行前: " + map1);
// 1.key不存在
System.out.println(map1.putIfAbsent("4", "d"));
// 2.key存在, value不同
System.out.println(map1.putIfAbsent("1", "aa"));
System.out.println("執行后: " + map1);

  輸出結果:

執行前: {1=a, 2=b, 3=c}
null
a
執行后: {1=a, 2=b, 3=c, 4=d}

6.getOrDefault

  作用是根據傳入的key 獲取value; 若Map 中沒有這個key 則返回指定的defaultValue; 若有,則相當於get方法.

  源碼如下, 內部采用三元運算符進行判斷key 是否存在決定返回的value.

@Override
public V getOrDefault(Object key, V defaultValue) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}

  比如:

HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");

System.out.println(map1.getOrDefault("4", "d"));
System.out.println(map1.getOrDefault("1", "a"));

  輸出結果:

d
a

7.merge

  如果key存在,則執行lambda表達式,並將表達式的值更新到對應的key 中. 表達式入參為oldVal和newVal(neVal即merge()的第二個參數)。表達式返回最終put的val。如果key不存在,則直接putnewVal.

  比如:

HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");

String retVal = map1.merge("1", "A", (oldVal, newVal) -> oldVal + newVal);
System.out.println(retVal);
System.out.println(map1);

  輸出結果:

aA
{1=aA, 2=b, 3=c}

  作用等同於下段代碼(JDK1.7以前版本)

if(map.containsKey(k)) {
    map.put(k, map.get(k) + newVal);
} else {
    map.put(k, newVal);
}

  源碼如下, 解釋見注釋部分.

public V merge(K key, V value,
               BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    // 刪除無關代碼
        int hash = hash(key);
    Node<K,V>[] tab; Node<K,V> first; int n, i;
    int binCount = 0;
    TreeNode<K,V> t = null;
    Node<K,V> old = null; // 該key原來的節點對象
    if (size > threshold || (tab = table) == null ||
        (n = tab.length) == 0) //第一個if,判斷是否需要擴容
        n = (tab = resize()).length;
    if ((first = tab[i = (n - 1) & hash]) != null) {
        // 第二個if,取出old Node對象
        // 繼續省略
    }
    if (old != null) {// 第三個if,如果 old Node 存在
        V v;
        if (old.value != null)
            // 如果old存在,執行lambda,算出新的val並寫入old Node后返回。
            v = remappingFunction.apply(old.value, value);
        else
            v = value;
        if (v != null) {

            old.value = v;
            afterNodeAccess(old);
        }
        else
            removeNode(hash, key, null, false, true);
        return v;
    }
    if (value != null) {
        //如果old不存在且傳入的newVal不為null,則put新的kv
        if (t != null)
            t.putTreeVal(this, tab, hash, key, value);
        else {
            tab[i] = newNode(hash, key, value, first);
            if (binCount >= TREEIFY_THRESHOLD - 1)
                treeifyBin(tab, hash);
        }
        // 省略
    }
    return value;
}

8.compute()

  根據已知的 k v 算出新的v並put。如果無此key,那么oldVal為null,lambda中涉及到oldVal的計算會報空指針。源碼和merge大同小異,就不放了。

HashMap<String, String> map1 = new HashMap<String, String>();
map1.put("1", "a");
map1.put("2", "b");
map1.put("3", "c");

String retVal = map1.compute("1", (key, oldVal) -> oldVal + "AAA");
System.out.println(retVal);
System.out.println(map1);

  輸出結果:

aAAA
{1=aAAA, 2=b, 3=c}

  和merge 方法相比, 功能類似, 區別在於傳入的參數不同;

9.compute() 的補充方法

1) computeIfAbsent()

  當key不存在時,才compute[見8]. 類似compute ,故偷個懶不作例子了

2) computeIfPresent()

  當key存在時,才compute[見8].類似compute ,故偷個懶不作例子了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM