欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java集合系列之HashMap源碼分析

 更新時(shí)間:2018年02月27日 14:25:07   作者:勞夫子  
這篇文章主要為大家詳細(xì)介紹了Java集合系列之HashMap源碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下

前面我們已經(jīng)分析了ArrayList和LinkedList這兩個(gè)集合,我們知道ArrayList是基于數(shù)組實(shí)現(xiàn)的,LinkedList是基于鏈表實(shí)現(xiàn)的。它們各自有自己的優(yōu)劣勢(shì),例如ArrayList在定位查找元素時(shí)會(huì)優(yōu)于LinkedList,而LinkedList在添加刪除元素時(shí)會(huì)優(yōu)于ArrayList。而本篇介紹的HashMap綜合了二者的優(yōu)勢(shì),它的底層是基于哈希表實(shí)現(xiàn)的,如果不考慮哈希沖突的話,HashMap在增刪改查操作上的時(shí)間復(fù)雜度都能夠達(dá)到驚人的O(1)。我們先看看它所基于的哈希表的結(jié)構(gòu)。

從上圖中可以看到,哈希表是由數(shù)組和鏈表共同構(gòu)成的一種結(jié)構(gòu),當(dāng)然上圖是一個(gè)不好的示例,一個(gè)好的哈希函數(shù)應(yīng)該要盡量平均元素在數(shù)組中的分布,減少哈希沖突從而減小鏈表的長(zhǎng)度。鏈表的長(zhǎng)度越長(zhǎng),意味著在查找時(shí)需要遍歷的結(jié)點(diǎn)越多,哈希表的性能也就越差。接下來我們來看下HashMap的部分成員變量。

//默認(rèn)初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

//默認(rèn)最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

//默認(rèn)加載因子, 指哈希表可以達(dá)到多滿的尺度
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//空的哈希表
static final Entry<?,?>[] EMPTY_TABLE = {};

//實(shí)際使用的哈希表
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

//HashMap大小, 即HashMap存儲(chǔ)的鍵值對(duì)數(shù)量
transient int size;

//鍵值對(duì)的閾值, 用于判斷是否需要擴(kuò)增哈希表容量
int threshold;

//加載因子
final float loadFactor;

//修改次數(shù), 用于fail-fast機(jī)制
transient int modCount;

//使用替代哈希的默認(rèn)閥值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

//隨機(jī)的哈希種子, 有助于減少哈希碰撞的次數(shù)
transient int hashSeed = 0;

由成員變量中看到,HashMap默認(rèn)的初始容量為16,默認(rèn)的加載因子是0.75。而threshold是集合能夠存儲(chǔ)的鍵值對(duì)的閥值,默認(rèn)是初始容量*加載因子,也就是16*0.75=12,當(dāng)鍵值對(duì)要超過閥值時(shí),意味著這時(shí)候的哈希表已處于飽和狀態(tài),再繼續(xù)添加元素就會(huì)增加哈希沖突,從而使HashMap的性能下降。這時(shí)會(huì)觸發(fā)自動(dòng)擴(kuò)容機(jī)制,以保證HashMap的性能。我們還可以看到哈希表其實(shí)就是一個(gè)Entry數(shù)組,數(shù)組存放的每個(gè)Entry都是單向鏈表的頭結(jié)點(diǎn)。這個(gè)Entry是HashMap的靜態(tài)內(nèi)部類,來看看Entry的成員變量。

static class Entry<K,V> implements Map.Entry<K,V> {
  final K key;   //鍵
  V value;     //值
  Entry<K,V> next; //下一個(gè)Entry的引用
  int hash;     //哈希碼
  
  ...        //省略下面代碼
}

一個(gè)Entry實(shí)例就是一個(gè)鍵值對(duì),里面包含了key和value,同時(shí)每個(gè)Entry實(shí)例還有一個(gè)指向下一個(gè)Entry實(shí)例的引用。為了避免重復(fù)計(jì)算,每個(gè)Entry實(shí)例還存放了對(duì)應(yīng)的Hash碼??梢哉fEntry數(shù)組就是HashMap的核心,所有的操作都是針對(duì)這個(gè)數(shù)組來完成的。由于HashMap源碼比較長(zhǎng),不可能面面俱到的介紹它的所有方法,因此我們只抓住重點(diǎn)來進(jìn)行介紹。接下來我們將以問題為導(dǎo)向,針對(duì)下面幾個(gè)問題深入探究HashMap的內(nèi)部機(jī)制。

