目录
一、HashMap简介
HashMap主要是用来存放键值对的,其key可以是null但是只能存在一个是null,而value可以存在多个。 HashMap是非线程安全的,实现了Map接口,是java中常用的集合之一,JDK8之前是有数组和链表实现的,而哈希表是为了减少哈希碰撞的;JDK8后对哈希碰撞优化了许多,当链表的长度大于默认的阈值的时候(默认为8),(当长度大于8小于64的候,会 先进行扩容,而不是进行转换为红黑树,只有长度大于等于64的时候,才会转换为红黑树),会进行扩容,转换为红黑树只是为了减少查找的时间。HashMap默认的初始化的长度是16,每次的扩容会增加为原来的两倍。
二、有关数据结构的分析
HashMap集合,其实就是一个散列表,使用扰动函数hash(),将key传入,通过key的hashcode值来计算hash值,然后通过(n-1)&hash,经过与运算得到存放的位置,当位置存在元素的时候,会比较key是否相等,即比较存在的元素和要存进去的元素的hash值和key值是否相等,相等就覆盖,不相等就存在元素后面,(无的理解就是,因为是通过数组和链表来实现的,那么主要的部分是数组,每一个数组元素的位置就是存放的一个链表,通过与运算得到的位置存在元素的时候,判断过后又不相等,那么就把他存在这个元素的后面,不知道我理解对不对),其实扰动函数就是为了减少哈希碰撞。
这是JDK8的hash扰动函数
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这是JDK7的扰动函数
static final int hash(Object key) {
int h;
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
明显可以看出JDK7比8多扰动了两次,所以在新能上会比JDK8要差许多,也就减少了一些哈希碰撞,其实hash方法主要是解决一些hashcode计算差的实现。
JDK8之后的最大优化就是这个扩容的机制,减小哈希碰撞,解决了链表长度带来的搜索能力差的问题,在大于64的时候转换为红黑树,大大减少了查询时间。当链表长度大于阈值8的时候,会先调用treeifyBin()这个方法,可以看出,先会获取到传入map长度为多少,判断长度是否为null或者小于64,满足才会调用resize()方法,进行扩容;如果长度大于64,那么就会执行这个方法进行转换为红黑树。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
底层的字段解析
//初始容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子,默认为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//该值是在判断是否转化为红黑树时使用的阈值,即想当于一个阈值计数,必须大于2小于8,大于8就会判断,而
//扩容
static final int TREEIFY_THRESHOLD = 8;
//当改变大小的期间,bucket的数量小于这个值
static final int UNTREEIFY_THRESHOLD = 6;
//判断是否转换为红黑树的最大临界值,当大于等于这个值的时候,就会转换为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//加载因子
final float loadFactor;
三、主要方法解析
1、构造器
①这个构造器,主要是两个参数,初始化容量和加载因子
//两个参数 initialCapacity 初始容量 loadFactor 加载因子
public HashMap(int initialCapacity, float loadFactor) {
/**
* 开始时判断初始容量是否大于零,成立则会抛出异常IllegalArgumentException
* 参数异常,即非法初始化容量
*/
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//当大于最大容量时,则会将容量规定为最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//当加载因子小于等于零或者为空的时候也会抛出一个异常IllegalArgumentException,参数异常,
//非法
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//都不满足才会执行下面这个,创建集合
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
②这个是只有一个参数,初始化容量
//当给定只有一个参数的时候,返回时,会默认加上负载因子
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
③这个是没有参数的,无参构造器
//默认容量大小为16,负载因子为0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
④传递的是一个map集合
//把一个map集合转化为hashmap集合,负载因子为默认的0.75
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) {
//获取得到的map集合的大小
int s = m.size();
//当map的容量大于零的时候
if (s > 0) {
//当node值为null是,即里面没有对象
if (table == null) { // pre-size
//计算ft的值与最大容量对比,获得的值给t
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//判断t的大小与目标的容量的2次幂
if (t > threshold)
//满足就替换
threshold = tableSizeFor(t);
}
//判断s与目标的容量的2次幂
else if (s > threshold)
//满足就调用方法进行扩容
resize();
//否则就进行转移数据到新的hashmap集合中,使用迭代器
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);
}
}
}
2、其他方法
//返回集合的大小
public int size() {
return size;
}
//返回集合是否为空
public boolean isEmpty() {
return size == 0;
}
//这个方法就是判断传过来的hash值,和key值是否存在
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//判断table中不为空,或者长度大于零,并且位置的n-1与hash值进行与运算是不为零的,才能进行下面
//的操作
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//要判断第一个hash值和传来的hash值是否相等,总要检查第一个节点
if (first.hash == hash && // always check first node
//判断第一个节点的key值是否等于这个传来的key或者传过来的key不等于null,并且用equals来比较也
//是相等的才会返回第一个节点的node值
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//判断第一个节点的下一个节点不为null
if ((e = first.next) != null) {
//判断第一个节点是否是树节点,是就调用getTreeNode()方法计算数节点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//不满足上面这些,就会循环遍历找到满足条件的key值
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
//循环条件是下一个不为null
} while ((e = e.next) != null);
}
}
return null;
}
下面这个方法,put是hashmap集合中的唯一的一个存放值的方法(供使用者调用的),调用的putVal()方法主要是为了,判断存入的值是否在链表中存在,判断这个key的hash值是否是节点值,不是map的节点值,还会判断是否是treemap的树节点值,是节点值,还会判断key是否存在,如果key存在还要满足使用equals()方法判断两个key值是否是相等的,相等就会覆盖,不等就会使用拉链法,放在该值的后面,这些作完还需要判断阈值大小,如果在8到64之间要进行扩容,resize()方法。afterNodeInsertion()方法只是会函数,为了方便LinkedHashMap()的处理。
//这个方法和get()差不多,get是获取value值,这个就是想判断集合中是否存在这个key,返回boolean类型
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断table的node是否存在
if ((tab = table) == null || (n = tab.length) == 0)
//满足就进行扩容
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//数据所在的位置是不是为null
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//判断p的hash值和传过来的key的hash值是否相等,或key不等于null与key调用equals判断为true
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//满足则有e与p是相等的
e = p;
else if (p instanceof TreeNode)
//判断是不是tree的节点,是就调用getTreeNode()方法得到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;
}
这个方法是生成树,从这个节点开始,balanceInsertion这个方法就是,判断左右节点是否有值,还有就是父节点是否存在值,然后返回Treeify()方法,结束循环,执行moveRootToFront方法,最后就是,把集合中的值搬移到另外一个新的tree树结构的集合中,要确保root是第一个节点,要计算第一个值的位置,通过n-1&hash做与运算的到位置,把node的值给到frist;当root不是第一个节点的时候,刚刚计算得到的位置的value值就是root,root.pre就会在下一次删除的时候断开链接。继续判断下一个节点是不是为null,frist.pre节点后的节点链接断开;这些不满足就会得到root节点后的第一个节点就是frist,然后root断开链接后的节点为null(这是root是第一个节点的情况)。
然后调用checkInvariants(root)方法,排序的方法,要判断父节点parent,左节点left,右节点right,大小进行排序,使用二叉树排序
//判断两个对象是不是一个对象,是就返回1,不是就返回-1
static int tieBreakOrder(Object a, Object b) {
int d;
f (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
final void treeify(Node<K,V>[] tab) {
//初始化TreeNode集合为null
TreeNode<K,V> root = null;
//循环遍历,获取node值,直到下一个是null,才停止循环
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
//如果节点值为null,不是红节点,节点值为x
x.parent = null;
x.red = false;
root = x;
}
else {
//计算x的hash值
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
//对比p的hash值和x的hash值,对dir赋值
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
//数据类型比较器,是null还是string
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
//比较k和pk是不是相同的值
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
//判断返回的dir是1还是-1,进而判断p节点的左右是否有值
if ((p = (dir <= 0) ? p.left : p.right) == null) {
//没有值就把xp设为上一节点
x.parent = xp;
//dir为-1的话,左节点就为x
if (dir <= 0)
xp.left = x;
else
//否则右节点为x
xp.right = x;
//然后调用方法balanceInsertion计算root值,然后跳出循环执行moveRootToFront方法
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,TreeNode<K,V> x) {
//设置x为红节点
x.red = true;
//定义四个TreeNode类型的变量
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//判断父节点是否为空
if ((xp = x.parent) == null) {
//为空就不是红节点
x.red = false;
return x;
}
//xp不是红节点,或者xp不是父节点
else if (!xp.red || (xpp = xp.parent) == null)
//返回root
return root;
//判断左右节点是不是红节点
if (xp == (xppl = xpp.left)) {
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
else {
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
//搬移到新的集合中
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
//root,tab及tab的长度不能为null
if (root != null && tab != null && (n = tab.length) > 0) {
//计算第一个数值的位置,位置n-1与root的hash值做与运算得到位置
int index = (n - 1) & root.hash;
//得到第一个位置的节点值
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
//如果root不是第一个节点,那么会确定计算得到的位置是第一个节点值,value的值就root
Node<K,V> rn;
tab[index] = root;
TreeNode<K,V> rp = root.prev;
//下一次删除的时候要断开链接
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
assert checkInvariants(root);
}
}
//递归不变的检查,得到树结构
static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
tb = t.prev, tn = (TreeNode<K,V>)t.next;
if (tb != null && tb.next != t)
return false;
if (tn != null && tn.prev != t)
return false;
if (tp != null && t != tp.left && t != tp.right)
return false;
if (tl != null && (tl.parent != t || tl.hash > t.hash))
return false;
if (tr != null && (tr.parent != t || tr.hash < t.hash))
return false;
if (t.red && tl != null && tl.red && tr != null && tr.red)
return false;
if (tl != null && !checkInvariants(tl))
return false;
if (tr != null && !checkInvariants(tr))
return false;
return true;
}
3、get方法
get方法就是通过key来计算节点值,通过调用getNode来的到key的节点值,先要判断是否右这个节点,才能得到value值。getNode方法在我看来就是计算hash值,找到位置,也是通过(n - 1) & hash来做与运算得到位置,得到位置,就会判断这个位置上的key值是不是和给的值一样,不一样就使用拉链法放在后面,一样就覆盖,在对比key的时候既要用==来判断,也要用equals来判断key值,最后才能得到key所对应的value值。
//获取集合中的数据
public V get(Object key) {
Node<K,V> e;
//调用方法getNode(),判断是否有这个hash值,和key值,有就返回key对应的value值,没有就返回null
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> 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<K,V>)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;
}
4、put方法
put方法其实和get方法相似,都要通过key值判断集合中是否存在key这个键,没有才能存储值,若果相同那么就覆盖,会调用putVal方法,上面右,就部解释来,其实就和getNode相似。
//put方法为hashmap集合的唯一一个存储值的方法(传递单个值)
public V put(K key, V value) {
//调用putVal()方法存储
return putVal(hash(key), key, value, false, true);
}
5、resize方法
扩容方法,先会判断容量大小,如果为空,那么就会以初始容量大小进行扩容,不为空,那么就会把数组大小给到oldCap中,然后会定义新的容量大小和新的阈值。然后oldCap与最大值进行比较,大于最大值,那么阈值大小也等于最大值;否则那就是oldCap左移一位的到newCap,相当于扩大两倍,并且要判断是否大于最大值,然后阈值大小也要扩大到原来的两倍。下面就要判断oldTab是否为null,进行for循环,新建Node数组e,先回判断第一个节点是否为null,不等于null那就把老的node改为null,在判断前已经把旧的值给新的来,那么就会判断下一个节点是不是null,等于null的话那就计算新的数组中的位置把e放到新数组中;然后会判断是不是treeNode,即是不是tree类数据,是就调用split方法把树进行拆分;后面就是把旧的数组里面的值搬移到新的数组中,因为不确定旧的数组中的节点是不是在新的数组中的同样位置,所以要确定搬移的位置,所以定义了四个数据来确定新数组的位置,其实新的位置就是在原来旧的位置和旧的位置加上旧的数组的大小,就在这两个位置,所要要判断是在高位,还是在低位,要hash值在进行因此判断,得到新的数组的位置,得到新的链表后,然后把旧的值搬移到新的位置。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//判断数组oldTab是否为空
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;
//然后返回oldTab
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
else if (oldThr > 0)
newCap = oldThr;
else {
//获得新的数组容量大小和阈值大小,为了下面创建新的数组做铺垫
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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//创建新的数组
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> 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<K,V>)e).split(this, newTab, j, oldCap);
else {
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> 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;
}