1. 程式人生 > >Java 集合:HashMap(put方法的實現 與 雜湊衝突)

Java 集合:HashMap(put方法的實現 與 雜湊衝突)

HashMap 概念

對於 Map ,最直觀就是理解就是鍵值對,對映,key-value 形式。一個對映不能包含重複的鍵,一個鍵只能有一個值。平常我們使用的時候,最常用的無非就是 HashMap。

HashMap 實現了 Map 介面,允許使用 null 值 和 null 鍵,並且不保證對映順序。

HashMap 有兩個引數影響效能:

  • 初始容量:表示雜湊表在其容量自動增加之前可以達到多滿的一種尺度
  • 載入因子:當雜湊表中的條目超過了容量和載入因子的乘積的時候,就會進行重雜湊操作。

如下成員變數原始碼:

static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
transient Node<K,V>[] table;

可以看到,預設載入因子為 0.75, 預設容量為 1 << 4,也就是 16。載入因子過高,容易產生雜湊衝突,載入因子過小,容易浪費空間,0.75是一種折中。

另外,整個 HashMap 的實現原理可以簡單的理解成:當我們 put 的時候,首先根據 key 算出一個數值 x,然後在 table[x] 中存放我們的值。這樣有一個好處是,以後的 get 等操作的時間複雜度直接就是O(1),因為 HashMap 內部就是基於陣列的一個實現。

put 方法的實現 與 雜湊衝突

下面再結合程式碼重點分析下 HashMap 的 put 方法的內部實現 和 雜湊衝突的解決辦法:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> 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<K,V>)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) // -1 for 1st
                        treeifyBin(tab, hash);
                        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;
}

首先我們看到 hash(key) 這個就是表示要根據 key 值算出一個數值,以此來決定在 table 陣列的哪一個位置存放我們的數值。(Ps:這個 hash(key) 方法 也是大有講究的,會嚴重影響效能,實現得不好會讓 HashMap 的 O(1) 時間複雜度降到 O(n),在JDK8以下的版本中帶來災難性影響。它需要保證得出的數在雜湊表中的均勻分佈,目的就是要減少雜湊衝突)

重要說明一下:

  • **JDK8 中雜湊衝突過多,連結串列會轉紅黑樹,時間複雜度是O(logn),不會是O(n) **
  • **JDK8 中雜湊衝突過多,連結串列會轉紅黑樹,時間複雜度是O(logn),不會是O(n) **
  • **JDK8 中雜湊衝突過多,連結串列會轉紅黑樹,時間複雜度是O(logn),不會是O(n) **

然後,我們再看到:

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
else {
    ......

這就表示,如果沒有 雜湊衝突,那麼就可以放入資料 tab[i] = newNode(hash, key, value, null); 如果有雜湊衝突,那麼就執行 else 需要解決雜湊衝突。

那麼放入資料 其實就是 建立一個 Node 節點,該 Node節點有屬性 key,value,分別儲存我們的 key 值 和 value 值,然後再把這個 Node 節點放入到 table 陣列中,並沒有什麼神祕的地方。

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

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

上述可以看到 Node 節點中 有一個 Node<K,V> next; ,其實仔細思考下就應該知道這個是用來解決雜湊衝突的。下面再看看是如何解決雜湊衝突的:

雜湊衝突:通俗的講就是首先我們進行一次 put 操作,算出了我們要在 table 陣列的 x 位置放入這個值。那麼下次再進行一個 put 操作的時候,又算出了我們要在 table 陣列的 x 位置放入這個值,那之前已經放入過值了,那現在怎麼處理呢?

其實就是通過連結串列法進行解決。

首先,如果有雜湊衝突,那麼:

if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
e = p;

需要判斷 兩者的 key 是否一樣的,因為 HashMap 不能加入重複的鍵。如果一樣,那麼就覆蓋,如果不一樣,那麼就先判斷是不是 TreeNode 型別的:

 else if (p instanceof TreeNode)
    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

這裡表示 是不是現在已經轉紅黑樹了(在大量雜湊衝突的情況下,連結串列會轉紅黑樹),一般我們小資料的情況下,是不會轉的,所以這裡暫時不考慮這種情況(Ps:本人也沒太深入研究紅黑樹,所以就不說這個了)。

如果是正常情況下,會執行下面的語句來解決雜湊衝突:

for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
        p.next = newNode(hash, key, value, null);
        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
        break;
    }
    if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
        break;
    p = e;
}

這裡其實就是用連結串列法來解決。並且:

  • 衝突的節點放在連結串列的最下面。
  • 衝突的節點放在連結串列的最下面。
  • 衝突的節點放在連結串列的最下面。

