本文涵盖内容
- HashMap 方法源码解析(构造方法、增删改查)
- HashMap特点、适用场景
- 常见面试题
特点,适用场景
- 线程不安全的
- 存储结构是 数组 + 链表 + 红黑树(jdk1.8)
- 允许key、value为null
- 无序
源码解析
1. 构造方法
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) {
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
}
if (initialCapacity > MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY;
}
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
}
this.loadFactor = loadFactor;
// 初始化阀值 ,添加的Entry超过该阀值 ,就进行扩容操作
this.threshold = tableSizeFor(initialCapacity);
}
2.1 增put(K key, V value)
public V put(K key, V value) {
// hash(key) 详见2.2
return this.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;
int i;
if ((tab = this.table) == null || (n = tab.length) == 0) {
// 如果第一次进行put,进行扩容获取初始化table操作,详见2.3
n = (tab = this.resize()).length;
}
if ((p = tab[i = (n - 1) & hash]) == null) {
// 如果该下标下没有数据择直接进行存储
tab[i] = this.newNode(hash, key, value, null);
} else {
Node<K, V> e;
K k;
// 如果i下标的Entry和要存储的entry相同就覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) {
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) {
// 把node 插入到链表尾部
p.next = this.newNode(hash, key, value, null);
// 如果链表节点数量>8个就改用红黑树结构
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
{
this.treeifyBin(tab, hash);
}
break;
}
// 如果在链表中找到相同的Entry就覆盖
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
break;
}
p = e;
}
}
// 执行覆盖操作 , 讲新的value赋值给同key的Entry
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) {
e.value = value;
}
this.afterNodeAccess(e);
return oldValue;
}
}
++this.modCount;
// 如果数量超过了阀值,就扩容
if (++this.size > this.threshold) {
this.resize();
}
this.afterNodeInsertion(evict);
return null;
}
2.2 hash(key)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2.3 resize()
final Node<K, V>[] resize() {
Node<K, V>[] oldTab = this.table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = this.threshold;
int newCap;
int newThr = 0;
// if 当前数组长度>0进行扩容 else 使用默认容量值 初始化新数组
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
this.threshold = Integer.MAX_VALUE;
return oldTab;
// oldcap * 2 扩容
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) {
newThr = oldThr << 1; // double threshold
}
} else if (oldThr > 0) // initial capacity was placed in threshold
{
newCap = oldThr;
} else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float) newCap * this.loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
(int) ft : Integer.MAX_VALUE);
}
this.threshold = newThr;
// 初始化长度为newcap的新数组
@SuppressWarnings({"rawtypes", "unchecked"}) Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
this.table = newTab;
// if 初始化不进入代码段 else 进入代码段进行扩容操作
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 如果该下标只有一个Entry设置该值即可
if (e.next == null) {
newTab[e.hash & (newCap - 1)] = e;
} else if (e instanceof TreeNode) {
// 如果链表长度超过8个会是一个红黑树node
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
} else { // preserve order
// 链表数据从当前数组转移到新数组
Node<K, V> loHead = null, loTail = null;
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
do {
next = e.next;
// 根据e.hash & oldCap) == 0 为标志把数组桶中的链表分成两个链表
// ==0 存储在原有下标newtable[j]下 !=0 存储在newtable[j+oldtable.length]下
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;
}
3. 删除 remove(Object key)
public V remove(Object key) {
Node<K, V> e;
return (e = this.removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K, V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K, V>[] tab;
Node<K, V> p;
int n;
int index;
// 只有当前数组!=null && table.length>0 && p = 数组index下标中第一个Entry != null 才能进行删除操作
if ((tab = this.table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K, V> node = null, e;
K k;
V v;
// 数组下标对应数据为删除数据
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) {
node = p;
} else if ((e = p.next) != null) {
// 如果存在链表 , 检测next是否是红黑树node 赋值
if (p instanceof TreeNode) {
node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
} else {
// 如果是普通单向链表 ,遍历链表找到该node 赋值
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 删除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode) {
// 红黑树删除
((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
} else if (node == p) {
// 链表头部删除
tab[index] = node.next;
} else {
// 链表删除
p.next = node.next;
}
++this.modCount;
--this.size;
this.afterNodeRemoval(node);
return node;
}
}
return null;
}
4.1 查get(Object key)
public V get(Object key) {
Node<K, V> e;
return (e = this.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;
Node<K, V> e;
int n;
K k;
if ((tab = this.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) {
// 如果是红黑树node 进行红黑树获取过程
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.2 4种遍历查询方式
Iterator i = map.entrySet().iterator();
while (i.hasNext()) {
Entry<String, String> e = (Entry<String, String>) i.next();
System.out.println(e.getKey() + "\t" + e.getValue());
}
Set<String> set = map.keySet();
for (String key : set) {
System.out.println(key + "\t" + map.get(key));
}
for (Map.Entry<String, String> e : map.entrySet()) {
System.out.println(e.getKey() + "\t" + e.getValue());
}
for (String v : map.values()) {
System.out.println("value= " + v);
}
与HashTable区别
- HashTable 线程安全,put get使用synchronized来进行同步
- 默认长度是11
- 数组扩容是原有数组长度 * 2 +1 ,扩容后遍历数组重新hash并计算index重新存储
- 不允许value为null
与ConcurrentHashMap区别
- ConcurrentHashMap线程安全 , key和value都不允许为null
常见面试题
1. HashMap put流程解析
- 求key的hash值
- 如果table == null 或者table.length == 0 说明还没有初始化table , 进行table初始化操作
- 根据table.length-1 & hash 得到数组下标index
- 如果数组下标index中数据==null table[index] = node
- 如果和下标inde中链表/红黑树中第一个值相同覆盖
- 如果node 是TreeNode(红黑树类型)就让红黑树管理
- 如果是链表 , 就循环把该node插入到表位 , 在循环过程中如果存在相同的node就覆盖,根据循环的count , 如果大于8个就把链表转化成红黑树来处理
- size++ 如果size>threshold 进行扩容
2. 扩容为2的幂数的原因 :
-
如果length不为2的幂,比如15。那么length-1的2进制就会变成1110。在h为随机数的情况下,和1110做&操作。尾数永远为0。那么0001、1001、1101等尾数为1的位置就永远不可能被entry占用。这样会造成浪费,不随机等问题。 length-1 二进制中为1的位数越多,那么分布就平均。
-
减少哈希碰撞,避免生成链表 , 降低查询效率
3. 在Jdk1.8 之后HashMap做了什么优化
- jdk1.8之后 , 当链表长度达到8个之后转成红黑树存储,以提升它的查询、插入效率
- 在数组扩容后数据转移优化:在1.8之前是需要遍历每个链表中的数据重新通过hash值&新数组长度取出下标 , 在1.8之后 会把原先数组下标中的链表根据hash & oldtable.length == 0 分成两个链表 , ==0的链表还是存在原先下标j下 , !=0的讲存储在newtable[j+oldtable]
4. hash方法中为什么 (h = key.hashCode()) ^ (h >>> 16) 这样设计
由于hash和(length-1)进行&运算,length绝大多数情况小于2的16次方。所以始终是hashcode 的低16位参与运算。
要是高16位也参与运算,会让得到的下标更加散列。
所以这样高16位是用不到的,如何让高16也参与运算呢。所以才有hash(Object key)方法。让他的hashCode()和自己的高16位^运算。所以(h >>> 16)得到他的高16位与hashCode()进行^运算。
重点来了,为什么用^:因为&和|都会使得结果偏向0或者1 ,并不是均匀的概念,所以用^。
这就是为什么有hash(Object key)的原因。
5. HashMap 是怎么扩容的
在size > threshold的时候进行resize扩容操作 。
源码注释.