1 说在前面
HashMap,所有JAVA开发者都应该熟知的一个结构,不仅仅因为它在项目开发时频繁被用到;而且HashMap在面试过程中几乎是必问的。
本期希望把我对于HashMap的认识分享给大家,从HashMap的基础使用过渡到部分重要源码的理解,文末也会贴出一些HashMap的经典面试题。我是1z,那么开始吧。
2 HashMap的基础使用
2.1基础知识
- HashMap是基于Map接口的一种键值对的存储结构,可以将数据以key-value的形式存储起来,键值对均都允许为null且在HashMap中是无序的且非同步的(线程不安全)
- HashMap在JDK1.7时使用数组+ 链表的形式实现。到了JDK1.8升级成为数组 + 链表 + 红黑树的形式实现
上图是HashMap 基于JDK1.7的数据结构
上图是HashMap 基于JDK1.8的数据结构
2.2 简单使用
package com.xq;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Description:HashMap的简单使用
* date: 2020/12/5 14:08
*
* @author lizhe
*/
public class HashMapDemo {
public static void main(String[] args) {
//1、构建一个关于名字 和 年龄的映射关系
Map map = new HashMap();
//2、存储数据
map.put("1z", 22);
map.put("hh", 21);
map.put(null, null);
//3、进行数据的取出
//遍历方法1(通过获取出的key进行得到map中的数据)
for (String s : map.keySet()) {
System.out.print(" key: " + s);
//根据key 获取value
System.out.print(" value: " + map.get(s));
System.out.println();
}
System.out.println("----------------------------------------");
//遍历方法2(直接获取entry实体)
for (Map.Entry entry : map.entrySet()) {
System.out.print(" key: " + entry.getKey());
System.out.print(" value: " + entry.getValue());
System.out.println();
}
System.out.println("----------------------------------------");
//遍历方法3(迭代器 本质上还是获取entry实体)
Iterator> it = map.entrySet().iterator();while (it.hasNext()) {
Map.Entry entry = it.next();
System.out.print(" key: " + entry.getKey());
System.out.print(" value: " + entry.getValue());
System.out.println();
}
}
}
package com.xq;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Description:HashMap的简单使用
* date: 2020/12/5 14:08
*
* @author lizhe
*/
public class HashMapDemo {
public static void main(String[] args) {
//1、构建一个关于名字 和 年龄的映射关系
Map map = new HashMap();
//2、存储数据
map.put("1z", 22);
map.put("hh", 21);
map.put(null, null);
//3、进行数据的取出
//遍历方法1(通过获取出的key进行得到map中的数据)
for (String s : map.keySet()) {
System.out.print(" key: " + s);
//根据key 获取value
System.out.print(" value: " + map.get(s));
System.out.println();
}
System.out.println("----------------------------------------");
//遍历方法2(直接获取entry实体)
for (Map.Entry entry : map.entrySet()) {
System.out.print(" key: " + entry.getKey());
System.out.print(" value: " + entry.getValue());
System.out.println();
}
System.out.println("----------------------------------------");
//遍历方法3(迭代器 本质上还是获取entry实体)
Iterator> it = map.entrySet().iterator();while (it.hasNext()) {
Map.Entry entry = it.next();
System.out.print(" key: " + entry.getKey());
System.out.print(" value: " + entry.getValue());
System.out.println();
}
}
}
TIPS: 从上述的代码中可以验证出HashMap的几个重要特征
- 存储的数据是无序的(没有按照放入的顺序进行读出)
- 放入的数据以key value的形式进行存储
- 允许放入key为null的键值对
- 这里要注意一个细节问题,如何去判断HashMap中是否包含key和value均都为null的键值对?
- 假设通过get()方法判断,则无论是否存在值为null,get的返回值均都为null(假设不存在该键值对,则get(null)的返回值为null表示不存在;假设存在该键值对,get方法应该返回该value的值,也为null),于是就产生了误解。
- 因此需要通过map.containsKey()方法来进行判断是否以存在当前key存储的键值对
3 关于HashMap源码的一些思考
3.1 HashMap的基础组成
以下内容以JDK1.8为例
根据上图HashMap的图可以知道,HashMap由一个个"节点"组成,这一个个Node节点就是HashMap最基础的"零件"。我们设置的key和value就存储在Node节点中,下面截取部分代码进行分析。
/*
*/
static class Node implements Map.Entry {
//当前数据的唯一标识
final int hash;
//存入其中的key值
final K key;
//存入其中的value值
V value;
//指向下一个节点的指针
Node next;
//当前节点的构造函数
Node(int hash, K key, V value, Node next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//获取key
public final K getKey() { return key; }
//获取value
public final V getValue() { return value; }
//toString
public final String toString() { return key + "=" + value; }
//定位hash值的算法
public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//获取value
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;return oldValue;
}
//判断是否相等
public final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {
Map.Entry,?> e = (Map.Entry,?>)o;if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))return true;
}return false;
}
}
/*
*/
static class Node implements Map.Entry {
//当前数据的唯一标识
final int hash;
//存入其中的key值
final K key;
//存入其中的value值
V value;
//指向下一个节点的指针
Node next;
//当前节点的构造函数
Node(int hash, K key, V value, Node next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//获取key
public final K getKey() { return key; }
//获取value
public final V getValue() { return value; }
//toString
public final String toString() { return key + "=" + value; }
//定位hash值的算法
public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//获取value
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;return oldValue;
}
//判断是否相等
public final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {
Map.Entry,?> e = (Map.Entry,?>)o;if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))return true;
}return false;
}
}
注: 如果学习数据结构的小伙伴们应该很容易就看出,上述的代码就是基于Node的链表实现,这也就是HashMap的基础结构。
在一个个Node中存放着我们填入的Key和Value,然后将基于Node的链表挂载在数组上就组成了一个简单的HashMap结构。
(ps:由此可见,自己手写一个简单的散列表并不是什么很难的事情,大家可以参考韩顺平数据结构和算法散列表那一部分的内容自己手写一遍,但是HashMap中存在的细节和设计者的对于一些细节的考量确是我们需要学习的)
当然HashMap不只是这么简单,基于JDK1.8满足一定的条件会有树化的过程,当前使用的节点也不是Node,而是基于红黑树的TreeNode。由于今天篇幅有限,咱们日后再看看树化(treeify)的具体流程和红黑树的那些事儿(先贴出一些基于红黑树的Node代码 康康)
static final class TreeNode extends LinkedHashMap.Entry {
TreeNode parent; // red-black tree links
TreeNode left;
TreeNode right;
TreeNode prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode root() {for (TreeNode r = this, p;;) {if ((p = r.parent) == null)return r;
r = p;
}
}
/**
* Ensures that the given root is the first node of its bin.
*/
static void moveRootToFront(Node[] tab, TreeNode root) {
int n;if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode first = (TreeNode)tab[index];if (root != first) {
Node rn;
tab[index] = root;
TreeNode rp = root.prev;if ((rn = root.next) != null)
((TreeNode)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 final class TreeNode extends LinkedHashMap.Entry {
TreeNode parent; // red-black tree links
TreeNode left;
TreeNode right;
TreeNode prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode root() {for (TreeNode r = this, p;;) {if ((p = r.parent) == null)return r;
r = p;
}
}
/**
* Ensures that the given root is the first node of its bin.
*/
static void moveRootToFront(Node[] tab, TreeNode root) {
int n;if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode first = (TreeNode)tab[index];if (root != first) {
Node rn;
tab[index] = root;
TreeNode rp = root.prev;if ((rn = root.next) != null)
((TreeNode)rn).prev = rp;if (rp != null)
rp.next = rn;if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
assert checkInvariants(root);
}
}
3.2 HashMap中的一些参数设计和说明
//初始大小,默认为2的整数次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 <
//哈希表的最大长度
static final int MAXIMUM_CAPACITY = 1 <
//默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//单条链表的树化阈值
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
//单条红黑树退化为链表的阈值
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
//最小的树化阈值(整体)
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
//初始大小,默认为2的整数次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 <
//哈希表的最大长度
static final int MAXIMUM_CAPACITY = 1 <
//默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//单条链表的树化阈值
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
//单条红黑树退化为链表的阈值
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
//最小的树化阈值(整体)
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
3.3 HashMap中的基于数据操作的源码
放入数据逻辑(putVal)
//HashMap中的PutVal源码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node[] tab; HashMap.Node p; int n, i;
//1.如果哈希表为null,或者长度为0 则进行resize() 扩容操作【这里也说明resize()有初始化的作用】if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//2.找到合适的桶 ,这里的(n - 1)&hash 是一种高效的取余操作if ((p = tab[i = (n - 1) & hash]) == null)
//3.如果发现当前’桶‘上没有node,则newNode(在桶上挂上第一个node)【链表挂上第一个节点】
tab[i] = newNode(hash, key, value, null);else {
HashMap.Node e; K k;
//4.如果发现头节点匹配,当前bucket上存在节点,则直接进行赋值替换即可【此时为链表状态】if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//5.如果发现当前的p属于TreeNode,说明当前bucket上挂载的是树结构,则进行红黑树的插入操作else if (p instanceof HashMap.TreeNode)
e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);else {
//6.如果上述的操作都不满足,对链表进行遍历操作for (int binCount = 0; ; ++binCount) {
//如果当前链表hash值不符合条件,则继续newNode(尾部追加)if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//当发现binCount:链表长度 >=树化阈值的时候,treeifyBin 树化if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);break;
}
//如果发现当前链表的某个节点hash值相同,不再进行链表遍历,将当前的值保存起来【即找到了匹配节点e 将e放置到已经准备好的容器p中】if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))break;
p = e;
}
}
//此时已经找到了value该存放的位置,放入值即可if (e != null) { // existing mapping for key
V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);return oldValue;
}
}
//操作结束之后,如果发现size的值超过阈值,进行扩容操作 <<1(*2)
++modCount;if (++size > threshold)
resize();
afterNodeInsertion(evict);return null;
}
//HashMap中的PutVal源码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node[] tab; HashMap.Node p; int n, i;
//1.如果哈希表为null,或者长度为0 则进行resize() 扩容操作【这里也说明resize()有初始化的作用】if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//2.找到合适的桶 ,这里的(n - 1)&hash 是一种高效的取余操作if ((p = tab[i = (n - 1) & hash]) == null)
//3.如果发现当前’桶‘上没有node,则newNode(在桶上挂上第一个node)【链表挂上第一个节点】
tab[i] = newNode(hash, key, value, null);else {
HashMap.Node e; K k;
//4.如果发现头节点匹配,当前bucket上存在节点,则直接进行赋值替换即可【此时为链表状态】if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//5.如果发现当前的p属于TreeNode,说明当前bucket上挂载的是树结构,则进行红黑树的插入操作else if (p instanceof HashMap.TreeNode)
e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);else {
//6.如果上述的操作都不满足,对链表进行遍历操作for (int binCount = 0; ; ++binCount) {
//如果当前链表hash值不符合条件,则继续newNode(尾部追加)if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//当发现binCount:链表长度 >=树化阈值的时候,treeifyBin 树化if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);break;
}
//如果发现当前链表的某个节点hash值相同,不再进行链表遍历,将当前的值保存起来【即找到了匹配节点e 将e放置到已经准备好的容器p中】if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))break;
p = e;
}
}
//此时已经找到了value该存放的位置,放入值即可if (e != null) { // existing mapping for key
V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);return oldValue;
}
}
//操作结束之后,如果发现size的值超过阈值,进行扩容操作 <<1(*2)
++modCount;if (++size > threshold)
resize();
afterNodeInsertion(evict);return null;
}
取出数据逻辑(get)
//HashMap中的get()操作
public V get(Object key) {
Node e;return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
先检测是否是first节点[通过高效化的取余操作找到所属桶的首位]
然后采用do while循环继续向后进行寻找目标值
(e.hash == hash && e.key == key || (key != null && key.equals(k)))
tip1: 如果equals true 说明hash一定相等,但是hash相同不一定equals为true tip2:如果hash不相等,则equals一定不等 */
final Node getNode(int hash, Object key) {
Node[] tab; Node 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)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;
}
//HashMap中的get()操作
public V get(Object key) {
Node e;return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
先检测是否是first节点[通过高效化的取余操作找到所属桶的首位]
然后采用do while循环继续向后进行寻找目标值
(e.hash == hash && e.key == key || (key != null && key.equals(k)))
tip1: 如果equals true 说明hash一定相等,但是hash相同不一定equals为true
tip2:如果hash不相等,则equals一定不等
*/
final Node getNode(int hash, Object key) {
Node[] tab; Node 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)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;
}
树化逻辑(treeifyBin)
//HashMap中的树化操作
//1. 单条链表节点个数 > 8
//2. 总数 > 64
//3. 当单条链表的节点个数小于6的时候,会进行重新链表化
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
//MIN_TREEIFY_CAPACITY == 64 [代表了如果阈值 if (tab == null || (n = tab.length) resize();else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode hd = null, tl = null;do {
TreeNode 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);
}
}
//HashMap中的树化操作
//1. 单条链表节点个数 > 8
//2. 总数 > 64
//3. 当单条链表的节点个数小于6的时候,会进行重新链表化
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
//MIN_TREEIFY_CAPACITY == 64 [代表了如果阈值 if (tab == null || (n = tab.length) resize();else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode hd = null, tl = null;do {
TreeNode 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);
}
}
动态扩容逻辑(resize)
//HashMap中的resize操作
/**
1.什么时候进行resize()??
1.当hashmap为null,或者长度为0的时候
2.当Map中存储的k-v值超过了阈值 16 * 0.75(默认: 容量 * 负载因子)
3.链表的长度 > 8[TREEIFY_THRESHOLD] 但 表的长度 */
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;if (oldCap > 0) {
//如果原始的容量 >= 最大容量 即2^30if (oldCap >= MAXIMUM_CAPACITY) {
//将阈值设置为Integer.MAX_VALUE.无需进行扩容了
threshold = Integer.MAX_VALUE;return oldTab;
}
//或者,新容量 = oldGap * 2 范围在 [16,2^30]
//此时新的阈值 <<1【*2】else if ((newCap = oldCap < oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr < }
//如果oldThr > 0 else if (oldThr > 0) // initial capacity was placed in threshold
//新的容量值 = 原始阈值
newCap = oldThr;else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
//默认阈值为 默认的负载因子 * 默认的容量(16 * 0.75)
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}if (newThr == 0) {
//否则 !!!新的阈值 == 新的容量 * 负载因子float ft = (float)newCap * loadFactor;
newThr = (newCap float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
//前方的代码进行了长度的扩展if (oldTab != null) {
//循环遍历hashtable中不为空的bucketfor (int j = 0; j Node 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)e).split(this, newTab, j, oldCap);else { // preserve order
//lo : 原bucket上存储
//hi : 新bucket上存储
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;do {
next = e.next;
/**
??? 这段位运算比较难理解
迁移的数据为[oldCap,2*oldCap-1]
以及在此基础上 + 2 * n * oldCap的数据
*/if ((e.hash & oldCap) == 0) {
//尾插法
//无需和java1.7一样有头插且倒转链表的操作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;
//新的下标为原下标 向前挪动 oldCap
newTab[j + oldCap] = hiHead;
}
}
}
}
}return newTab;
}
//HashMap中的resize操作
/**
1.什么时候进行resize()??
1.当hashmap为null,或者长度为0的时候
2.当Map中存储的k-v值超过了阈值 16 * 0.75(默认: 容量 * 负载因子)
3.链表的长度 > 8[TREEIFY_THRESHOLD] 但 表的长度 */
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;if (oldCap > 0) {
//如果原始的容量 >= 最大容量 即2^30if (oldCap >= MAXIMUM_CAPACITY) {
//将阈值设置为Integer.MAX_VALUE.无需进行扩容了
threshold = Integer.MAX_VALUE;return oldTab;
}
//或者,新容量 = oldGap * 2 范围在 [16,2^30]
//此时新的阈值 <<1【*2】else if ((newCap = oldCap < oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr < }
//如果oldThr > 0 else if (oldThr > 0) // initial capacity was placed in threshold
//新的容量值 = 原始阈值
newCap = oldThr;else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
//默认阈值为 默认的负载因子 * 默认的容量(16 * 0.75)
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}if (newThr == 0) {
//否则 !!!新的阈值 == 新的容量 * 负载因子float ft = (float)newCap * loadFactor;
newThr = (newCap float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
//前方的代码进行了长度的扩展if (oldTab != null) {
//循环遍历hashtable中不为空的bucketfor (int j = 0; j Node 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)e).split(this, newTab, j, oldCap);else { // preserve order
//lo : 原bucket上存储
//hi : 新bucket上存储
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;do {
next = e.next;
/**
??? 这段位运算比较难理解
迁移的数据为[oldCap,2*oldCap-1]
以及在此基础上 + 2 * n * oldCap的数据
*/if ((e.hash & oldCap) == 0) {
//尾插法
//无需和java1.7一样有头插且倒转链表的操作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;
//新的下标为原下标 向前挪动 oldCap
newTab[j + oldCap] = hiHead;
}
}
}
}
}return newTab;
}
TIP: 以上就是关于HashMap一些源码级的梳理,此次只是挑了几个重点来进行讲述,HashMap中还有很多很多值得细细品味的点,希望大家可以在网上自行搜索,收获更多喔。
4. 关于HashMap的一些面试题
谈一谈HashMap的特性
- HashMap存储键值对实现快速存取,key值不可重复,但是允许为null【放在table[0]的位置】,如果key值重复则覆盖
- HashMap线程不安全,非同步
- 底层是hash表,不能保证有序
HashMap的一些定位算法
- hashcode的重载 public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); }
- hash函数(为什么需要使用hash函数 : 单纯使用hashCode的值大多数情况会小于2的16次方,所以会浪费hashcode的高16位,如果参与进来使得结果更加散列,同时增加了随机性,减少了哈希碰撞的次数)
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } - 定位函数 index = hash & (cap - 1)
- 将数组长度控制为2的幂次? (为什么要控制为2的幂次?)
- 如果控制为2的幂次,则保证了cap - 1二进制位为11111...1 hash & 当前值的结果 和 hash % cap一致
- 使用连续的 1.....1,保证了不同的key算出相等结果的概率较小,分布均匀,减少了hash碰撞 )
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n = MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n = MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
如果两个键的hashcode相同,如何获取值对象?
HashCode相同,说明会出现哈希碰撞,如果当前key值相同则替换旧值(调用equals判断),否则链接到链表后面,链表长度超过8的时候转为红黑树处理
HashMap的负载因子有什么用? 为什么默认0.75?
loadFactor代表HashMap的拥挤程度,影响hash操作到同一个位置的概率,默认HashMap的负载因子为0.75。
当谈到为什么0.75的时候:
原因是如果设置成1,这样会发生大量的hash碰撞。有些位置的链表会很长,不利于查询。 省空间而费时间。
如果设置成0.5,hash碰撞的几率小了很多,但是会频繁扩容,费空间而省时间。
经过研究,0.75的数值比较均衡,在空间和时间做了个取舍。