因為 首先有:p = tab[i = (n - 1) & hash] ,再 for 迴圈,然後有 if ((e = p.next) == null) { ,並且如果 當前節點的下一個節點有值的話,那麼就 p = e;,這就說明了放在最下面。

強烈建議自己拿筆拿紙畫畫。

總結

  • 一個對映不能包含重複的鍵,一個鍵只能有一個值。允許使用 null 值 和 null 鍵,並且不保證對映順序。
  • HashMap 解決衝突的辦法先是使用連結串列法,然後如果雜湊衝突過多,那麼會把連結串列轉換成紅黑樹,以此來保證效率。
  • 如果出現了雜湊衝突,那麼新加入的節點放在連結串列的最後面。

相關推薦

Java 集合HashMapput方法實現 衝突

HashMap 概念 對於 Map ,最直觀就是理解就是鍵值對,對映,key-value 形式。一個對映不能包含重複的鍵,一個鍵只能有一個值。平常我們使用的時候,最常用的無非就是 HashMap。 HashMap 實現了 Map 介面,允許使用 null 值 和 nu

Java集合HashMap原始碼剖析

一、HashMap概述   HashMap基於雜湊表的 Map 介面的實現。此實現提供所有可選的對映操作,並允許使用 null 值和 null 鍵。(除了不同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證對映的順序,特別是它不保證該順序恆久

Java 集合TreeMap工作原理及實現

前言 本文轉載自:點這裡,該部落格非常不錯,建議前去看看。 正文 1. 概述 A Red-Black tree based NavigableMap implementation. The map is sorted according to the natura

HashMapput()方法實現原理

突然想解剖HashMap實現原理,Map連結串列的作者原始碼如何實現?也可以豐富一下自己的程式設計思想,也想讓讀者看見如何觀看別人原始碼的思路和方法。所以心血來潮的我,就來解析HashMap底層原理! 送給讀者的話:一個合格的程式設計師一定要學會觀看別人程式碼

Java集合LinkedList JDK1.8 原始碼解讀

LinkedList介紹 還是和ArrayList同樣的套路,顧名思義,linked,那必然是基於連結串列實現的,連結串列是一種線性的儲存結構,將儲存的資料存放在一個儲存單元裡面,並且這個儲存單元裡面還維護了下一個儲存單元的地址。在LinkedList的連結串列儲存單元中,不僅存放了下一個儲存單元的地址,還存

修改tomcat埠號tomcat埠號其他應用衝突

在Tomcat的conf資料夾裡有個server.xml檔案,修改裡面的  <Connector port="8080" protocol="HTTP/1.1"                connectionTimeout="20000"            

Java容器HashMapJava 7實現原理

一、HashMap的定義和建構函式 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, S

Java 集合深入理解17HashMap 在 JDK 1.8 後新增的紅黑樹結構

上篇文章我們介紹了 HashMap 的主要特點和關鍵方法原始碼解讀,這篇文章我們介紹 HashMap 在 JDK1.8 新增樹形化相關的內容。 讀完本文你將瞭解到: 傳統 HashMap 的缺點 JDK 1.8 以前 HashMap 的實

java集合HashMapput方法

首先我們需要對hashmap初始換容器大小 HashMap< String, String > hashMap = new HashMap<>(16); 如果我們的寫的數字小於MAXIMUM_CAPACITY = 1 &

Java利用遞迴方法實現角谷定理

問題描述:        角谷定理。輸入一個自然數,若為偶數,則把它除以2,若為奇數,則把它乘以3加1。經過如此有限次運算後,總可以得到自然數值1。求經過多少次可得到自然數1。 如:輸入22, 輸出 22 11 34 17 52 26 1

java集合之----HashMap原始碼分析基於JDK1.71.8

一、什麼是HashMap 百度百科這樣解釋: 簡而言之,HashMap儲存的是鍵值對(key和value),通過key對映到value,具有很快的訪問速度。HashMap是非執行緒安全的,也就是說在多執行緒併發環境下會出現問題(死迴圈) 二、內部實現 (1)結構 HashM

hashmap實現原理(值計算,put方法,擴容) jdk1.8帶來的優化 hashmap併發安全 ConcurrentHashMap

HashMap的原始碼,實現原理,JDK8中對HashMap做了怎樣的優化。 ArrayList和LinkedList的優缺點——陣列的特點是:定址容易,插入和刪除困難;而連結串列的特點是:定址困難,插入和刪除容易。 hashmap底層

Java 集合原始碼解析1Iterator

Java 提供的 集合類都在 Java.utils 包下,其中包含了很多 List, Set, Map, Queue… 它們的關係如下面這張類圖所示: 可以看到,Java 集合主要分為兩類:Collection 和 Map. 而 Collection 又繼承了 Iter

Java 集合深入理解4List 介面

在 Java 集合深入理解:Collection 中我們熟悉了 Java 集合框架的基本概念和優點,也瞭解了根介面之一的 Collection,這篇文章來加深 Collection 的子介面之一 List 的熟悉。 List 介面 一個 List 是一個元素有

井字棋遊戲實現-java低階方法實現

import java.util.Scanner; public class JingGame  {     final static int LENGTH=3; //棋局的長度     final static int NUM_FOR_WIN=3;//幾個子連起來贏   

JAVA集合迭代器實現原理

前言 在JAVA的學習和開發中,經常需要對集合或者陣列進行遍歷,遍歷的方法有多種:for迴圈、foreach、迭代器。 for迴圈的實現簡單明瞭,就是迴圈下標,判斷邊界,取到每個下標的資料。至於foreach和迭代器,其實foreach在反編譯以後可以看到就是迭代器實現的,因此,今天來學

散列表衝突處理的方法之開地址法二次探測再實現

#include "hash.h"#include "common.h"#include <assert.h>typedef enum entry_status {     EMPTY,     ACTIVE,     DELETED } entry_status_t;typedef struct

深度解析Java 8JDK1.8 AbstractQueuedSynchronizer的實現分析

前言 Java中的FutureTask作為可非同步執行任務並可獲取執行結果而被大家所熟知。通常可以使用future.get()來獲取執行緒的執行結果,線上程執行結束之前,get方法會一直阻塞狀態,直到call()返回,其優點是使用執行緒非同步執行任務的情況下還可以獲取到

好使Java集合中的Collections.sort方法對list排序的兩種方法

      ret = String.valueOf(m2.invoke(((E)b), null).toString().length()).compareTo(String.valueOf(m1.invoke(((E)a), null).toString().length()));          if

Java 集合深入理解3Collection

今天心情有點粉,來學學 Collection 吧! 什麼是集合? 集合,或者叫容器,是一個包含多個元素的物件; 集合可以對資料進行儲存,檢索,操作; 它們可以把許多個體組織成一個整體: 比如一副撲克牌(許多牌組成的集合); 比如一個電話本