JAVA之HashMap源码分析

hashmap是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。另外,HashMap是非线程安全的,也就是说在多线程的环境下,可能会存在问题,而Hashtable是线程安全的。

hashmap的数据结构结合了数组和链表的优点(如下图),既能像线性数组那样随机存取,又便于插入删除。
《JAVA之HashMap源码分析》

1.HashMap的属性

/** 2 * 默认的初始容量16. 3 */
 4      static final int DEFAULT_INITIAL_CAPACITY = 16;
 5      /** 6 * 最大容量 7 */
 8      static final int MAXIMUM_CAPACITY = 1 << 30;
 9      /** 10 * 默认装载因子0.75f. 11 */
12      static final float DEFAULT_LOAD_FACTOR = 0.75f;
13      /** 14 * 存储数据的Entry数组 15 */
16      transient Entry[] table;
17      /** 18 * map中目前保存的键值对的数量 19 */
20      transient int size;
21      /** 22 * 需要调整大小的极限值(容量*装载因子)(map在当前capacity能存储的最大键值对数量) 23 */
24      int threshold;
25      /** 26 *装载因子,当HashMap的数据大小>=容量*加载因子时,HashMap会将容量扩容 27 */
28      final float loadFactor;
29      /** 30 * map结构被改变的次数 31 */
32      transient volatile int modCount;
size:该变量保存了该 HashMap 中所包含的 key-value 对的数量。
threshold:该变量包含了 HashMap 能容纳的 key-value 对的极限,它的值等于 HashMap 的容量乘以负载因子(load factor)。

当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。
其中的负载因子loadFactor的理解为:HashMap中的数据量/HashMap的总容量(initialCapacity),当loadFactor达到指定值或者0.75时候,HashMap的总容量自动扩展一倍,以此类推。

hashmap的API
void clear()
从此映射中移除所有映射关系。
Object clone()
返回此 HashMap 实例的浅表副本:并不复制键和值本身。
boolean containsKey(Object key)
如果此映射包含对于指定键的映射关系,则返回 true。
boolean containsValue(Object value)
如果此映射将一个或多个键映射到指定值,则返回 true。
Set

    /** *使用默认的容量及装载因子构造一个空的HashMap */
      public HashMap() {
          this.loadFactor = DEFAULT_LOAD_FACTOR;
          threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
          table = new Entry[DEFAULT_INITIAL_CAPACITY];//根据默认容量(16)初始化table
         init();
      }
   /** * 根据给定的初始容量的装载因子创建一个空的HashMap * 初始容量小于0或装载因子小于等于0将报异常 */
     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);
         int capacity = 1;
         //设置capacity为大于initialCapacity且是2的幂的最小值
         while (capacity < initialCapacity)
             capacity <<= 1;
         this.loadFactor = loadFactor;
         threshold = (int)(capacity * loadFactor);
         table = new Entry[capacity];
         init();
     }
   /** *根据指定容量创建一个空的HashMap */
     public HashMap(int initialCapacity) {
              this(initialCapacity, DEFAULT_LOAD_FACTOR);//调用上面的构造方法,容量为指定的容量,装载因子是默认值
     }
   /** *通过传入的map创建一个HashMap,容量为默认容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的较大者,装载因子为默认值 */
     public HashMap(Map<? extends K, ? extends V> m) {
         this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                       DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
         putAllForCreate(m);
     }

上面代码中粗体字代码包含了一个简洁的代码实现:找出大于 initialCapacity 的、最小的 2 的 n 次方值,并将其作为 HashMap 的实际容量(由 capacity 变量保存)。例如给定 initialCapacity 为 10,那么该 HashMap 的实际容量就是 16。

同时我们可以在创建 HashMap 时根据实际需要适当地调整 load factor 的值;如果程序比较关心空间开销、内存比较紧张,可以适当地增加负载因子;如果程序比较关心时间开销,内存比较宽裕则可以适当的减少负载因子
3.Entry的实现

static class Entry<K,V> implements Map.Entry<K,V> {
         final K key;
         V value;
         Entry<K,V> next;//对下一个节点的引用
         final int hash;

         Entry(int h, K k, V v, Entry<K,V> n) {
             value = v;
             next = n;
             key = k;
             hash = h;
         }

         public final K getKey() {
             return key;
         }

         public final V getValue() {
             return value;
         }

         public final V setValue(V newValue) {
           V oldValue = value;
             value = newValue;
             return oldValue;//返回的是之前的Value
         }

