从新详尽的理解HasMap

关于hashCode

hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中肯定对象的存储地址的.java

1.hashcode是用来查找的,若是你学过数据结构就应该知道,在查找和排序这一章有 例如内存中有这样的位置 0 1 2 3 4 5 6 7 而我有个类,这个类有个字段叫ID,我要把这个类存放在以上8个位置之一,若是不用hashcode而任意存放,那么当查找时就须要到这八个位置里挨个去找,或者用二分法一类的算法。 但若是用hashcode那就会使效率提升不少。 咱们这个类中有个字段叫ID,那么咱们就定义咱们的hashcode为ID%8,而后把咱们的类存放在取得得余数那个位置。好比咱们的ID为9,9除8的余数为1,那么咱们就把该类存在1这个位置,若是ID是13,求得的余数是5,那么咱们就把该类放在5这个位置。这样,之后在查找该类时就能够经过ID除 8求余数直接找到存放的位置了。 2.可是若是两个类有相同的hashcode怎么办那(咱们假设上面的类的ID不是惟一的),例如9除以8和17除以8的余数都是1,那么这是否是合法的,回答是:能够这样。那么如何判断呢?在这个时候就须要定义 equals了。 也就是说,咱们先经过 hashcode来判断两个类是否存放某个桶里,但这个桶里可能有不少类,那么咱们就须要再经过 equals 来在这个桶里找到咱们要的类。 那么。重写了equals(),为何还要重写hashCode()呢? 想一想,你要在一个桶里找东西,你必须先要找到这个桶啊,你不经过重写hashcode()来找到桶,光重写equals()有什么用啊面试

理解了hashCode咱们来理解HashMap

HashMap概述

HashMap是基于哈希表的Map接口的非同步实现。此实现提供全部可选的映射操做,并容许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 在java编程语言中,最基本的结构就是两种,一个是数组,另一个是模拟指针(引用),全部的数据结构均可以用这两个基本结构来构造的,HashMap也不例外。HashMap其实是一个“链表散列”的数据结构,即数组和链表的结合体。算法

内部存储

HashMap的内部存储是一个数组(bucket),数组的元素Node实现了是Map.Entry接口(hash, key, value, next),next非空时指向定位相同的另外一个Entry,如图:编程

关于hashCode

hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中肯定对象的存储地址的.数组

1.hashcode是用来查找的,若是你学过数据结构就应该知道,在查找和排序这一章有 例如内存中有这样的位置 0 1 2 3 4 5 6 7 而我有个类,这个类有个字段叫ID,我要把这个类存放在以上8个位置之一,若是不用hashcode而任意存放,那么当查找时就须要到这八个位置里挨个去找,或者用二分法一类的算法。 但若是用hashcode那就会使效率提升不少。 咱们这个类中有个字段叫ID,那么咱们就定义咱们的hashcode为ID%8,而后把咱们的类存放在取得得余数那个位置。好比咱们的ID为9,9除8的余数为1,那么咱们就把该类存在1这个位置,若是ID是13,求得的余数是5,那么咱们就把该类放在5这个位置。这样,之后在查找该类时就能够经过ID除 8求余数直接找到存放的位置了。 2.可是若是两个类有相同的hashcode怎么办那(咱们假设上面的类的ID不是惟一的),例如9除以8和17除以8的余数都是1,那么这是否是合法的,回答是:能够这样。那么如何判断呢?在这个时候就须要定义 equals了。 也就是说,咱们先经过 hashcode来判断两个类是否存放某个桶里,但这个桶里可能有不少类,那么咱们就须要再经过 equals 来在这个桶里找到咱们要的类。 那么。重写了equals(),为何还要重写hashCode()呢? 想一想,你要在一个桶里找东西,你必须先要找到这个桶啊,你不经过重写hashcode()来找到桶,光重写equals()有什么用啊安全

理解了hashCode咱们来理解HashMap

HashMap概述

HashMap是基于哈希表的Map接口的非同步实现。此实现提供全部可选的映射操做,并容许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 在java编程语言中,最基本的结构就是两种,一个是数组,另一个是模拟指针(引用),全部的数据结构均可以用这两个基本结构来构造的,HashMap也不例外。HashMap其实是一个“链表散列”的数据结构,即数组和链表的结合体。数据结构