1. HashMap在構(gòu)造時(shí)做了哪些操作?

//構(gòu)造器, 傳入初始化容量和加載因子
public HashMap(int initialCapacity, float loadFactor) {
  if (initialCapacity < 0) {
    throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
  }
  //如果初始化容量大于最大容量, 就把它設(shè)為最大容量
  if (initialCapacity > MAXIMUM_CAPACITY) {
    initialCapacity = MAXIMUM_CAPACITY;
  }
  //如果加載因子小于0或者加載因子不是浮點(diǎn)數(shù), 則拋出異常
  if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
    throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
  }
  //設(shè)置加載因子
  this.loadFactor = loadFactor;
  //閾值為初始化容量
  threshold = initialCapacity;
  init();
}

void init() {}

所有的構(gòu)造器都會(huì)調(diào)用到這個(gè)構(gòu)造器,在這個(gè)構(gòu)造器中我們看到除了對(duì)參數(shù)做一些校驗(yàn)之外,它就做了兩件事,設(shè)置加載因子為傳入的加載因子,設(shè)置閥值為傳入的初始化大小。而init方法是空的,啥也沒做。注意,這時(shí)候并沒有根據(jù)傳入的初始化大小去新建一個(gè)Entry數(shù)組哦。那在什么時(shí)候再去新建數(shù)組呢?繼續(xù)往下看。

2. HashMap添加鍵值對(duì)時(shí)會(huì)進(jìn)行什么操作?

//放置key-value鍵值對(duì)到HashMap中
public V put(K key, V value) {
  //如果哈希表沒有初始化就進(jìn)行初始化
  if (table == EMPTY_TABLE) {
    //初始化哈希表
    inflateTable(threshold);
  }
  if (key == null) {
    return putForNullKey(value);
  }
  //計(jì)算key的hash碼
  int hash = hash(key);
  //根據(jù)hash碼定位在哈希表的位置
  int i = indexFor(hash, table.length);
  for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    Object k;
    //如果對(duì)應(yīng)的key已經(jīng)存在, 就替換它的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;
    }
  }
  modCount++;
  //如果沒有對(duì)應(yīng)的key就添加Entry到HashMap中
  addEntry(hash, key, value, i);
  //添加成功返回null
  return null;
}

看到,在添加鍵值對(duì)時(shí)會(huì)先檢查哈希表是否是個(gè)空表,如果是空表就進(jìn)行初始化。之后再進(jìn)行后續(xù)操作,調(diào)用哈希函數(shù)計(jì)算傳入的key的Hash碼。根據(jù)hash碼定位到Entry數(shù)組的指定槽位,然后對(duì)該槽位的單向鏈表進(jìn)行遍歷,如果傳入的已經(jīng)存在了就進(jìn)行替換操作,否則就新建一個(gè)Entry添加到哈希表中。

3. 哈希表是怎樣初始化的?

//初始化哈希表, 會(huì)對(duì)哈希表容量進(jìn)行膨脹, 因?yàn)橛锌赡軅魅氲娜萘坎皇?的冪
private void inflateTable(int toSize) {
  //哈希表容量必須是2的次冪
  int capacity = roundUpToPowerOf2(toSize);
  //設(shè)置閥值, 這里一般是取capacity*loadFactor
  threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
  //新建指定容量的哈希表
  table = new Entry[capacity];
  //初始化哈希種子
  initHashSeedAsNeeded(capacity);
}

上面我們知道,在構(gòu)造HashMap時(shí)不會(huì)新建Entry數(shù)組,而是在put操作時(shí)檢查當(dāng)前哈希表是否是個(gè)空表,如果是空表就調(diào)用inflateTable方法進(jìn)行初始化。上面貼出了這個(gè)方法的代碼,可以看到方法內(nèi)部會(huì)重新計(jì)算Entry數(shù)組的容量,因?yàn)樵跇?gòu)造HashMap時(shí)傳入的初始化大小可能不是2的冪,因此要將這個(gè)數(shù)轉(zhuǎn)換成2的冪再去根據(jù)新的容量新建Entry數(shù)組。初始化哈希表時(shí)再次重新設(shè)置閥值,閥值一般是capacity*loadFactor。此外,在初始化哈希表時(shí)還會(huì)去初始化哈希種子(hashSeed),這個(gè)hashSeed用于優(yōu)化哈希函數(shù),默認(rèn)為0是不使用替代哈希算法,但是也可以自己去設(shè)置hashSeed的值,以達(dá)到優(yōu)化效果。具體下面會(huì)講到。

4. HashMap在什么時(shí)候判斷是否要擴(kuò)容,以及它是怎樣擴(kuò)容的?

//添加Entry方法, 先判斷是否要擴(kuò)容
void addEntry(int hash, K key, V value, int bucketIndex) {
  //如果HashMap的大小大于閥值并且哈希表對(duì)應(yīng)槽位的值不為空
  if ((size >= threshold) && (null != table[bucketIndex])) {
    //因?yàn)镠ashMap的大小大于閥值, 表明即將發(fā)生哈希沖突, 所以進(jìn)行擴(kuò)容
    resize(2 * table.length);
    hash = (null != key) ? hash(key) : 0;
    bucketIndex = indexFor(hash, table.length);
  }
  //在這里表明HashMap的大小沒有超過閥值, 所以不需要擴(kuò)容
  createEntry(hash, key, value, bucketIndex);
}

