深入Java集合学习系列:HashMap的实现原理

来源:互联网 发布:阿里云ecs配置php环境 编辑:程序博客网 时间:2024/05/26 15:54

1.    HashMap概述:

   HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

  

2.    HashMap的数据结构:

   在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。


   从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

源码如下:


<span style="font-family:KaiTi_GB2312;font-size:14px;">static final Entry<?,?>[] EMPTY_TABLE = {};    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  /*** The number of key-value mappings contained in this map.*/ transient int size; int threshold;  // 临界值 它等于HashMap的容量乘以负载因子 final float loadFactor;// 负载因子 public V put(K key, V value) {    // 如果table为空,则使其不为空         if (table == EMPTY_TABLE) {             inflateTable(threshold);         }        // 如果key为null,调用putForNullKey处理         if (key == null)             return putForNullKey(value);         int hash = hash(key);      // 搜索指定hash值对应的索引         int i = indexFor(hash, table.length);         for (Entry<K,V> e = table[i]; e != null; e = e.next) {             Object k;   // 如果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         modCount++;         addEntry(hash, key, value, i);         return null; } // 添加entry void addEntry(int hash, K key, V value, int bucketIndex) {         if ((size >= threshold) && (null != table[bucketIndex])) {             resize(2 * table.length);//原来长度的2倍             hash = (null != key) ? hash(key) : 0;             bucketIndex = indexFor(hash, table.length);         }             createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) {         Entry<K,V> e = table[bucketIndex];   // 头插法建立链         table[bucketIndex] = new Entry<>(hash, key, value, e);         size++;  } </span>
</pre><pre name="code" class="java">
<p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; line-height: 25.2px;"><span style="font-family:KaiTi_GB2312;font-size:14px;">  当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。</span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; line-height: 25.2px;"><span style="font-family:KaiTi_GB2312;font-size:14px;">   那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。</span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; line-height: 25.2px;"><span style="font-family:KaiTi_GB2312;font-size:14px;"></span></p><span style="font-family:KaiTi_GB2312;font-size:14px;">void resize(int newCapacity) {         Entry[] oldTable = table;//先记录下来table         int oldCapacity = oldTable.length;         if (oldCapacity == MAXIMUM_CAPACITY) {             threshold = Integer.MAX_VALUE;  //static final int MAXIMUM_CAPACITY = 1 << 30;             return;         }             Entry[] newTable = new Entry[newCapacity];//这个是原来长度的2倍         transfer(newTable, initHashSeedAsNeeded(newCapacity));         table = newTable;         threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);     }     /**     * Transfers all entries from current table to newTable.     */     void transfer(Entry[] newTable, boolean rehash) {// rehash 是否重新hash         int newCapacity = newTable.length;         for (Entry<K,V> e : table) {             while(null != e) {                 Entry<K,V> next = e.next;                 if (rehash) {                     e.hash = null == e.key ? 0 : hash(e.key);                 }                 int i = indexFor(e.hash, newCapacity);                 e.next = newTable[i];                 newTable[i] = e;                 e = next;             }         }     }  /**     * Initialize the hashing mask value. We defer(延迟) initialization until we     * really need it.     */     final boolean initHashSeedAsNeeded(int capacity) {         boolean currentAltHashing = hashSeed != 0;         boolean useAltHashing = sun.misc.VM.isBooted() &&                 (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);         boolean switching = currentAltHashing ^ useAltHashing;         if (switching) {             hashSeed = useAltHashing                 ? sun.misc.Hashing.randomHashSeed(this)                 : 0;         }         return switching;     } // 内部类 entry  static class Entry<K,V> implements Map.Entry<K,V> {         final K key;         V value;         Entry<K,V> next;// 指向下一个entry         int hash;             /**         * Creates new entry.         */         Entry(int h, K k, V v, Entry<K,V> n) {             value = v;             next = n;             key = k;             hash = h;         }</span>


   可以看出,Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。


  3) 归纳起来简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

 

.    HashMap的resize(rehash):

   当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

   那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

 

.    HashMap的性能参数:

   HashMap 包含如下几个构造器:

   HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。

   HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。

   HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。

   HashMap的基础构造器HashMap(int initialCapacity, float loadFactor)带有两个参数,它们是初始容量initialCapacity和加载因子loadFactor。

   initialCapacity:HashMap的最大容量,即为底层数组的长度。

   loadFactor:负载因子loadFactor定义为:散列表的实际元素数目(n)/ 散列表的容量(m)。

   负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。

   HashMap的实现中,通过threshold字段来判断HashMap的最大容量:


  1. threshold = (int)(capacity * loadFactor);  

   结合负载因子的定义公式可知,threshold就是在此loadFactor和capacity对应下允许的最大元素数目,超过这个数目就重新resize,以降低实际的负载因子。默认的的负载因子0.75是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize后的HashMap容量是容量的两倍:

 


  1. if (size++ >= threshold)     
  2.     resize(2 * table.length);    

 

