HashMap 概述
HashMap 是 Map 族下我们最常用的一种,HashMap 是基于哈希表的 Map 接口的实现。存储的对象是 Entry(包含了 Key 和 Value)。
在 HashMap 中,会根据 hash 算法来计算 key-value的存储位置并进行快速存取,特别的,HashMap 最多只允许一条 Entry 的建为 NULL,多余的会覆盖,但允许多条 Entry 的值为 NULL 。此外,HashMap是 Map 的一个非同步实现类。
虽然 HashMap 和 HashSet 实现的接口规范不同,但是他们底层的 Hash 存储机制完全相同。 实际上, HashSet 本身就是在 HashMap 的基础上实现的。
HashMap 在 JDK 中的定义(1.8版本)
HashMap 实现了 Map 接口,并集成了 AbstractMap 抽象类,和 AbstractCollection 抽象类在 Collection 族的作用类似。AbstractMap 提供了 Map 接口的骨干实现。 以最大限度的减少实现 Map 接口所需的工作,
1 2 public class HashMap <K ,V > extends AbstractMap <K ,V >implements Map <K ,V >, Cloneable , Serializable
HashMap 的构造函数
HashMap 一共提供了四个构造函数,其中默认无参的构造函数和参数为 Map 的构造函数为 Java Collection Framework 规范的推荐实现,其余两个构造函数则是 HashMap 专门提供的。
HashMap()
该构造函数,构造一个具有 > 默认初始容量(16)和默认负载因子(0.75)的空 HashMap,1.8版本中,对其进行了优化,改为put时才会创建 table[]。
1 2 3 public HashMap () { this .loadFactor = DEFAULT_LOAD_FACTOR; }
HashMap(int initialCapacity, float loadFactor)
该构造函数意在构造一个指定初始容量和指定负载因子的空 HashMap,赋值时才创建 table[]
1 2 3 4 5 6 7 8 9 10 11 12 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); }
HashMap(int initialCapacity)
HashMap(Map<? extends K,? extends V> m)
该构造器意在构造一个与指定 Map 具有相同映射的 HashMap, 初始容量不小于16,具体容量依赖指定 Map的大小,负载因子是0.75。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public HashMap (Map<? extends K, ? extends V> m) { this .loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false ); } final void putMapEntries (Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0 ) { if (table == null ) { float ft = ((float )s / loadFactor) + 1.0F ; int t = ((ft < (float )MAXIMUM_CAPACITY) ? (int )ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } else if (s > threshold) resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false , evict); } } }
所谓初始容量和负载因子,这两个参数是影响 HashMap 性能的重要参数,容量表示哈希表中 table 数组的大小,负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间使用程度。
负载因子越大,散列表的填装度越高,反之越小
Hash 的相关概念
Hash 就是把任意长度的输入 pre-Input(预映射),通过哈希算法,变换成固定长度的输出(通常是整型),该输出是哈希值。 这种转换是一种压缩映射。简单的说,就是一种将任意长度的消息压缩到某一固定长度的消息摘要函数。
HashMap 的数据结构
哈希在数据结构中的应用
数组的特点:寻址容易,插入和删除困难。
链表的特点:寻址困难,插入和删除容易。
如果综合两者的特性,做出一种寻址容易,插入和删除也容易的数据结构,哈希表。哈希表有多种实现方式,最经典的一种叫做 拉链法 可以理解为 链表的数组。如下图所示:
左边是个数组,数组的每个成员是个链表,根据元素的自身特征,把元素分配到不同的链表中去,反过来,根据特征找出正确的链表,再从链表中找出正确的元素。
其中,根据元素特征计算元素数组下标的方法就是哈希算法。
哈希表适合快速查找、删除的基本数据结构:
hash 函数(哈希算法)的选择:针对不同的对象(字符串、整数等)具体的法系方法
碰撞处理:常用的两种方式,一种是 open hashing,即拉链法
另一种是 closed hashing,即开地址法
Java 中最常用的两种结构是数组和链表,几乎所有的数据结构都可以利用这两种来组合实现,实际上,HashMap 就是一个链表数组,以下是它的数据结构:
initialCapacity 就代表了该数组的长度。
Entry
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 static class Node <K ,V > implements Map .Entry <K ,V > { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this .hash = hash; this .key = key; this .value = value; this .next = next; } public final K getKey () { return key; } public final V getValue () { return value; } public final String toString () { return key + "=" + value; } public final int hashCode () { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue (V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals (Object o) { if (o == this ) return true ; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true ; } return false ; } }
Node(Entry) 为 HashMap 的内部类,实现了 Map.Entry 接口,包含了键 key,值 value,下一个结点 next,以及hash值四个属性。 Node(Entry) 是构成哈希表的基石,是哈希表所存储的元素的具体形式。
HashMap 的快速存储
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); } final V putVal (int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> 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<K,V> 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<K,V>)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 ) treeifyBin(tab, hash); break ; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break ; p = e; } } if (e != null ) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null ) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null ; }
正如 JDK 官方对该方法的描述那样,使用 hash() 方法对一个对象的 hashCode 进行重新计算,是为了防止质量低下的 hashCode() 函数实现。 由于 hashMap 的支撑数组长度总是 2 的幂次,通过右移可以使得低位的数据尽量不同,使得 hash 值分布均匀。
通过上述 hash() 方法计算得到的 Key 的 hash 值后,怎么才能保证元素均匀分布到 table 的每个桶中?我们会想到取模, 但是取模效率低, HashMap 是通过 t = (hash & (length - 1))处理,简单而高效。
将新的创建的 Entry 链入链表的表头。如果 HashMap 中元素的个数超过极限值,则容量扩大两倍。
随着 HashMap 中元素数量越来越多,发生碰撞的概率将越来越大,产生的子链长度也越来越长,这样会影响 HashMap 的存储速度,为了保证 HashMap 的效率,系统需要在某个临界点进行扩容,该临界点就是:HashMap 中的元素数量在数值上等于 threshold(table 数组长度加载因子) 它需要重新计算这些元素在新 table 中的位置并进行复制处理。
如果能提前预知 HashMap 中元素的个数,那么在构造 HashMap 时预设元素的个数能够有效提高 HashMap 性能。
步骤:
若 oldCapacity 已达到最大值,直接将thresold 设为 Integer.Max_VALUE 然后返回
否则,创建一个更大的数组,将每条 Entry 重新哈希到新的数组中
重哈希主要是一个重新计算原 HashMap 中的元素在新 table 数组中的位置并进行复制处理的过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void transfer ( Entry[] newTable ) { Entry[] src = table; int newCapacity = newTable.length; for ( int j = 0 ; j < src.length; j++ ) { Entry<K, V> e = src[j]; if ( e != null ) { src[j] = null ; do { Entry<K, V> next = e.next; int i = indexFor( e.hash, newCapacity ); e.next = newTable[i]; newTable[i] = e; e = next; } while ( e != null ); } } }
重哈希的步骤:
将原数组 table 赋值给 src
将数组 src 中的每条链重新添加到 newTable 中
每条链中的每个元素依次添加到 newTable 中相应的索引中。
将根据新的数组长度,对 hash(key.hashCode())的返回值进行重新分配索引
HashMap 的读取实现
相对 HashMap 的存储而言,读取就显得比较简单,HashMap 只需通过 key 的 hash 值定位到 table 数组的某个特定的索引,然后查找并返回 该 key 对应的 value 即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 ); }
步骤:
根据 key 的 hashCode 值计算它的 hash 码
根据 hash 码,找到 table 数组中对应的索引
再索引链表中 找到 key 相同的 Entry,返回 entry.value 即可