//對(duì)哈希表進(jìn)行擴(kuò)容
void resize(int newCapacity) {
  Entry[] oldTable = table;
  int oldCapacity = oldTable.length;
  //如果當(dāng)前已經(jīng)是最大容量就只能增大閥值了
  if (oldCapacity == MAXIMUM_CAPACITY) {
    threshold = Integer.MAX_VALUE;
    return;
  }
  //否則就進(jìn)行擴(kuò)容
  Entry[] newTable = new Entry[newCapacity];
  //遷移哈希表的方法
  transfer(newTable, initHashSeedAsNeeded(newCapacity));
  //將當(dāng)前哈希表設(shè)置為新的哈希表
  table = newTable;
  //更新哈希表閾值
  threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

在調(diào)用put方法添加一個(gè)鍵值對(duì)時(shí),如果集合中沒有存在的key就去調(diào)用addEntry方法新建一個(gè)Entry??吹缴厦尜N出的addEntry代碼,在新建一個(gè)Entry之前會(huì)先判斷當(dāng)前集合元素的大小是否超過了閥值,如果超過了閥值就調(diào)用resize進(jìn)行擴(kuò)容。傳入的新的容量是原來哈希表的兩倍,在resize方法內(nèi)部會(huì)新建一個(gè)容量為原先的2倍的Entry數(shù)組。然后將舊的哈希表里面的元素全部遷移到新的哈希表,其中可能會(huì)進(jìn)行再哈希,根據(jù)initHashSeedAsNeeded方法計(jì)算的值來確定是否進(jìn)行再哈希。完成哈希表的遷移之后,將當(dāng)前哈希表替換為新的,最后再根據(jù)新的哈希表容量來重新計(jì)算HashMap的閥值。

5. 為什么Entry數(shù)組的大小必須為2的冪?

 //返回哈希碼對(duì)應(yīng)的數(shù)組下標(biāo)
 static int indexFor(int h, int length) {
   return h & (length-1);
 }

indexFor方法是根據(jù)hash碼來計(jì)算出在數(shù)組中對(duì)應(yīng)的下標(biāo)。我們可以看到在這個(gè)方法內(nèi)部使用了與(&)操作符。與操作是對(duì)兩個(gè)操作數(shù)進(jìn)行位運(yùn)算,如果對(duì)應(yīng)的兩個(gè)位都為1,結(jié)果才為1,否則為0。與操作經(jīng)常會(huì)用于去除操作數(shù)的高位值,例如:01011010 & 00001111 = 00001010。我們繼續(xù)回到代碼中,看看h&(length-1)做了些什么。

已知傳入的length是Entry數(shù)組的長(zhǎng)度,我們知道數(shù)組下標(biāo)是從0開始計(jì)算的,所以數(shù)組的最大下標(biāo)為length-1。如果length為2的冪,那么length-1的二進(jìn)制位后面都為1。這時(shí)h&(length-1)的作用就是去掉了h的高位值,只留下h的低位值來作為數(shù)組的下標(biāo)。由此可以看到Entry數(shù)組的大小規(guī)定為2的冪就是為了能夠使用這個(gè)算法來確定數(shù)組的下標(biāo)。

6. 哈希函數(shù)是怎樣計(jì)算Hash碼的?

//生成hash碼的函數(shù)
final int hash(Object k) {
  int h = hashSeed;
  //key是String類型的就使用另外的哈希算法
  if (0 != h && k instanceof String) {
    return sun.misc.Hashing.stringHash32((String) k);
  }
  h ^= k.hashCode();
  //擾動(dòng)函數(shù)
  h ^= (h >>> 20) ^ (h >>> 12);
  return h ^ (h >>> 7) ^ (h >>> 4);
}

hash方法的最后兩行是真正計(jì)算hash值的算法,計(jì)算hash碼的算法被稱為擾動(dòng)函數(shù),所謂的擾動(dòng)函數(shù)就是把所有東西雜糅到一起,可以看到這里使用了四個(gè)向右移位運(yùn)算。目的就是將h的高位值與低位值混合一下,以此增加低位值的隨機(jī)性。在上面我們知道定位數(shù)組的下標(biāo)是根據(jù)hash碼的低位值來確定的。key的hash碼是通過hashCode方法來生成的,而一個(gè)糟糕的hashCode方法生成的hash碼的低位值可能會(huì)有很大的重復(fù)。為了使得hash碼在數(shù)組上映射的比較均勻,擾動(dòng)函數(shù)就派上用場(chǎng)了,把高位值的特性糅合進(jìn)低位值,增加低位值的隨機(jī)性,從而使散列分布的更加松散,以此提高性能。下圖舉了個(gè)例子幫助理解。

7. 替代哈希是怎么回事?

我們看到hash方法中首先會(huì)將hashSeed賦值給h。這個(gè)hashSeed就是哈希種子,它是一個(gè)隨機(jī)的值,作用就是幫助優(yōu)化哈希函數(shù)。hashSeed默認(rèn)是0,也就是默認(rèn)不使用替代哈希算法。那么什么時(shí)候使用hashSeed呢?首先需要設(shè)置開啟替代哈希,在系統(tǒng)屬性中設(shè)置jdk.map.althashing.threshold的值,在系統(tǒng)屬性中這個(gè)值默認(rèn)是-1,當(dāng)它是-1的時(shí)候使用替代哈希的閥值為Integer.MAX_VALUE。這也意味著可能你永遠(yuǎn)也不會(huì)使用替代哈希了。當(dāng)然你可以把這個(gè)閥值設(shè)小一點(diǎn),這樣當(dāng)集合元素達(dá)到閥值后就會(huì)生成一個(gè)隨機(jī)的hashSeed。以此增加hash函數(shù)的隨機(jī)性。為什么要使用替代哈希呢?當(dāng)集合元素達(dá)到你設(shè)定的閥值之后,意味著哈希表已經(jīng)比較飽和了,出現(xiàn)哈希沖突的可能性就會(huì)大大增加,這時(shí)對(duì)再添加進(jìn)來的元素使用更加隨機(jī)的散列函數(shù)能夠使后面添加進(jìn)來的元素更加隨機(jī)的分布在散列表中。

注:以上分析全部基于JDK1.7,不同版本之間會(huì)有較大的改動(dòng),讀者需要注意。

相關(guān)文章

  • Java數(shù)據(jù)結(jié)構(gòu)之復(fù)雜度篇

    Java數(shù)據(jù)結(jié)構(gòu)之復(fù)雜度篇

    算法復(fù)雜度分為時(shí)間復(fù)雜度和空間復(fù)雜度。其作用:?時(shí)間復(fù)雜度是度量算法執(zhí)行的時(shí)間長(zhǎng)短;而空間復(fù)雜度是度量算法所需存儲(chǔ)空間的大小
    2022-01-01
  • SpringBoot如何使用@RequestBody進(jìn)行數(shù)據(jù)校驗(yàn)

    SpringBoot如何使用@RequestBody進(jìn)行數(shù)據(jù)校驗(yàn)

    在Web開發(fā)中,前臺(tái)向后臺(tái)發(fā)送數(shù)據(jù)是非常常見的場(chǎng)景,而在SpringBoot框架中,我們通常使用@RequestBody注解來接收前臺(tái)發(fā)送的?JSON數(shù)據(jù),并將其轉(zhuǎn)化為Java對(duì)象,本文將介紹如何在?SpringBoot?中使用?@RequestBody?進(jìn)行數(shù)據(jù)校驗(yàn)
    2023-06-06
  • java中Hashtable和HashMap的區(qū)別分析

    java中Hashtable和HashMap的區(qū)別分析

    java中Hashtable和HashMap的區(qū)別分析,需要的朋友可以參考一下
    2013-04-04
  • SpringBoot整合RabbitMQ的5種模式實(shí)戰(zhàn)

    SpringBoot整合RabbitMQ的5種模式實(shí)戰(zhàn)

    本文主要介紹了SpringBoot整合RabbitMQ的5種模式實(shí)戰(zhàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-08-08
  • java9開始——接口中可以定義private私有方法

    java9開始——接口中可以定義private私有方法

    這篇文章主要介紹了從java9開始 接口中可以定義private私有方法,幫助大家更好的理解和學(xué)習(xí)Java,感興趣的朋友可以了解下
    2020-10-10
  • Java實(shí)現(xiàn)經(jīng)典大富翁游戲的示例詳解

    Java實(shí)現(xiàn)經(jīng)典大富翁游戲的示例詳解

    大富翁,又名地產(chǎn)大亨。是一種多人策略圖版游戲。參與者分得游戲金錢,憑運(yùn)氣(擲骰子)及交易策略,買地、建樓以賺取租金。本文將通過Java實(shí)現(xiàn)這一經(jīng)典游戲,感興趣的可以跟隨小編一起學(xué)習(xí)一下
    2022-02-02
  • Java 中的 Unsafe 魔法類的作用大全

    Java 中的 Unsafe 魔法類的作用大全

    Unsafe是位于sun.misc包下的一個(gè)類,主要提供一些用于執(zhí)行低級(jí)別、不安全操作的方法,關(guān)于Java 中的 Unsafe 魔法類,到底有啥用處,你都了解嗎,下面通過本文給大家普及一下
    2021-06-06
  • Java Semaphore實(shí)現(xiàn)高并發(fā)場(chǎng)景下的流量控制

    Java Semaphore實(shí)現(xiàn)高并發(fā)場(chǎng)景下的流量控制

    在java開發(fā)的工作中是否會(huì)出現(xiàn)這樣的場(chǎng)景,你需要實(shí)現(xiàn)一些異步運(yùn)行的任務(wù),該任務(wù)可能存在消耗大量?jī)?nèi)存的情況,所以需要對(duì)任務(wù)進(jìn)行并發(fā)控制。本文將介紹通過Semaphore類優(yōu)雅的實(shí)現(xiàn)并發(fā)控制,感興趣的可以了解一下
    2021-12-12
  • MyBatis的模糊查詢mapper.xml的寫法講解

    MyBatis的模糊查詢mapper.xml的寫法講解

    這篇文章主要介紹了MyBatis的模糊查詢mapper.xml的寫法講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • Spring Boot的Profile配置詳解

    Spring Boot的Profile配置詳解

    本篇文章主要介紹了Spring Boot的Profile配置詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-05-05

最新評(píng)論