HashMap存储结构
HashMap根据hash算法计算出index,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap只允许一条记录的key为null,允许多条value为null。
HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
源码解析
成员变量
bin(是hashmap专用术语,约定桶后面存放的每一个数据称为bin )
/**
* 默认的初始容量,必须是2的幂。16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 最大的容量。1073741824 必须是2的幂
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 没有指定的时候默认加载因子 默认的加载因子0.75是对空间和时间效率的一个平衡选择
* 如果内存空间很多而又对时间效率要求很高,可以降低加载因子Loadfactor的值;
* 如果内存空间紧张而对时间效率要求不高,可以增加加载因子loadFactor的值,这个值可以大于1。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* bin转为红黑树判断条件之一 bin数量大于8
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 由树转换成链表的阈值UNTREEIFY_THRESHOLD当执行resize操作时
* 当桶中bin的数量少于UNTREEIFY_THRESHOLD时使用链表来代替树。默认值是6
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 如果bin中的数量大于TREEIFY_THRESHOLD,但是capacity小于MIN_TREEIFY_CAPACITY,依然使用链表存储。
* 此时会进行resize操作,如果capacity大于MIN_TREEIFY_CAPACITY进行树化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 存放KV数据的数组。第一次使用的时候被初始化,根据需要可以重新resize。分配的长度总是2的幂。
*/
transient Node[] table;
/**
* 当被调用entrySet时被赋值。通过keySet()方法可以得到map key的集合,通过values方法可以得到map *value的集合。
*/
transient Set> entrySet;
/**
* map的大小
*/
transient int size;
/**
* 结构修改的次数
*/
transient int modCount;
/**
* 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
*/
int threshold;
/**
* 填充因子
*/
final float loadFactor;
构造函数
/**
* @param initialCapacity 初始容量
* @param loadFactor 填充因子
*/
public HashMap(int initialCapacity, float loadFactor) {
//容量小于0抛出illega异常
if (initialCapacity < 0) {
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
}
//如果初始容量大于1<<30 那么容量大小就等于1<<30
if (initialCapacity > MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY;
}
//如果加载因子小于等于0或者不是float类型
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
}
//赋值填充因子
this.loadFactor = loadFactor;
//临界值 刚开始以为这样写是一个Bug,
//觉得应该这样写this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
//此处带着疑问去看put方法,就会明白此处为啥这么设计。这里是懒加载。并没有new的时候直接加载
//找到大于等于initialCapacity的最小的2的幂
this.threshold = tableSizeFor(initialCapacity);
}
/**
* @param 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,使用默认加载因子
*/
public HashMap(Map extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
//这个方法下文putAll的时候会详细介绍
putMapEntries(m, false);
}
put()解析
/**
* @param key
* @param value
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 这里就是低16位和高16位异或。
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* @param key hash以后的值
* @param key
* @param value
* @param onlyIfAbsent true:不改变存在的值;false:改变存在的值
* @param evict if false, the table is in creation mode.
* @return 返回老的值或者空
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab;
Node p;
int n, i;
//判断节点数组是否为null,是null进行扩容 ;不等于null,把节点长度赋值给n,节点长度为0扩容
if ((tab = table) == null || (n = tab.length) == 0)
//进行扩容操作,并把扩容后的长度赋值给n
{
n = (tab = resize()).length;
}
// (tab.length-1)&hash 得到index 。注意,同一个元素在扩容前后可能得到的index不一样
if ((p = tab[i = (n - 1) & hash]) == null)
//如果等于null,直接newNode,放入tab;不等于null,p=当前下标得到的第一个bin
{
tab[i] = newNode(hash, key, value, null);
} else {
Node e;
K k;
//判断第一个节点是否等于当前元素,如果等于赋值给e。
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
e = p;
}
//判断p是否是treeNode,如果是红黑树,则添加数据。红黑树的crud有文章介绍
else if (p instanceof TreeNode) {
e = ((TreeNode) p).putTreeVal(this, tab, hash, key, value);
} else {
//循环查找元素
for (int binCount = 0; ; ++binCount) {
//获取p的下一个bin
if ((e = p.next) == null) {
//构建一个新的节点,把地址赋值给上个记得点的next指针域
p.next = newNode(hash, key, value, null);
//判断bin数量是否大于8。这里减1因为binCount是从0开始的
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//转为红黑树。
//如果容量小于MIN_TREEIFY_CAPACITY,不会进行树化,会进行扩容
{
treeifyBin(tab, hash);
}
break;
}
//判断元素是否等于当前元素。和上面同理
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
break;
}
p = e;
}
}
//e不等于空,说明之前已经存在这个key
if (e != null) { // existing mapping for key
//获取老的值
V oldValue = e.value;
//onlyIfAbsent=fals 或者oldValue=null 改变存在的值。返回老的值
if (!onlyIfAbsent || oldValue == null) {
e.value = value;
}
//这个方法是为了LinkedHashMap服务的
afterNodeAccess(e);
return oldValue;
}
}
//改变了结构,modCount加1
++modCount;
//判断size+1 是否大于临界值,大于扩容
if (++size > threshold) {
resize();
}
//这个方法是为了LinkedHashMap服务的
afterNodeInsertion(evict);
return null;
}
resize()解析
借用网上图片表述
/**
* 初始化table,或者table的大小扩大两倍。
* 如果是table空,根据tableSizeFor方法计算出threshold的值,然后赋值给capacity,然后重新计算threshold=capacit*loadFactor。
* 如果table不为空。根据2的幂来扩容,原来bin的元素要么在原来的位置或者从原来的位置移动到(原索引+oldCap)。
* 扩容以后看新增hash值的bit位是1,index=(原索引+oldCap),否则就是原来的index位置
*/
final Node[] resize() {
//当前table赋值给oldTab
Node[] oldTab = table;
//获取老的table长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//当前的threshold赋值给oldThr
int oldThr = threshold;
//定义新的容量,新的临界值
int newCap, newThr = 0;
//如果老的容量大于0
if (oldCap > 0) {
//老的容量大于最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
//临界值等于Integer.MAX_VALUE
threshold = Integer.MAX_VALUE;
return oldTab;
}
//oldCap扩大2倍赋值给newCap,比较newCap是否小于最大容量 并且oldCap大于等于默认容量
//如果满足,oldThr扩大两倍赋值给newThr
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) {
newThr = oldThr << 1;
}
}
//如果oldThr大于0,说明初始化map的传入了初始容量
else if (oldThr > 0)
//oldThr赋值给newCap。这里需要结合this.threshold = tableSizeFor(initialCapacity);这里计算出来的threshold是。最小且大于initialCapacity的2次幂
{
newCap = oldThr;
} else {
//进入此处,说明new HashMap的时候没有指定容量。
//赋值默认容量,临界值等于默认容量*默认加载因子
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果newThr等于0,说明oldThr大于0;
if (newThr == 0) {
//新的容量*加载因子得到新的临界值
float ft = (float) newCap * loadFactor;
//赋值newThr
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
(int) ft : Integer.MAX_VALUE);
}
//临界值赋值给threshold
threshold = newThr;
@SuppressWarnings({"rawtypes", "unchecked"})
//定义新的table数组,指定长度为newCap
Node[] newTab = (Node[]) new Node[newCap];
//newTab赋值给table
table = newTab;
//判断oldTab是否等于null
if (oldTab != null) {
//循环oldCap,把老的元素重新放入newCap
for (int j = 0; j < oldCap; ++j) {
//定义e元素
Node e;
//获取j位置的索引赋值给e,判断oldTab是否等于空
if ((e = oldTab[j]) != null) {
//把下标为j的元素置为空
oldTab[j] = null;
//判断e后面是否还有元素
if (e.next == null)
//把e放入newTab 新的index处
{
newTab[e.hash & (newCap - 1)] = e;
}
//判断e是否是红黑树,此处不细讲。后面会讲
else if (e instanceof TreeNode) {
((TreeNode) e).split(this, newTab, j, oldCap);
} else { // preserve order
//以下代码作用保护排序和元素移动。
//元素移动:把oldTab中的元素挂到newTab中。这里需要注意的是,元素的位置可能不变, 也有可能变为(原索引+oldCap)
//定义节点元素。lohead代表index不变的元素
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
//e的下一个元素赋值给next
next = e.next;
//判断bin中元素的index是否变化,如果==0说明index没变。
if ((e.hash & oldCap) == 0) {
if (loTail == null) {
loHead = e;
} else {
loTail.next = e;
}
loTail = e;
}
//不等于0说明index=index+oldCap
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;
}
//将节点元素放入j+oldCap
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
get()解析
/**
* 传入key
*/
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* @param key the key
* @return the node, or null if none
*/
final Node getNode(int hash, Object key) {
//定义Node数组;定义一个节点first,e;定义n,定义一个K类型的k
Node[] tab;
Node first, e;
int n;
K k;
//判断,如果table不等于null,table的长度大于0,根据计算出来的index获取table元素不等于null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//这里总是检查第一个元素,我也不知道为啥这么检查。有知道的朋友可以分享一下
//判断第一个元素的hash和传入key的hash是否相等并且判断传入的key等于first的key
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//返回第一个节点元素
{
return first;
}
//判断first节点下是否还挂着其它节点
if ((e = first.next) != null) {
//判断是否树化
if (first instanceof TreeNode)
//从红黑树中查找
{
return ((TreeNode) first).getTreeNode(hash, key);
}
do {
//循环查找,没有找到返回null
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
return e;
}
} while ((e = e.next) != null);
}
}
return null;
}
putAll()解析
/**
* 拷贝传入的元素到map
*/
public void putAll(Map extends K, ? extends V> m) {
putMapEntries(m, true);
}
/**
* @param m the map
* @param evict 是false代表是构造方法的时候调用这个,如果是true代表是其它方法
*/
final void putMapEntries(Map extends K, ? extends V> m, boolean evict) {
//获取map的大小
int s = m.size();
//如果size大于0
if (s > 0) {
//判断table是否为null
if (table == null) { // pre-size
//使用s/loadFactor+1.0 其实是为了获得table的容量
float ft = ((float) s / loadFactor) + 1.0F;
//比较容量是否超过最大值
int t = ((ft < (float) MAXIMUM_CAPACITY) ?
(int) ft : MAXIMUM_CAPACITY);
//扩大临界值
if (t > threshold) {
threshold = tableSizeFor(t);
}
}
//扩容判断
else if (s > threshold) {
resize();
}
//循环添加
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);
}
}
}
remove()解析
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
/**
* @param hash hash以后的值
* @param key the key
* @param value 如果matchValue是true,才使用value,其它忽略
* @param matchValue 如果true必须value,key相等才能删除 only remove if value is equal
* @param movable 如果false删除的时候不移动节点
* @return the node, or null if none
*/
final Node removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//定义一个tab,定义一个Node
Node[] tab;
Node p;
int n, index;
//table不等于null并且table长度大于0,根据index获取table元素不等于null
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//定义变量
Node node = null, e;
K k;
V v;
//判断第一个节点是否key是否相等,如果相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//赋值node
{
node = p;
}
//判断下一个节点元素是否
else if ((e = p.next) != null) {
//判断p是否是红黑树
if (p instanceof TreeNode) {
node = ((TreeNode) p).getTreeNode(hash, key);
} else {
//循环bin判断key是否存在
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//node不等于null,说明根据这个key找到了这个元素。判断value是否相等
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//判断是否是红黑树
if (node instanceof TreeNode) {
((TreeNode) node).removeTreeNode(this, tab, movable);
}
//判断node是否是第一个元素。如果是获取node的下一个元素赋值给tab[index]
else if (node == p) {
tab[index] = node.next;
} else
//不是第一个元素。从p元素口面删除node元素
{
p.next = node.next;
}
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
keySet()和Values()
// 返回此映射中包含的键的map视图
// 解惑:最开始在看这段代码的时候并没有看懂。
// 1:此处直接返回了一个包含key的Set集合,但是又没有对map的key进行处理,所以一直在纠结这里是怎么处理的。最后在同事的指点下,幡然大悟。
// 2:这里只是返回了一个包含map的视图,对这个集合进行遍历的时候会调用iterator方法。iteraotr方法会调用new KeyIterator();KeyIterator这个类继承了hashIterator。
//3:遍历set集合的时候会调用next方法。next方法会调用HashIterator的NextNode方法。下面对nextNode方法做了介绍
public Set keySet() {
Set ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
final class KeySet extends AbstractSet {
public final int size() {
return size;
}
public final void clear() {
HashMap.this.clear();
}
public final Iterator iterator() {
return new KeyIterator();
}
public final boolean contains(Object o) {
return containsKey(o);
}
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer super K> action) {
Node[] tab;
if (action == null) {
throw new NullPointerException();
}
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node e = tab[i]; e != null; e = e.next) {
action.accept(e.key);
}
}
if (modCount != mc) {
throw new ConcurrentModificationException();
}
}
}
}
final class KeyIterator extends HashIterator
implements Iterator {
public final K next() {
return nextNode().key;
}
}
abstract class HashIterator {
Node next; // next entry to return
Node current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
//调用iterator的方法的时候会初始化HashIterator
HashIterator() {
//把map中的modCount赋值给expectedModCount
expectedModCount = modCount;
//table赋值给t
Node[] t = table;
current = next = null;
index = 0;
//找到一个bin元素不为空就返回
if (t != null && size > 0) { // advance to first entry
do {
} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
//nextNode方法 此处就是循环查找next元素
final Node nextNode() {
//定义t
Node[] t;
//把next元素赋值给e
Node e = next;
//判断线性结构的修改
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
if (e == null) {
throw new NoSuchElementException();
}
//如果bin上还有节点直接返回下一个节点。如果没有循环则循环table查找下一个index的node元素
if ((next = (current = e).next) == null && (t = table) != null) {
do {
} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
Node p = current;
if (p == null) {
throw new IllegalStateException();
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
HashMap面试总结
1:HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,使用ConcurrentHashMap。
2:hashMap采用数组加链表的形式。如果链表的长度大于8并且capacity大于MIN_TREEIFY_CAPACITY=64才会变成红黑树。
3:hashMap默认的容量是16,默认容量并不是第一次new的时候容量就是16,而是put的时候resize()的扩容的。
4:hashMap默认的加载因子loadFactor=0.75,这个数值是对空间和时间效率的一个平衡选择。如果内存空间很多而又对时间效率要求很高,可以降低负载因子Loadfactor的值;如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。
5:hash()算法原理跟获取元素index有关。(n-1) & hash
put()方法简介:
1:根据key获取hash值,hash算法大致是key的hashCode的高16位和低16位异或。得到key的hash&(table.length-1)得到index
2:判断tab元素是否为null,如果null进行扩容操作。扩容完成以后根据(n-1)&hash(key)获取到元素下标。
3:根据index获取tab中的元素e,如果获取到e为空,说明该index没元素,直接赋值即可。如果获取到的e不为空,判断e.key和key是否相等,如果相等直接覆盖原来的值即可。如果不相等,判断e是否是TreeNode,如果是,根据e从红黑树查询元素。如果不是TreeNode,循环index下标的元素,判断是否有元素相等,如果有key相等直接返回。没有元素相等链表的尾节点插入元素即可,此处还会判断节点元素是否大于TREEIFY_THRESHOLD,如果大于TREEIFY_THRESHOLD转为红黑树。转为红黑树需要链表长度大于等于8,容量大于等于64
4:如果key已经存在,把value直接覆盖,不需要修改modCount直接返回即可。
5:如果不存在,需要modCount++;判断size++>threshold,大于扩容即可。
get()方法简介:
1:根据key获取hash。
2:根据(n-1)&hash获取index
3:判断index的第一个节点元素e是否等于key
4:判断e.next是否等于空,如果不等于空,循环bin查找元素
resize()方法简介:
1:判断oldTab.length>0
2:oldTab.length>0,判断oldTab是否大于最大容量MAXIMUM_CAPACITY,如果大于则threshold=Integer.MAX_VALUE。不大于就把容量扩大2倍并且比较是否大于MAXIMUM_CAPACITY,不大于则临界值扩大2倍。
3:如果oldTab.length<=0,临界值大于0,说明这是第一次初始化map并且初始化map的时候指定了初始容量。这个时候会调用tableSizeFor()计算临界值。扩容的时候会把newCap=threshold。重新计算临界值=新的容量*加载因子
4:如果oldTab.length<=0,临界值不大于0。容量等于默认值,临界值=默认容量*加载因子
5:循环之前的数组,判断同一个index的bin上是否有多个元素,如果只有一个元素,根据这个元素重新计算index,赋值即可。如果存在多个元素,判断元素是否是红黑树,如果是进行红黑树扩容。如果不是则进行元素移动,元素可能在原来的index。也有可能变为oldCap+index。此处移动元素的原理,就是循环bin上的元素。
9:HashMap中为什么table,entrySet要使用transient是来修饰。(transient修饰表示不可被序列化)
stackoverflow 查了一下,大概有两个原因。
1:transient 是表明该数据不参与序列化。因为 HashMap 中的存储数据的数组数据成员中,数组还有很多的空间没有被使用,没有被使用到的空间被序列化没有意义。所以需要手动使用 writeObject() 方法,只序列化实际存储元素的数组。
2:由于不同的虚拟机对于相同 hashCode 产生的 Code 值可能是不一样的,如果你使用默认的序列化,那么反序列化后,元素的位置和之前的是保持一致的,可是由于 hashCode 的值不一样了,那么定位函数 indexOf()返回的元素下标就会不同,这样不是我们所想要的结果.