JAVA之HashMap源码分析

来源:互联网 发布:java中基本数据类型 编辑:程序博客网 时间:2024/05/17 07:28

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

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相关内容时,瞻仰了几位大牛的笔记,进行了整理供以后学习,并非原创,请见谅。

1 0
原创粉丝点击