         public final boolean equals(Object o) {
             if (!(o instanceof Map.Entry))//先判断类型是否一致
                 return false;
             Map.Entry e = (Map.Entry)o;
             Object k1 = getKey();
             Object k2 = e.getKey();
        // Key相等且Value相等则两个Entry相等
             if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                 Object v1 = getValue();
                 Object v2 = e.getValue();
                 if (v1 == v2 || (v1 != null && v1.equals(v2)))
                     return true;
             }
             return false;
         }
         // hashCode是Key的hashCode和Value的hashCode的异或的结果
         public final int hashCode() {
             return (key==null   ? 0 : key.hashCode()) ^
                    (value==null ? 0 : value.hashCode());
         }
         // 重写toString方法,是输出更清晰
         public final String toString() {
             return getKey() + "=" + getValue();
         }

         void recordAccess(HashMap<K,V> m) {
         }

         void recordRemoval(HashMap<K,V> m) {
         }
     }

4.put()
这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。也就是说数组中存储的是最后插入的元素。到这里为止,HashMap的大致实现,我们应该已经清楚了。

    public V put(K key, V value)   
    {   
        // 如果 key 为 null,调用 putForNullKey 方法进行处理 
        if (key == null)   
            return putForNullKey(value);   
        // 根据 key 的 keyCode 计算 Hash 值 
        int hash = hash(key.hashCode());   
        // 搜索指定 hash 值在对应 table 中的索引 
        int i = indexFor(hash, table.length);  
        // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素 
        for (Entry<K,V> e = table[i]; e != null; e = e.next)   
        {   
            Object k;   
            // 找到指定 key 与需要放入的 key 相等(hash 值相同 
            // 通过 equals 比较放回 true) 
            if (e.hash == hash && ((k = e.key) == key   
                || key.equals(k)))   
            {   
                V oldValue = e.value;   
                e.value = value;   
                e.recordAccess(this);   
                return oldValue;   
            }   
        }   
        // 如果 i 索引处的 Entry 为 null,表明此处还没有 Entry或者在i位置无法覆盖之前的内容
        modCount++;   
        // 将 key、value 添加到 i 索引处 
        addEntry(hash, key, value, i);   
        return null;   
    }  

如果key值为空,我们来看看putForNullKey的处理过程:

private V putForNullKey(V value) {
         for (Entry<K,V> e = table[0]; e != null; e = e.next) {
             if (e.key == null) {
                 V oldValue = e.value;
                 e.value = value;
                 e.recordAccess(this);
                 return oldValue;
             }
         }
         modCount++;
         addEntry(0, null, value, 0);
         return null;
     }

当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
上面方法提供了一个根据 hashCode() 返回值来计算 Hash 码的方法:hash(),这个方法是一个纯粹的数学计算,其方法如下:

static int hash(int h) 
{ 
    h ^= (h >>> 20) ^ (h >>> 12); 
    return h ^ (h >>> 7) ^ (h >>> 4); 
}

对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 Hash 码值总是相同的。接下来程序会调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。indexFor(int h, int length) 方法的代码如下:

static int indexFor(int h, int length) 
{ 
    return h & (length-1); 
}

当 length 总是 2 的倍数时,h & (length-1)将是一个非常巧妙的设计:假设 h=5,length=16, 那么 h & length – 1 将得到 5;如果 h=6,length=16, 那么 h & length – 1 将得到 6 ……如果 h=15,length=16, 那么 h & length – 1 将得到 15;但是当 h=16 时 , length=16 时,那么 h & length – 1 将得到 0 了;当 h=17 时 , length=16 时,那么 h & length – 1 将得到 1 了……这样保证计算得到的索引值总是位于 table 数组的索引之内。(其实就是一个简单的mod运算)。

但要注意的相同的key的hascode肯定是一样的,但hashcode相同的话key不一定相同。

根据上面 put 方法的源代码可以看出,当程序试图将一个 key-value 对放入 HashMap 中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但 key 不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。

如:

hashmap.put("数学"80);
hashmap.put("英语"90);

如果“数学”和“英语”计算出来的hashcode是一样的,分别调用hash(hashcode)后,得到的index也是相同,意味着他们在那它们的存储位置相同,但是“英语”并不会覆盖掉“数学”,因为((k = e.key) == key || key.equals(k))会判断出他们两者key本就是不相同的,导致“数学”这个entry会存储在tabel[index]这个位置上,然后再指向“数学”entry。

