Java HashMap的工做原理

面試的時候常常會碰見諸如:「java中的HashMap是怎麼工做的」,「HashMap的get和put內部的工做原理」這樣的問題。本文將用一個簡單的例子來解釋下HashMap內部的工做原理。首先咱們從一個例子開始,而不單單是從理論上,這樣,有助於更好地理解,而後,咱們來看下get和put究竟是怎樣工做的。java

咱們來看個很是簡單的例子。有一個」國家」(Country)類,咱們將要用Country對象做爲key,它的首都的名字(String類型)做爲value。下面的例子有助於咱們理解key-value對在HashMap中是如何存儲的。面試

1. Country.javaapi

package org.arpit.javapostsforlearning;
public class Country {
 
 String name;
 long population;
 
 public Country(String name, long population) {
  super();
  this.name = name;
  this.population = population;
 }
 public String getName() {
  return name;
 }
 public void setName(String name) {
  this.name = name;
 }
 public long getPopulation() {
  return population;
 }
 public void setPopulation(long population) {
  this.population = population;
 }
 
 // If length of name in country object is even then return 31(any random number) and if odd then return 95(any random number).
 // This is not a good practice to generate hashcode as below method but I am doing so to give better and easy understanding of hashmap.
 @Override
 public int hashCode() {
  if(this.name.length()%2==0)
   return 31;
  else
   return 95;
 }
 @Override
 public boolean equals(Object obj) {
 
  Country other = (Country) obj;
   if (name.equalsIgnoreCase((other.name)))
   return true;
  return false;
 }
 
}

2. HashMapStructure.java(main class)數組

import java.util.HashMap;
import java.util.Iterator;
   
public class HashMapStructure {
   
    /**
     * @author Arpit Mandliya
     */
    public static void main(String[] args) {
           
        Country india=new Country("India",1000);
        Country japan=new Country("Japan",10000);
           
        Country france=new Country("France",2000);
        Country russia=new Country("Russia",20000);
           
        HashMap<country,string> countryCapitalMap=new HashMap<country,string>();
        countryCapitalMap.put(india,"Delhi");
        countryCapitalMap.put(japan,"Tokyo");
        countryCapitalMap.put(france,"Paris");
        countryCapitalMap.put(russia,"Moscow");
           
        Iterator<country> countryCapitalIter=countryCapitalMap.keySet().iterator();//put debug point at this line
        while(countryCapitalIter.hasNext())
        {
            Country countryObj=countryCapitalIter.next();
            String capital=countryCapitalMap.get(countryObj);
            System.out.println(countryObj.getName()+"----"+capital);
            }
        }
   
   
}

如今,在第23行設置一個斷點,在項目上右擊->調試運行(debug as)->java應用(java application)。程序會停在23行,而後在countryCapitalMap上右擊,選擇「查看」(watch)。將會看到以下的結構:微信

從上圖能夠觀察到如下幾點:app

1. 有一個叫作table大小是16的Entry數組。dom

2. 這個table數組存儲了Entry類的對象。HashMap類有一個叫作Entry的內部類。這個Entry類包含了key-value做爲實例變量。咱們來看下Entry類的結構。Entry類的結構:ide

