HashMap
无序,存储与取出的顺序不相同;不可重复,存储的数据不能重复。
底层数据结构为哈希表,哈希表又称为散列表,是由数组和单向链表的组合形成的数据结构。JDK8之后,当链表长度达到8时,则会转化为红黑树。
存取原理:
1、put(k, v)
第一步:先将key,value封装到一个node节点中。
第二步:底层调用hashCode()计算hash的值,并通过哈希函数的算法将hash值转化为数组下标,如果下标位置上没有任何元素,就把Node添加到这里;如果下标位置上有链表,会用指定key与链表上的节点中的key进行比对,如果相同,则将value覆盖,如果不同,就将这个新节点添加在这个链表的末尾。
2、get(k)
通过hashCode()方法计算出指定key对应的hash值,再通过哈希算法将其转化为数组下标,然后通过数组下标找到哈希表中数组对应的位置,如果这个位置上什么都没有,则返回null;如果有单向链表,再通过equals方法对比这个下标所在的位置上的链表中的所有节点的key值,如果所有都返回false,则返回null,如果指定key值与某个节点的key通过equals方法返回true,则将这个节点的value值返回。
综上,注意放在HashMap中key部分的元素和HashSet中的元素,要同时重写hashCode()和equals()。
初始化容量
HashMap的初始化容量为16,指定容量时必须是2的倍数,这是因为达到散列均匀同时为了提高HashMap的存取效率。默认加载因子是0.75。
默认加载因子是指 当HashMap集合底层哈希表中的数组容量达到75%时,数组开始扩容。
JDK8之后,HashMap中单链表的长度大于8时,会将其改为红黑树的数据结构,小于6的时候会变回单向链表。为了提高检索效率。
源码分析
1、get
public V get(Object key) {
Node<K,V> e;
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) {
//通过key的哈希值与数组长度-1进行按位与运算得到数组下标
//如果该处的结点的哈希值与指定key的哈希值一致并且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) {
//链表长度大于8的情况下在这个结点在红黑树中,那么返回这个红黑树中指定key的树结点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//如果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;
}
2、put
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;
//如果数组为初始化,长度为0则进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//判断桶中第一个元素(数组中的)是否为null,是null就将新节点放在这里
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//数组中已存在元素
else {
Node<K,V> e; K k;
//比较桶中的元素(数组中的)与新插入的元素hash值,key值是否相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//将桶中的第一个元素赋值给e,用e来记录
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) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//判断链表中是否存在这个结点,如果存在直接跳出循环修改value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;//与前面的判断中e=p.next配合遍历链表
}
}
//桶中存在与新插入的结点key、hash值相同的结点(结点已存在)
if (e != null) { // existing mapping for key
//记录e的value
V oldValue = e.value;
//onlyIfAbsent为false(可以更改已存在的value)或者e的value为null
if (!onlyIfAbsent || oldValue == null)
//将value覆盖,新值替换旧值
e.value = value;
//访问后回调
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//插入后实际大小大于阈值则重新扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
分析不到位的地方望大家多多指正!