Chinaunix首页 | 论坛 | 博客

分类: Java

2011-10-05 18:50:28

HashMap源码分析

 

作者:十年砍柴   撰写日期:2011-9-26

 

HashMapjava中使用非常频繁的容器类,也是java笔试面试中经常会问到的。深入理解HashMap有助于我们更好的使用它。

 

1. HashMap内部结构

       了解HashMap的数据结构,有助于理解HashMap的各种操作。HashMap内部结构如下图:

由上图可知,整个HashMap内部构造是一个数组,数组元素是一个链表。熟悉哈希的应该知道,数组元素的存储位置使用哈希方法得到,而链表的作用就是解决哈希存在的冲突。HashMap中数组长度为2的整数幂,默认初始长度为16 ,装载因子为0.75


 

点击(此处)折叠或打开

  1. static final int DEFAULT_INITIAL_CAPACITY = 16;
  2. static final float DEFAULT_LOAD_FACTOR = 0.75f;

初始化一个HashMap类,可以指定一个容量参数initialCapacity,构造函数会根据initialCapacity进行初始化,找到一个大于等于initialCapacity且为2的整数幂的值作为哈希数组的长度:


 

点击(此处)折叠或打开

  1. int capacity = 1;
  2.      while (capacity < initialCapacity)
  3.             capacity <<= 1;
  4.      this.loadFactor = loadFactor;
  5.      threshold = (int)(capacity * loadFactor);
  6.      table = new Entry[capacity];

   HashMap 中存储的元素为Entry类,此类是HashMap中定义的内部类:


 

点击(此处)折叠或打开

  1. static class Entry<K,V> implements Map.Entry<K,V> {
  2.         final K key;
  3.         V value;
  4.         Entry<K,V> next;
  5.         final int hash;
  6. }

    其中key为存入的key值,value为存入的value值,next是指向下一个Entry元素的指针,hashkeyhashcode经过hash后的值。

 

2. HashMap中的各种操作

      插入操作put


 

点击(此处)折叠或打开

  1. public V put(K key, V value) {
  2.        if (key = = null)
  3.             return putForNullKey(value);
  4.         int hash = hash(key.hashCode()); //对key的hashcode值重新hash

  5.         int i = indexFor(hash, table.length); //找到key在table中的放入位置

  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  7.         //检查内部是否已存在key,若存在,则更新其value

  8.             Object k;
  9.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  10.                 V oldValue = e.value;
  11.                 e.value = value;
  12.                 e.recordAccess(this);
  13.                 return oldValue;
  14.             }
  15.         }
  16.         modCount++;
  17.         addEntry(hash, key, value, i); //将新的Entry放入table中

  18.         return null;
  19.     }

插入算法对keynull的情况进行了特别处理,HashMap会将keynull的值插入到table数组的一个位置所在的链表中。HashMap采用如下算法进行hash


 

点击(此处)折叠或打开

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

addEntry算法将Entry插入链表的头部:


 

点击(此处)折叠或打开

  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2.     Entry<K,V> e = table[bucketIndex];
  3.         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
  4.         if (size++ >= threshold)
  5.             resize(2 * table.length);
  6.     }

插入元素后,检查数组中存放的元素是否超过门限值(数组长度*装载因子),若超过,扩展数组的长度为原来的2倍,并对原数组的元素重新进行哈希,并放入新数组中的新的位置。


 

点击(此处)折叠或打开

  1. void resize(int newCapacity) {
  2.         Entry[] oldTable = table;
  3.         int oldCapacity = oldTable.length;
  4.         if (oldCapacity == MAXIMUM_CAPACITY) {
  5.             threshold = Integer.MAX_VALUE;
  6.             return;
  7.         }
  8.         Entry[] newTable = new Entry[newCapacity];
  9.         transfer(newTable);
  10.         table = newTable;
  11.         threshold = (int)(newCapacity * loadFactor);
  12.     }
  13. /* Transfers all entries from current table to newTable */
  14.     void transfer(Entry[] newTable) {
  15.         Entry[] src = table;
  16.         int newCapacity = newTable.length;
  17.         for (int j = 0; j < src.length; j++) {
  18.             Entry<K,V> e = src[j];
  19.             if (e != null) {
  20.                 src[j] = null;
  21.                 do {
  22.                     Entry<K,V> next = e.next;
  23.                     int i = indexFor(e.hash, newCapacity);
  24.                     e.next = newTable[i];
  25.                     newTable[i] = e;
  26.                     e = next;
  27.                 } while (e != null);
  28.             }
  29.         }
  30.     }

    数组空间重新分配及数组元素重新哈希是非常耗时的,因此,在使用HashMap的过程中,如果可以预知存放元素个数的大致范围,可以在初始化一个HashMap时指定数组长度,从而可以有效避免重新哈希的情况。下面看HashMap是如何快速取值的:


 

点击(此处)折叠或打开

  1. public V get(Object key) {
  2.         if (key == null)
  3.             return getForNullKey(); //从数组第一个元素所在的链表取

  4.         int hash = hash(key.hashCode());
  5.         for (Entry<K,V> e = table[indexFor(hash, table.length)];
  6.              e != null;
  7.              e = e.next) {
  8.             Object k;
  9.             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
  10.                 return e.value;
  11.         }
  12.         return null;
  13. }

key等于null的情况比较简单,不再说明,对于key不为null的情况,首先通过hash方法,找到带查找的key所在数组中的位置,然后遍历相应的链表。

 

遍历操作:

由于hashMap中元素的存储位置与插入顺序无关,在遍历时,hashMap不保证遍历结果与插入顺序一致。

HashMap通常使用keySetentrySet进行遍历,keySet遍历方式将元素集的key值转换成一个Set,代码如下:


 

点击(此处)折叠或打开

  1. Map map = new HashMap();
  2.   Iterator iter = map.keySet().iterator();
  3.   while (iter.hasNext()) {
  4.    Object key = iter.next();
  5.    Object value = map.get(key);
  6.   }

entrySethashMap中存储的元素Entry转换成entrySet,其遍历方式如下:


 

点击(此处)折叠或打开

  1. for(Map.Entry<Integer, String>m:map.entrySet()){
  2.          Object key=m.getKey();
  3.           Object value=m.getValue();
  4.  }

这两种遍历方式在效率上有所差别:ketSet遍历了两次,第一次将元素集转换成keySet,第二次通过map.get(key)有遍历了一次;entrySet遍历方式只遍历了一次,因此在效率上较前者有所提高。

阅读(761) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~