前言
HashMap是一个不管在工作中还是面试中,都经常会碰到地一种集合,对其进行学习,不管对其他源码的学习还是更合理的使用HashMap上都有很大的帮助。
本篇主体以HashMap最常用的put,remove而展开的源码解析。
灵魂一问
了解过HashMap的人应该都知道HashMap的默认容量是2 n ,扩容也是按两倍扩容。 这里大家肯定想说:
为什么HashMap容量都是2的n次幂呢?
HashMap是利用hash值来计算应该存放在数组中的位置。如果hash值大于当前容量呢,该怎么计算,一般都会想到用求余的方法:index = hash % size,这样能分散得出在当前容量的table中的位置。不过这样真的好吗?
求余运算在编译后是这样的: a % b 旧相当与 a - (a / b) * b 的运算。是多步运算。
而 & 运算,编译后就是一条CPU指令,效率要比求余快得多。而当b是2 n 时,a % b 的结果等同于 (b-1) & a 。
而HashMap就是通过这样,高效的算出一个桶的index在哪里。
成员变量
话不多说切入正题。
这里大致描述每个变量的作用,后面源码分析中,会进一步看到变量如何使用。
- 默认容量 2 4
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;复制代码
- 最大容量 2 30
static final int MAXIMUM_CAPACITY = 1 << 30;复制代码
由上面两个变量以即HashMap的扩容为2倍的机制,不禁会有一个疑问。
为什么HashMap容量都是2的n次幂呢?
大家都知道HashMap是利用hash值来计算应该存放在数组中的位置。如果hash值大于当前容量呢,该怎么计算,
- 默认加载因子(笔者比较喜欢称作扩容系数)
static final float DEFAULT_LOAD_FACTOR = 0.75f;复制代码
- 树化阈值(链表长度到达这个值就会转换为树结构)
static final int TREEIFY_THRESHOLD = 8;复制代码
- 解除树形阈值(树的节点树小于这个长度就会退化为链表结构)
static final int UNTREEIFY_THRESHOLD = 6;复制代码
- 最小树化容量(当Node数组容量小于这个值时不会树化,只会扩容)
static final int MIN_TREEIFY_CAPACITY = 64;复制代码
- 桶数组
transient Node[] table;复制代码
- 桶集合(用来提供hashMap的集合操作,本身不存储元素)
transient Set> entrySet;复制代码
- 容量大小
transient int size;复制代码
- 修改的次数
transient int modCount;复制代码
hashMap在迭代的时候,会把modCount传入,作为期望值(类似于乐观锁)如果迭代的时候其他线程进行了增删改操作,modCount就会改变,改变后继续迭代会抛出异常ConcurrentModificationException。
- 节点数组(大部分都把它叫桶数组)
transient Node[] table;复制代码
- 扩容阈值
int threshold;复制代码
到达这个值,HashMap就会执行扩容
桶的结构
本篇源码都是JDK1.8版本的,在1.8之后,HashMap的桶内结构分为两种,一种是链表,还有一种树结构。
链表
static class Node implements Map.Entry { final int hash; //hash值 final K key; //键 V value; //值 Node next; //下一个节点 ... }复制代码
树
static final class TreeNode extends LinkedHashMap.Entry { TreeNode parent; // 父节点 TreeNode left; //左子节点 TreeNode right; //右子节点 TreeNode prev; //前节点 boolean red; //是否为红节点 }复制代码
这里继承了LinkHashMap的Entry,而LinkHashMap的Entry又继承了HashMap的Node。所以这里的树节点,不仅是红黑树结构,还包含着一个双向链表。类似下图这种关系,除了树的根节点是链表的首节点之外,链表顺序与树节点顺序没有什么关系(后面源码之中也会验证这一点)。