一、HashMap使用
HashMap也是我们使用非常多的Collection,它是基于哈希表的 Map 接口的实现,以key-value的形式存在。
哈希表也称散列表,根据关键码值key 进行访问的数据结构,也就是说,能够将关键码映射到表中一个位置我们就可以去访问其记录value,加快查找的速度,这个映射函数叫做散列函数,存放记录的数组叫做散列表。
在HashMap中,key-value总是会当做一个整体来处理,系统会根据hash算法来来计算key-value的存储位置,我们总是可以通过key快速地存、取value。
HashMap实现了Map接口,继承AbstractMap。
数组:寻址容易O(1),插入和删除困难O(N)
链表:寻址困难O(N),插入和删除容易O(1)
综合两者的特性,变成了一个寻址容易,插入删除也容易的数据结构
哈希表有多种不同的结构(实现),我们介绍的是最常用的结构-拉链法(链地址法)
遍历方式
1.在for循环中使用entries实现Map的遍历
for (Map.Entry<Object, Object> entries : map.entrySet()) {
System .out .println(next .getKey() +":"+next .getValue() );
}
2.通过Iterator遍历
Iterator<Map.Entry<Object, Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Object, Object> entry = iterator.next();
System .out .println(next .getKey() +":"+next .getValue() );
}
二、 HashMap源码jdk1.8
- HashMap允许空值和空键
- HashMap是非线程安全
- HashMap元素是无序 LinkedHashMap TreeMap
- (HashTable不允许为空 线程安全)
属性
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
初始容量,加载因子。这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量,加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。
对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。
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;
//table不为空 且 table长度>0 且 table索引位置(table.length - 1&hash)的节点不为空
if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {
// 若first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
// first的next节点不为空则继续遍历
if ((e = first.next) != null) {
// 如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
流程:1、若table不为null 且 table的长度>0 且 索引节点first(table.length - 1&hash)不为null,则查找
2、若该索引的节点hash 和key值==传参,则返回first节点
3、若first.next不为null继续遍历
若该节点为红黑树类型,则getTreeNode()查找
若为普通链表、则遍历链表、找到与传参相等的节点,返回该节点
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;
// table是否为空||length==0,是resize()进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else { // table表该索引位置不为空,则进行查找
Node<K,V> e; K k;
// 判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断p节点是否为TreeNode, 如果是putTreeVal()查找目标节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//p节点为普通链表节点,使用binCount统计链表的节点数
else {
for (int binCount = 0; ; ++binCount) {
//如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 节点数是否超过8个,是treeifyBin()将链表节点转为红黑树节点,
// -1是因为循环是从p节点的下一个节点开始的
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果插入节点后节点数超过阈值,则调用resize方法进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
流程:1、若table 为空 || tab.length0,resize()进行初始化
2、找到传参的索引位置,如果索引头节点pnull,则在该索引新增一个节点
否则:
(1)如果p节点的key和hash值跟传入的相等, 则p节点为要查找的目标节点,将p节点赋值给e节点
(2) 判断p节点是否为TreeNode类型, 是则putTreeVal()查找目标节点
(3)判断p节点是否为普通链表节点,是使用binCount统计链表的节点数
如果p.next==null时,则找不到目标节点,新增一个节点并插入链表尾部
如果e节点hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
3、e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue、
4、如果size大于阈值、resize()扩容
resize()
resize时机
1)table==null
2) table需要扩容的时候
过程
1)table进行扩容
2)table原先节点进行重哈希
a.HashMap的扩容指的是数组的扩容,因为数组的空间是连续的,所以需要数组的扩容即开辟一个更大空间的数组,将老数组上的元素全部转移到新数组上来
b.在HashMap中扩容,先新建一个2倍原数组大小的新数组,然后遍历原数组的每一个位置,如果这个位置存在节点,则将该位置的链表转移到新数组
c.在jdk1.8中,因为涉及到红黑树,jdk1.8实际上还会用到一个双向链表去维护一个红黑树中的与元素,所以jdk1.8在转移某个位置的元素时,首先会判断该节点是否是一个红黑树节点,然后遍历该红黑树所对应的双向链表,将链表中的节点放到新数组的位置当中
d.最后元素转移完之后,会将新数组的对象赋值给HashMap中table属性,老数组会被回收