HashMap在java中使用的频率很高,同时也是面试时的必问的问题。今天咱们就来学习下jHashMap的源码,版本为jdk1.8。学习之前,先一起了解下HashMap的数据结构,便于理解后面所讲的内容。
HashMap的底层数据结构
由图可见,HashMap主要是由 数组+链表+红黑树 构成的。最外层是一个数组,数组中的每一个元素称作桶(segment),每个桶中存在着链表或红黑树,其中链表或红黑树中的每一个元素又称作bin。
简单的描述下put的步骤。往map中put键值对时,首先计算键值对中key的hash值,以此确定插入数组中的位置(也就是下标值),但是可能存在同一hash值的元素已经被放在数组同一位置了,这种现象称为碰撞,这时按照尾插法(jdk1.7及以前为头插法)的方式添加key-value到同一hash值的元素的后面,链表就这样形成了。当链表长度超过8时,链表自动转换为红黑树。
静态全区变量
/*** 默认初始化容量,值为16* 必须是2的n次幂.*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/*** 最大容量, 容量不能超出这个值。如果一个更大的初始化容量在构造函数中被指定,将被MAXIMUM_CAPACITY替换.* 必须是2的倍数。最大容量为1<<30,即2的30次方。*/
static final int MAXIMUM_CAPACITY = 1 << 30;/*** 默认的加载因子。*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;/*** 将链表转化为红黑树的临界值。* 当添加一个元素被添加到有至少TREEIFY_THRESHOLD个节点的桶中,桶中链表将被转化为树形结构。* 临界值最小为8*/
static final int TREEIFY_THRESHOLD = 8;/*** 恢复成链式结构的桶大小临界值* 小于TREEIFY_THRESHOLD,临界值最大为6*/
static final int UNTREEIFY_THRESHOLD = 6;/*** 桶可能被转化为树形结构的最小容量。当哈希表的大小超过这个阈值,才会把链式结构转化成树型结构,否则仅采取扩容来尝试减少冲突。* 应该至少4*TREEIFY_THRESHOLD来避免扩容和树形结构化之间的冲突。*/
static final int MIN_TREEIFY_CAPACITY = 64;
一起走遍HashMap的流程(举个栗子)
- 初始化HashMap
public static void main(String[] args) {HashMap<String, String> hashMap = new HashMap<>(2);hashMap.put("java", "爪哇");String java= hashMap.get("java");System.out.println(java);}
由于我们预计会放入一个元素,出于性能考虑,我们将容量设置为 2,既保证了性能,也节约了空间
/*** 初始化时进入的第一个方法*/public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}/*** 初始化时进入的第二个方法,传入参数有(容量值,加载因子)* 流程解析:如果初始容量小于零,则抛出异常;如果初始容量大于最大容量,将最大容量值赋值给初始容量;如果加载因子小于零也会抛出异常* 接着对负载因子进行赋值,最后通过特定方法计算阀值(无论放入任何一个int 数字,都能找到离他最近的 2 的幂次方数字(并且比他大)并赋值*/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 的两个构造方法,其中,我们设置了初始容量为 2, 而默认的加载因子我们之前说过:0.75,当然也可以自己设置,但 0.75 是最均衡的设置,没有特殊要求不要修改该值,加载因子过小,理论上能减少 hash 冲突,加载因子过大可以节约空间,减少 HashMap 中最耗性能的操作:reHash。
2.往HashMap中put键值对
/*** put时进入的第一个方法*/
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}/*** put时进入的第二个方法(计算key的hash值)* 流程解析:当key为null时,就返回零;不为null,则进入下一步计算,首先算出key的hashcode,当前key为“java”,则h=3254818,然后h* 异或h无符号右移16位的值,返回值为3254803*/
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}/*** put时进入的第三个方法*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 当前对象的数组是null 或者数组长度时0时,则需要初始化数组if ((tab = table) == null || (n = tab.length) == 0)// 得到数组的长度 16n = (tab = resize()).length;// 如果通过hash值计算出的下标的地方没有元素,则根据给定的key 和 value 创建一个元素if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else { // 如果hash冲突了Node<K,V> e; K k;// 如果给定的hash和冲突下标中的 hash 值相等并且 (已有的key和给定的key相等(地址相同,或者equals相同)),说明该key和已有的key相同if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))// 那么就将已存在的值赋给上面定义的e变量e = p;// 如果以存在的值是个树类型的,则将给定的键值对和该值关联。else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 如果key不相同,只是hash冲突,并且不是树,则是链表else { // 循环,直到链表中的某个节点为null,或者某个节点hash值和给定的hash值一致且key也相同,则停止循环。for (int binCount = 0; ; ++binCount) {// 如果next属性是空if ((e = p.next) == null) {// 那么创建新的节点赋值给已有的next 属性p.next = newNode(hash, key, value, null);// 如果树的阀值大于等于7,也就是,链表长度达到了8(从0开始)。if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st// 如果链表长度达到了8,且数组长度小于64,那么就重新散列,如果大于64,则创建红黑树treeifyBin(tab, hash);// 结束循环break;}// 如果hash值和next的hash值相同且(key也相同)if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))// 结束循环break;// 如果给定的hash值不同或者key不同。// 将next 值赋给 p,为下次循环做铺垫p = e;}}// 通过上面的逻辑,如果e不是null,表示:该元素存在了(也就是他们呢key相等)if (e != null) { // existing mapping for key// 取出该元素的值V oldValue = e.value;// 如果 onlyIfAbsent 是 true,就不要改变已有的值,这里我们是false。// 如果是false,或者 value 是nullif (!onlyIfAbsent || oldValue == null)// 将新的值替换老的值e.value = value;// 访问后回调afterNodeAccess(e);// 返回之前的旧值return oldValue;}}// 如果e== null,需要增加 modeCount 变量,为迭代器服务。++modCount;// 如果数组长度大于了阀值if (++size > threshold)// 重新散列resize();// 插入后回调afterNodeInsertion(evict);// 返回nullreturn null;}
该方法为 HashMap 的核心方法,以下是该方法的步骤。
①判断数组是否为空,如果是空,则创建默认长度位 16 的数组。
②通过与运算计算对应 hash 值的下标,如果对应下标的位置没有元素,则直接创建一个。
③如果有元素,说明 hash 冲突了,则再次进行 3 种判断。
1.判断两个冲突的key是否相等,equals 方法的价值在这里体现了。如果相等,则将已经存在的值赋给变量e。最后更新e的
value,也就是替换操作。
2.如果key不相等,则判断是否是红黑树类型,如果是红黑树,则交给红黑树追加此元素。
3.如果key既不相等,也不是红黑树,则是链表,那么就遍历链表中的每一个key和给定的key是否相等。如果,链表的长度
大于等于8了,则将链表改为红黑树,这是Java8 的一个新的优化。
④最后,如果这三个判断返回的 e 不为null,则说明key重复,则更新key对应的value的值。
⑤对维护着迭代器的modCount 变量加一。
⑥最后判断,如果当前数组的长度已经大于阈值了。则重新hash。
3.根据键get值
/*** get时进入的第一个方法* 返回指定的key映射的value,如果value为null,则返回null。*/
public V get(Object key) {Node<K,V> e;//如果通过key获取到的node为null,则返回null,否则返回node的value。getNode方法的实现就在下面。return (e = getNode(hash(key), key)) == null ? null : e.value;
}
get(E e)可以分为三个步骤:
- 通过hash(Object key)方法计算key的哈希值hash。
- 通过getNode( int hash, Object key)方法获取node。
- 如果node为null,返回null,否则返回node.value。
/***g et时进入的第二个方法* 根据key的哈希值和key获取对应的节点* * @param hash 指定参数key的哈希值* @param key 指定参数key* @return 返回node,如果没有则返回null*/
final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;//如果哈希表不为空,而且key对应的桶上不为空if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {//如果桶中的第一个节点就和指定参数hash和key匹配上了if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))//返回桶中的第一个节点return first;//如果桶中的第一个节点没有匹配上,而且有后续节点if ((e = first.next) != null) {//如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);//如果当前的桶不采用红黑树,即桶中节点结构为链式结构do {//遍历链表,直到key匹配if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}//如果哈希表为空,或者没有找到节点,返回nullreturn null;
}
getNode方法又可分为以下几个步骤:
①如果哈希表为空,或key对应的桶为空,返回null
②如果桶中的第一个节点就和指定参数hash和key匹配上了,返回这个节点。
③如果桶中的第一个节点没有匹配上,而且有后续节点
1.如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点
2.如果当前的桶不采用红黑树,即桶中节点结构为链式结构,遍历链表,直到key匹配
④找到节点返回null,否则返回null。
3.resize() 扩容机制
声明一个hashmap时不给它一个容量值时,hashmap会默认的容量值为16。若声明时给定的容量值非2的n次幂,则会自动转为2的n次幂,比如初始值给的5,hashmap会自动转换为8。
如果 put值的数量大于阈值时,hashmap就会执行扩容,其中阈值为数组长度*加载因子。比如我们使用hashmap的默认容量16时,这时阈值=0.75*16=12,接着我们再put第十三个数据时,hashmap就开始扩容,扩容之后的长度为原长度的2倍,也是32。扩容就是把原来的小水桶废弃,直接用更大的水桶替换。
PS:部分图文来源网络(侵删)