.    Fail-Fast机制:

   我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。

   这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。


  1. HashIterator() {  
  2.     expectedModCount = modCount;  
  3.     if (size > 0) { // advance to first entry  
  4.     Entry[] t = table;  
  5.     while (index < t.length && (next = t[index++]) == null)  
  6.         ;  
  7.     }  
  8. }  

 

   在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map:

   注意到modCount声明为volatile,保证线程之间修改的可见性。


  1. final Entry<K,V> nextEntry() {     
  2.     if (modCount != expectedModCount)     
  3.         throw new ConcurrentModificationException();  

 

   在HashMap的API中指出:

   由所有HashMap类的“collection 视图方法”所返回的迭代器都是快速失败的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。

   注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。



自定义Map ,便于更好的l理解Map底层数据结构原理:

<span style="font-family:KaiTi_GB2312;font-size:14px;">public class CustomHashMap<K,V>{@SuppressWarnings("unchecked")private LinkedList<Entry<K,V>>[]  arr  = new LinkedList[9]; //Map的底层结构就是:数组+链表!private int size;public void put(K key,V value){Entry<K, V> entry=new Entry<K, V>(key, value);//Hash keyint hashcode=key.hashCode();hashcode=hashcode<0?-hashcode:hashcode;int index=hashcode%arr.length;LinkedList<Entry<K,V>> object=arr[index];if(object==null){LinkedList<Entry<K,V>> list=new LinkedList<>();list.add(entry);arr[index]=list;}else{for (int i = 0; i < object.size(); i++) {Entry<K,V> e=object.get(i);if(e.getKey().equals(key)){e.setValue(value);return;  //键值重复直接覆盖!}}object.add(entry);}size++;}public V get(K key){int index=key.hashCode()%arr.length;LinkedList<Entry<K,V>> object=arr[index];if(object!=null){for (int i = 0; i < object.size(); i++) {Entry<K,V> e=object.get(i);if(e.getKey().equals(key)){return e.getValue();}}}return null;}public Entry<K,V> remove(K key){if (size == 0) {       return null;    } int index=key.hashCode()%arr.length;LinkedList<Entry<K,V>> array=arr[index];Entry<K,V> entry=null;if(array!=null){if(array.size()==1){ //说明只有一个值Entry<K,V> e=array.get(0);if(e.getKey().equals(key)){entry=e;array=null;arr[index]=null;size--;}}else{for (Iterator<Entry<K, V>> iterator = array.iterator(); iterator.hasNext();) {Entry<K, V> e = (Entry<K, V>) iterator.next();if(e.getKey().equals(key)){entry=e;iterator.remove();break;}}}}return entry;}public boolean containsKey(K key){int index=key.hashCode()%arr.length;LinkedList<Entry<K,V>> array=arr[index];for(int i=0;i<array.size();i++){Entry<K,V> e=array.get(i);if(e.getKey().equals(key)){return true;}}return false;}public boolean containsValue(V value){for (int i = 0; i < size; i++) {LinkedList<Entry<K,V>> array=arr[i];if(array!=null){for (int j = 0; j < array.size(); j++) {Entry<K,V> entry=array.get(j);if(entry!=null){if (value.equals(entry.value))                    return true; }}}}return false;}class Entry<K,V>{private K key;private V value;public K getKey() {return key;}public void setKey(K key) {this.key = key;}public V getValue() {return value;}public void setValue(V value) {this.value = value;}public Entry(K key, V value) {super();this.key = key;this.value = value;}@Overridepublic int hashCode() {  return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());}@Overridepublic boolean equals(Object obj) {if (this == obj)return true;if (obj == null)return false;if (getClass() != obj.getClass())return false;Entry other = (Entry) obj;if (key == null) {if (other.key != null)return false;} else if (!key.equals(other.key))return false;if (value == null) {if (other.value != null)return false;} else if (!value.equals(other.value))return false;return true;}}public static void main(String[] args) {CustomHashMap<String,String> m = new CustomHashMap<String,String>();m.put("测试", "谢霆锋");m.put("测试", "刘德华");m.put("测试2", "周润发");System.out.println(m.size);String w = m.get("测试");m.remove("测试");    w = m.get("测试");System.out.println(w); }}</span>


0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 工作混时间很懒怎么办 老婆欲望太强了怎么办? 孕晚期血糖高怎么办啊 被过山风咬了怎么办 大腿上长肥胖纹怎么办 减肥减的胸小了怎么办 手机刷机变砖了怎么办 手部肌肉萎缩了怎么办 龙吐珠花冻着了怎么办 发财树浇水多了怎么办 发财树根部烂了怎么办 发财树的根烂了怎么办 淘米水发酵臭了怎么办 眼睛下的小细纹怎么办 内眼角开的太小怎么办 做完美瞳眼睛红怎么办 纹完眼线眼睛红怎么办 21岁眼下小细纹怎么办 20岁眼部有细纹怎么办 纹身几天后晕色怎么办 屁眼长了痔疮大怎么办 苹果os系统坏了怎么办 苹果6出现白苹果怎么办 苹果平板白屏了怎么办 腿上的皮肤很干怎么办 鱼身上掉了鳞片怎么办 患上恋爱恐慌症怎么办 我觉得活着好累怎么办 我的世界遇到him怎么办 孕妇闻了樟脑球怎么办 电脑不能识别u盘怎么办 电脑识别不了u盘怎么办 神之子武器爆了怎么办 中控本考试没过怎么办 震后安全逃生后怎么办 逃出电梯游戏2关怎么办 车尾灯灯罩破了怎么办 着火了吸入了烟怎么办 多媒课件着火了怎么办 用手楚了眼睛疼怎么办 眼睛又痒又干涩怎么办