内部存储

HashMap的内部存储是一个数组(bucket),数组的元素Node实现了是Map.Entry接口(hash, key, value, next),next非空时指向定位相同的另外一个Entry,如图:并发

HashMap实现存储和读取

存储
public V put(K key, V value) {
      // HashMap容许存放null键和null值。
     // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
     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;
         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++;
     // 将key、value添加到i索引处。
     addEntry(hash, key, value, i);
     return null;
 }
复制代码

根据hash值获得这个元素在数组中的位置(即下标),若是数组该位置上已经存放有其余元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最早加入的放在链尾。若是数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。 hash(int h)方法根据key的hashCode从新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,形成的hash冲突。编程语言

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

HashMap中要找到某个元素,须要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,因此咱们固然但愿这个HashMap里面的元素位置尽可能的分布均匀些,尽可能使得每一个位置上的元素数量只有一个,那么当咱们用hash算法求得这个位置的时候,立刻就能够知道对应位置的元素就是咱们要的,而不用再去遍历链表,这样就大大优化了查询的效率。高并发

根据上面 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() 方法的说明。

具体如何根据hash计算下标呢,参见

JDK 源码中 HashMap 的 hash 方法原理是什么?

获取
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中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,而后经过key的equals方法在对应位置的链表中找到须要的元素。

HashMap的resize

当hashmap中的元素愈来愈多的时候,碰撞的概率也就愈来愈高(由于数组的长度是固定的),因此为了提升查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操做也会出如今ArrayList中,因此这是一个通用的操做,不少人对它的性能表示过怀疑,不过想一想咱们的“均摊”原理,就释然了,而在hashmap数组扩容以后,最消耗性能的点就出现了:原数组中的数据必须从新计算其在新数组中的位置,并放进去,这就是resize。 那么hashmap何时进行扩容呢?当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认状况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍,而后从新计算每一个元素在数组中的位置,而这是一个很是消耗性能的操做,因此若是咱们已经预知hashmap中元素的个数,那么预设元素的个数可以有效的提升hashmap的性能。好比说,咱们有1000个元素new HashMap(1000), 可是理论上来说new HashMap(1024)更合适,不过上面annegu已经说过,即便是1000,hashmap也自动会将其设置为1024。 可是new HashMap(1024)还不是更合适的,由于0.751000 < 1000, 也就是说为了让0.75 * size > 1000, 咱们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

1.8的优化

Java8作的改变: 1.HashMap是数组+链表+红黑树(JDK1.8增长了红黑树部分),当链表长度>=8时转化为红黑树 在JDK1.8版本中,对数据结构作了进一步的优化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增刪改查的特色提升HashMap的性能,其中会用到红黑树的插入、刪除、查找等算法。 2. java8 中对hashmap护容不是从新计算全部元素在数组的位置,而是咱们使用的是2次幂的扩展(指长度扩为原来2倍),因此,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置在扩充HashMap的时候,不须要像JDK1.7的实现那样从新计算hash,只须要看看原来的hash值新增的那个bit是1仍是0就行了,是0的话索引没变,是1的话索引变成“原索引+oldCap"。

面试中一般被问到的。

HashMap高并发状况下会出现什么问题?

扩容问题 HashMap的存放自定义类时,须要实现自定义类的什么方法?

hashCode和equals.经过hash(hashCode)而后模运算(实际上是与的位操做)定位在Entry数组中的下标,而后遍历这以后的链表,经过equals比较有没有相同的key,若是有直接覆盖value,若是没有就从新建立一一个Entry。

hashmap为何能够插入空值?

HashMap中添加key == null的Entry时会调用putForNullKey方法直接去遍历table[0]Entry链表,寻找e.key == null的Entry或者没有找到遍历结束若是找到了e.key==null,就保存null值对应的原值oldValue,而后覆盖原值,并返回oldValue若是在table[O]Entrty链表中没有找到就调用addEntry方法添加一个key为null的Entry

Hashmap 为何线程不安全(hash 碰撞和扩容致使)

HashMap扩容的的时候可能会造成环形链表,形成死循环。

相关文章
相关标签/搜索