static class Entry implements Map.Entry
{
        final K key;
        V value;
        Entry next;
        final int hash;
        ...//More code goes here
}   `

3. 每當往hashmap裏面存放key-value對的時候,都會爲它們實例化一個Entry對象,這個Entry對象就會存儲在前面提到的Entry數組table中。如今你必定很想知道,上面建立的Entry對象將會存放在具體哪一個位置(在table中的精確位置)。答案就是,根據key的hashcode()方法計算出來的hash值(來決定)。hash值用來計算key在Entry數組的索引。函數

4. 如今,若是你看下上圖中數組的索引10,它有一個叫作HashMap$Entry的Entry對象。post

5. 咱們往hashmap放了4個key-value對,可是看上去好像只有2個元素!!!這是由於,若是兩個元素有相同的hashcode,它們會被放在同一個索引上。問題出現了,該怎麼放呢?原來它是以鏈表(LinkedList)的形式來存儲的(邏輯上)。

上面的country對象的key-value的hash值是如何計算出來的。

<code>Japan的Hash值是95,它的長度是奇數。

India的Hash值是95,它的長度是奇數。

Russia的Hash值是31,它的長度是偶數。

France,它的長度是偶數。
</code>

下圖會清晰的從概念上解釋下鏈表。

因此,如今假如你已經很好地瞭解了hashmap的結構,讓咱們看下put和get方法。

Put :

讓咱們看下put方法的實現:

/**
  * Associates the specified value with the specified key in this map. If the
  * map previously contained a mapping for the key, the old value is
  * replaced.
  *
  * @param key
  *            key with which the specified value is to be associated
  * @param value
  *            value to be associated with the specified key
  * @return the previous value associated with <tt>key</tt>, or <tt>null</tt>
  *         if there was no mapping for <tt>key</tt>. (A <tt>null</tt> return
  *         can also indicate that the map previously associated
  *         <tt>null</tt> with <tt>key</tt>.)
  */
 public V put(K key, V value) {
  if (key == null)
   return putForNullKey(value);
  int hash = hash(key.hashCode());
  int i = indexFor(hash, table.length);
  for (Entry<k , V> e = table[i]; e != null; e = e.next) {
   Object k;
   if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    V oldValue = e.value;
    e.value = value;
    e.recordAccess(this);
    return oldValue;
   }
  }
 
  modCount++;
  addEntry(hash, key, value, i);
  return null;
 }

如今咱們一步一步來看下上面的代碼。

  1. 對key作null檢查。若是key是null,會被存儲到table[0],由於null的hash值老是0。
  2. key的hashcode()方法會被調用,而後計算hash值。hash值用來找到存儲Entry對象的數組的索引。有時候hash函數可能寫的很很差,因此JDK的設計者添加了另外一個叫作hash()的方法,它接收剛纔計算的hash值做爲參數。
  3. indexFor(hash,table.length)用來計算在table數組中存儲Entry對象的精確的索引。
  4. 在咱們的例子中已經看到,若是兩個key有相同的hash值(也叫衝突),他們會以鏈表的形式來存儲。因此,這裏咱們就迭代鏈表。
  • 若是在剛纔計算出來的索引位置沒有元素,直接把Entry對象放在那個索引上。
  • 若是索引上有元素,而後會進行迭代,一直到Entry->next是null。當前的Entry對象變成鏈表的下一個節點。
  • 若是咱們再次放入一樣的key會怎樣呢?邏輯上,它應該替換老的value。事實上,它確實是這麼作的。在迭代的過程當中,會調用equals()方法來檢查key的相等性(key.equals(k)),若是這個方法返回true,它就會用當前Entry的value來替換以前的value。

Get:

如今咱們來看下get方法的實現:

/**
  * Returns the value to which the specified key is mapped, or {@code null}
  * if this map contains no mapping for the key.
  *
  * <p>
  * More formally, if this map contains a mapping from a key {@code k} to a
  * value {@code v} such that {@code (key==null ? k==null :
  * key.equals(k))}, then this method returns {@code v}; otherwise it returns
  * {@code null}. (There can be at most one such mapping.)
  *
  * </p><p>
  * A return value of {@code null} does not <i>necessarily</i> indicate that
  * the map contains no mapping for the key; it's also possible that the map
  * explicitly maps the key to {@code null}. The {@link #containsKey
  * containsKey} operation may be used to distinguish these two cases.
  *
  * @see #put(Object, Object)
  */
 public V get(Object key) {
  if (key == null)
   return getForNullKey();
  int hash = hash(key.hashCode());
  for (Entry<k , V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
   Object k;
   if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
    return e.value;
  }
  return null;
 }

當你理解了hashmap的put的工做原理,理解get的工做原理就很是簡單了。當你傳遞一個key從hashmap總獲取value的時候:

  1. 對key進行null檢查。若是key是null,table[0]這個位置的元素將被返回。
  2. key的hashcode()方法被調用,而後計算hash值。
  3. indexFor(hash,table.length)用來計算要獲取的Entry對象在table數組中的精確的位置,使用剛纔計算的hash值。
  4. 在獲取了table數組的索引以後,會迭代鏈表,調用equals()方法檢查key的相等性,若是equals()方法返回true,get方法返回Entry對象的value,不然,返回null。

要牢記如下關鍵點:

  • HashMap有一個叫作Entry的內部類,它用來存儲key-value對。
  • 上面的Entry對象是存儲在一個叫作table的Entry數組中。
  • table的索引在邏輯上叫作「桶」(bucket),它存儲了鏈表的第一個元素。
  • key的hashcode()方法用來找到Entry對象所在的桶。
  • 若是兩個key有相同的hash值,他們會被放在table數組的同一個桶裏面。
  • key的equals()方法用來確保key的惟一性。
  • value對象的equals()和hashcode()方法根本一點用也沒有。
  • Java學習交流QQ羣:589809992 咱們一塊兒學Java!

我有一個微信公衆號,常常會分享一些Java技術相關的乾貨。若是你喜歡個人分享,能夠用微信搜索「Java團長」或者「javatuanzhang」關注。

相關文章
相關標籤/搜索