5.addEntry()

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); //**指向e**
    // 如果 Map 中的 key-value 对的数量超过了极限
    if (size++ >= threshold) 
        // 把 table 对象的长度扩充到 2 倍。
        resize(2 * table.length);    // ②
}

系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序①号代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。

以上过程就是新建一个Entry对象,并放在当前位置的Entry链表的头部。然后判断size是否达到了需要扩容的界限并让size增加1,如果达到了扩容的界限则调用resize(int capacity)方法。

注意threshold和capacity的区别,threshold指map里存储的entry的个数(HashMap中的数据量),而initialCapacity指的是tabel数组的长度。

void resize(int newCapacity) {
         Entry[] oldTable = table;
         int oldCapacity = oldTable.length;
         // 这个if块表明,如果容量已经到达允许的最大值,即MAXIMUN_CAPACITY,则不再拓展容量,而将装载拓展的界限值设为计算机允许的最大值。
         // 不会再触发resize方法,而是不断的向map中添加内容,即table数组中的链表可以不断变长,但数组长度不再改变
         if (oldCapacity == MAXIMUM_CAPACITY) {
             threshold = Integer.MAX_VALUE;
             return;
         }
         // 创建新数组,容量为指定的容量
         Entry[] newTable = new Entry[newCapacity];
         transfer(newTable);
         table = newTable;
         // 设置下一次需要调整数组大小的界限
         threshold = (int)(newCapacity * loadFactor);
     }

这里需要重点看看transfer方法:

void transfer(Entry[] newTable) {
         // 保留原数组的引用到src中,
         Entry[] src = table;
         // 新容量使新数组的长度
         int newCapacity = newTable.length;
      // 遍历原数组
         for (int j = 0; j < src.length; j++) {
             // 获取元素e
             Entry<K,V> e = src[j];
             if (e != null) {
                 // 将原数组中的元素置为null
                 src[j] = null;
                 // 遍历原数组中j位置指向的链表
                 do {
                     Entry<K,V> next = e.next;
                     // 根据新的容量计算e在新数组中的位置
                     int i = indexFor(e.hash, newCapacity);
                     // 将e插入到newTable[i]指向的链表的头部
                     e.next = newTable[i];
                     newTable[i] = e;
                     e = next;
                 } while (e != null);
             }
         }
     }

tranfer方法将所有的元素重新哈希,因为新的容量变大,所以每个元素的哈希值和位置都是不一样的。

6.get()

public V get(Object key) 
 { 
     // 如果 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; 
         // 搜索该 Entry 链的下一个 Entr 
         e = e.next)         // ①
     { 
         Object k; 
         // 如果该 Entry 的 key 与被搜索 key 相同
         if (e.hash == hash && ((k = e.key) == key 
             || key.equals(k))) 
             return e.value; 
     } 
     return null; 
 }

从上面代码中可以看出,如果 HashMap 的每个 bucket 里只有一个 Entry 时,HashMap 可以根据索引、快速地取出该 bucket 里的 Entry;在发生“Hash 冲突”的情况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。

HashMap运用举例

package com.sort;  

import java.util.HashMap;  
import java.util.Iterator;  
import java.util.Map;  
import java.util.Scanner;  
import java.util.Set;  

/** * 统计一句英语的简单统计各个单词出现的次数 * * @author Owner * */  
public class MapTest3 {  

    public static void main(String[] args) {  

        Scanner sc = new Scanner(System.in);  

        System.out.println("请输入一句英语,单词间用空格隔开:");  

        String sentence = sc.nextLine();  

        String[] arr = sentence.split(" ");  

        // 键代表着单词,值代表着次数 
        Map<String, Integer> map = new HashMap<String, Integer>();  
        for (int i = 0; i < arr.length; i++) {  
            if (!map.containsKey(arr[i])) {  
                map.put(arr[i], 1);  
            } else {  
                // 说明map中,存在该元素 
                int num = map.get(arr[i]);  
                map.put(arr[i], ++num);  
            }  
        }  

        System.out.println("统计单词出现的个数,结果如下:");  

        Set<String> set = map.keySet();  

        for (Iterator<String> iterator = set.iterator(); iterator.hasNext();) {  
            String key = iterator.next();  

            Integer value = map.get(key);  

            System.out.println(key + "=" + value);  
        }  
    }  
} 

注:以上内容是我在学习HashMap相关内容时,瞻仰了几位大牛的笔记,进行了整理供以后学习,并非原创,请见谅。

    原文作者:一生所爱丶
    原文地址: https://blog.csdn.net/EmilSinclair4391/article/details/50959617
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