HashMap的原理和底层实现

HashMap的特点及原理分析

特点

HashMap的是java的中使用最为频繁的地图类型,其读写效率较高,但是因为其是非同步的,即读写等操作都是没有锁保护的,所以在多线程场景下是不安全的,容易出现数据不一致的问题。在单线程场景下非常推荐使用。

原理

HashMap中的整体结构,如下图所示:

HashMap的原理和底层实现

根据图片可以很直观的看到,HashMap中的实现并不难,是由数组和链表两种数据结构组合而成的,其节点类型均为名为条目的类(后边会对条目做讲解)。采用这种数据结果,即是综合了两种数据结果的优点,既能便于读取数据,也能方便的进行数据的增删。

每一个哈希表,在进行初始化的时候,都会设置一个容量值(容量)和加载因子(loadFactor)。容量值指的并不是表的真实长度,而是用户预估的一个值,真实的表长度,是不小于容量的2的整数次幂。加载因子是为了计算哈希表的扩容门限,如果哈希表保存的节点数量达到了扩容门限,哈希表就会进行扩容的操作,扩容的数量为原表数量的2倍。默认情况下,能力的值为16,loadFactor的值为0.75(综合考虑效率与空间后的折衷)。

  • 数据写入。以HashMap(String,String)为例,即对于每一个节点,其关键值类型为String,值的值类型也为String。在向哈希表中插入数据的时候,首先会计算出键值的的hashCode,即key.hashCode()。关于的hashCode方法的实现,有兴趣的朋友可以看一下JDK的源码(之前看到信息说有一次面试中问到了这个知识点)。该方法会返回一个32位的int类型的值,以int h = key.hashCode()为例。获取到h的值之后,会计算该键对应的哈​​希表中的数组的位置,计算方法就是取模运算,h%表.length。因为table的长度为2的整数次幂,所以可以用h与table.length-1直接进行位与运算,即是,index = h&(table.length-1)。得到的index就是放置。新数据的位置
HashMap的原理和底层实现


  • 如果插入多条数据,则有可能最后计算出来的指数是相同的,比如1和17,计算的指数均为1.这时候出现了散列冲突.HashMap解决哈希冲突的方式,就是使用链表。个链表,保存的是指数相同的数据。
  • 数据读取。从哈希表中读取数据时候,先定位到对应的索引,然后遍历对应位置的链表,找到关键值和的hashCode相同的节点,获取对应的值值。
  • 数据删除。在HashMap中中,数据删除的成本很低,直接定位到对应的索引,然后遍历该链表,删除对应的节点。哈希表中数据的分布越均匀,则删除数据的效率越高(考虑到极端场景,数据均保存到了数组中,不存在链表,则复杂度为O(1))。

JDK源码分析

构造方法

 /** * Constructs an empty {@code HashMap} with the specified initial * capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ 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); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }

从构造方法中可以看到

  • 参数中的参数:initialCapacity并不是哈希表的真实大小。真实的表大小,是不小于参数:initialCapacity的2的整数次幂。
  • 哈希表的大小是存在上限的,就是2的30次幂。当哈希表的大小到达该数值时候,之后就不再进行扩容,只是向链表中插入数据了。
  • PUT方法
 /** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with {@code key}, or * {@code null} if there was no mapping for {@code key}. * (A {@code null} return can also indicate that the map * previously associated {@code null} with {@code key}.) */ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }

可以看到:

  • 给哈希表分配空间的动作,是向表中添加第一个元素触发的,并不是在哈希表初始化的时候进行的。
  • 如果对应索引的数组值为空,即插入该索引位置的第一个元素,则直接设置标签[I]的值即可。
  • 查看数组中索引位置的节点是否具有相同的密钥和散列如果有,则修改对应值即可。
  • 遍历数组中索引位置的链表,如果找到了具有相同密钥和散列的节点,跳出循环,进行值更新操作。否则遍历到链表的结尾,并在链表最后添加一个节点,将对应数据添加进去。
  • 方法中涉及到了树节点,可以暂时先不关注。
  • GET方法
 /** * Returns the value to which the specified key is mapped, * or {@code null} if this map contains no mapping for the key. * * 

More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code (key==null ? k==null : * key.equals(k))}, then this method returns {@code v}; otherwise * it returns {@code null}. (There can be at most one such mapping.) * *

A return value of {@code null} does not necessarily * indicate that the map contains no mapping for the key; it's also * possible that the map explicitly maps the key to {@code null}. * The {@link #containsKey containsKey} operation may be used to * distinguish these two cases. * * @see #put(Object, Object) */ public V get(Object key) { Node e; return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * Implements Map.get and related methods * * @param hash hash for key * @param key the key * @return the node, or null if none */ final Node getNode(int hash, Object key) { Node[] tab; Node first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }

代码分析:

  • 先定位到数组中的索引位置,检查第一个节点是否满足要求 
  • 遍历对应该位置的链表,找到满足要求节点进行回报

扩容操作

 /** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ final Node[] resize() { Node[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node[] newTab = (Node[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode)e).split(this, newTab, j, oldCap); else { // preserve order Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }

代码分析:

  • 如果就容量大于0,容量到达最大值,则不扩容。容量未到达最大值,则新容量和新门限翻倍。
  • 如果旧门限和旧容量均为0,则相当于初始化,设置对应的容量和门限,分配空间。
  • 旧数据的整理部分,非常非常的巧妙,先膜拜一下众位大神。在外层遍历节点数组,对于每一个表[j]时,判断该节点扩容之后,是属于低位部分(原数组),还是高位部分(扩容部分数组)判断的方式就是位与旧数组的长度,如果为0则代表的是地位数组,因为索引的值小于旧数组长度,位与的结果就是0;相反,如果不为零,则为高位部分数组。低位数组,添加到以loHead为头的链表中,高位数组添加到以hiHead为头的数组中。链表遍历结束,分别设置新哈希表的索引位置和(指数+旧表长度)位置的值。非常的巧妙。

注意点

  • HashMap中的操作中未进行锁保护,所以多线程场景下存取数据,很存在数据不一致的问题,不推荐使用
  • HashMap的中键和值可以为空
  • 计算指数的运算,h&(length - 1),感觉很巧妙,学习了
  • 哈希表的扩容中的数据整理逻辑,写的非常非常巧妙,大开眼界


分享到:


相關文章: