以下内容基于JDK1.8
以下内容将分为如下几个章节
- 概述
- 继承关系图
- 相关成员变量
- 内部静态类
- 构造函数
- 常用方法
- 优缺点
概述
- 实现了所有
Map
的相关操作 - 允许key为空或value为空
- 除了允许空值和非同步外,其他与
HashTable
类似
继承关系图
相关静态成员变量
/**
* 默认存储容量--16=2^4
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大存储容量--1073741824=2^30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认负载因子,即默认情况下当键值对数量大于时DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR=12时,会发生扩容
*/
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;
相关成员变量
/**
* 节点表,首次使用时初始化,并根据需要调整大小
* 分配时,长度总是2的幂
*/
transient Node<K,V>[] table;
/**
* 保留缓存entrySet(),用于抽象类的keySet()和values()方法
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 键值对个数
*/
transient int size;
/**
* 修改次数
*/
transient int modCount;
/**
* 下次扩容是size的阈值=容量*负载因子
* The next size value at which to resize (capacity * load factor).
*/
int threshold;
/**
* 负载因子
*/
final float loadFactor;
内部静态类Node<K,V>
static class Node<K,V> implements Map.Entry<K,V> {
//存储key对应hash值
final int hash;
//key
final K key;
//value
V value;
//指向下一个节点
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(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;
}
}
构造方法
- 无参构造函数
public HashMap() {
//设置负载因子为默认值
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
- 传入参数为容量构造函数
public HashMap(int initialCapacity) {
//源码见3
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
- 传入参数为容量和负载因子的构造函数
public HashMap(int initialCapacity, float loadFactor) {
//判断传入容量是否小于0
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;
//设置扩容阈值
this.threshold = tableSizeFor(initialCapacity);
}
- 3.2 返回大于且最接近cap的2的n次幂
tableSizeFor
方法 - 例如,当cap=3时,则返回4
- 当cap=12时,则返回16
- 当cap=16时,则返回16
即返回值>=传入值
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 < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
- 传入参数为Map实现类构造函数
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
- 4.1
putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
方法
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//获取元素个数
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
//计算其容量=元素个数/负载因子+1
float ft = ((float)s / loadFactor) + 1.0F;
//判断其容量是否大于最大容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//判断容量是否大于0
if (t > threshold){
//计算其下次扩容阈值
threshold = tableSizeFor(t);
}
}else if (s > threshold){
//扩容方法
resize();
}
//迭代map子类
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
//获取key
K key = e.getKey();
//获取value
V value = e.getValue();
//将键值对放入到散列桶中,hash(key)为计算key的hash值
putVal(hash(key), key, value, false, evict);
}
}
}
其中,resize
方法和putVal
是HashMap比较核心的方法
由于putVal
调用了resize
方法,先来看resize
方法吧
- 4.2 扩容
resize()
方法
final Node<K,V>[] resize() {
//获取散列桶
Node<K,V>[] oldTab = table;
//获取散列桶长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获取扩容阈值
int oldThr = threshold;
//新散列桶长度,新扩容阈值
int newCap, newThr = 0;
//判断散列桶长度是否大于0
if (oldCap > 0) {
//判断散列桶长度是否超过最大容量
if (oldCap >= MAXIMUM_CAPACITY) {
//扩容阈值=整型最大值
threshold = Integer.MAX_VALUE;
//已到最大容量,不能再扩容了,直接返回原散列桶
return oldTab;
}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY){
//设置新容量=散列桶长度*2
//若新容量小于最大容量且散列桶长度>=16
//新扩容阈值=扩容阈值*2
newThr = oldThr << 1; // double threshold
}
}else if (oldThr > 0){
//若散列桶长度=0且扩容阈值>0
//新容量=扩容阈值
newCap = oldThr;
}else {
//若散列桶长度=0且扩容阈值=0
//新容量=默认初试容量,即16
newCap = DEFAULT_INITIAL_CAPACITY;
//新扩容阈值=默认初试容量*默认负载因子,及16*0.75-12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//判断新扩容阈值是否为0
if (newThr == 0) {
//初始容量=新容量*负载因子
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//设置扩容阈值=新扩容阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//创建大小为新容量的散列桶数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//迭代散列桶判断对应节点是否为空
if ((e = oldTab[j]) != null) {
//将对应元素置为空
oldTab[j] = null;
//判断节点是否有下一个节点
if (e.next == null){
//无下一个节点,则重新计算节点在新散列桶中的索引
//新索引值=节点hash值&新散列桶长度-1
//这么做的好处是啥,此处有一个前提,那就是散列桶的长度是2的n次幂
//假设新散列桶长度为16,那么newCap - 1=15
//例子1 e.hash=03,索引=03&15= 0000 0011
// & 0000 1111
// = 0000 0011 = 03
//例子2 e.hash=07,索引=07&15= 0000 0111
// & 0000 1111
// = 0000 0111 = 07
//例子3 e.hash=13,索引=13&15= 0000 1101
// & 0000 1111
// =0000 1101 = 13
//例子4 e.hash=16,索引=16&15= 0001 0000
// & 0000 1111
// = 0000 1101 = 00
//例子5 e.hash=25,索引=25&15= 0001 1001
// & 0000 1111
// = 0000 1001 = 09
//假设新散列桶长度为12,那么newCap - 1 = 11,已同样的例子来对比就是另外一种情形
//例子1 e.hash=03,索引=03&11= 0000 0011
// & 0000 1011
// = 0000 0011 = 03
//例子2 e.hash=07,索引=07&11= 0000 0111
// & 0000 1011
// = 0000 0011 = 03
//例子2 e.hash=13,索引=13&11= 0000 1101
// & 0000 1011
// = 0000 1001 = 09
//例子3 e.hash=16,索引=16&15= 0001 0000
// & 0000 1011
// = 0000 1101 = 01
//例子4 e.hash=25,索引=25&15= 0001 1001
// & 0000 1011
// = 0000 1001 = 09
//对比发现,已通过e.hash & (newCap - 1)分配散列桶索引
//只要保证节点的hash值尽可能的不重复,那么节点就可以越均匀分布在基于数组散列桶中,从而保证效率
newTab[e.hash & (newCap - 1)] = e;
}else if (e instanceof TreeNode){
//红黑树,忽略
((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) 例1
//e.hash=09 0000 1001
//oldCap=16 0001 0000
// 二者 &= 0000 0000=0
//(e.hash & oldCap) 例2
//e.hash=13 0000 1101
//oldCap=16 0001 0000
// 二者 &= 0000 0000=0
//(e.hash & oldCap) 例3
//e.hash=31 0001 1111
//oldCap=16 0001 0000
// 二者 &= 0001 0000=16
//(e.hash & oldCap) 例4
//e.hash=25 0001 0011
//oldCap=16 0001 0000
// 二者 &= 0001 0000=16
//从上述例子可得结论和代码:
//当节点的hashcode>=oldCap时,e.hash & oldCap > 0
//当节点的hashcode<oldCap时,e.hash & oldCap = 0
//loHead,loTail的含义是低位的头结点和尾结点
//hiHead,hiTail的含义是高位的头结点和尾结点
//那何为高位呢,就是如果节点对应的hash值大于等于散列桶长度
//低位则相反,其hash值小于散列桶长度
//以下仅几行代码就代码实现了如下功能
//1.重新计算节点在新散列桶中的位置
//2.保证节点在散列桶中的顺序与在扩容前一致
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);
//当loTail不为空,即节点的hashcode<oldCap时,节点在新散列桶中的位置不变
if (loTail != null) {
//尾结点已经是最后一个节点了,其下一节点需置为空
loTail.next = null;
//将低位头结点放入新散列桶索引值=[j]处
//低位节点的位置在扩容后相对于老散列桶来说位置未发生变化
newTab[j] = loHead;
}
//当hiTail不为空,即节点的hashcode>=oldCap时,节点在新散列桶中位置=原位置+原散列桶长度
if (hiTail != null) {
hiTail.next = null;
//将高位头结点放入新散列桶索引值=[j+老散列桶长度]处
//高位节点的位置在扩容后相对于老散列桶来说位置向后挪动了[老散列桶长度]个位置
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
分析完resize()
方法,可以得出如下结论:
-
设置扩容阈值的
tableSizeFor(初始容量)
方法能够保证散列桶的长度为2的n次幂,也就是说
元素能均匀分布在散列桶中,保证在进行增,删,改,查时效率最高
-
如果已知放入元素个数时, 最好是通过
2或3构造函数
得到HashMap
实例,
再进行操作,否则在且添加元素较多时,就需要频繁扩容,那就得不偿失了 -
如果未知放入元素个数时,就需要进行提前预判了,既不要太大浪费空间,也不要太小避免频繁扩容
- 4.3 将键值对放入散列桶
putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
方法
/**
* 实现了Map接口的put和related方法
* @param hash--key对应的hash值
* @param key--放入的key
* @param value--放入的key对应的value值
* @param onlyIfAbsent--若为true,则不覆盖已存在的值
* @param evict--若为false,散列桶将处于创建模式,此参数在HashMap中无意义
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//当前散列桶
Node<K,V>[] tab;
//p为散列桶中索引为i的头节点或迭代时的节点
Node<K,V> p;
//n为散列桶长度, i为键值对在散列桶中的索引值
int n, i;
//判断散列桶是否为空
if ((tab = table) == null || (n = tab.length) == 0){
//若为空,则调用扩容方法进行扩容
n = (tab = resize()).length;
}
//判断在散列桶中索引为i处是否为空
if ((p = tab[i = (n - 1) & hash]) == null){
//头结点p为空则初始化一个节点后将其放入
tab[i] = newNode(hash, key, value, null);
}else {
//头结点p不为空
//e用来存储p结点的下一节点
Node<K,V> e;
//已存在节点的键
K k;
//判断
//1.节点p的hash值是否与传入键的hash值一致
//2.(节点p的key是否与与传入key的内存地址相同)或(传入key不为空且传入键与已存在键相同)
if (p.hash == hash &&
((k = p.key) == key
|| (key != null && key.equals(k)))){
//若1,2均满足则用e先存储p
e = p;
}else if (p instanceof TreeNode){
//红黑树忽略
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
}else {
//hash值和键值既不相同,且不为红黑树
//迭代中索引为i的节点,binCount用来记录散列桶中索引为i处的元素个数
for (int binCount = 0; ; ++binCount) {
//判断p下一节点是否为空
if ((e = p.next) == null) {
//为空则说明p下面无节点,直接创建新节点并将其设置为p的下一个节点
p.next = newNode(hash, key, value, null);
//如果元素个数大于等于8,则转为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) {// -1 for 1st
treeifyBin(tab, hash);
}
break;
}
//判断
//1.节点e的hash值是否与传入键的hash值一致
//2.(节点e的key是否与与传入key的内存地址相同)或(传入key不为空且传入键与已存在键相同)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))){
break;
}
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null){
//若onlyIfAbsent为false或节点存储值不为空,则用传入值替换旧值
e.value = value;
}
//在HashMap中该方法只做了声明,未实现任何逻辑
afterNodeAccess(e);
//返回旧值
return oldValue;
}
}
++modCount;
//判断放入键值对后,是否需要进行扩容
if (++size > threshold){
//大于扩容阈值,则需进行扩容
resize();
}
//在HashMap中该方法只做了声明,未实现任何逻辑
afterNodeInsertion(evict);
return null;
}
常用方法
1 放入键值对put(K key, V value)
方法
/**
* @param key 放入键
* @param value 放入值
* @return 返回key之前存储值,若HashMap中之前未存储key,那么返回空
*/
public V put(K key, V value) {
//详见4.3
return putVal(hash(key), key, value, false, true);
}
- 通过键删除元素
remove(Object key)
方法
/**
* @param key 删除键
* @return 返回key之前存储值,若HashMap中之前未存储key,那么返回空
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
- 2.1 通过key对应的hash值,key值删除元素
removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)
方法
/*
* @param hash--key对应的hash值
* @param key--删除的key
* @param value--删除的key对应的值
* @param matchValue--若为true,则仅删除匹配的值
* @param movable--若为false,则在删除时不移动其他节点
* @return 之前存储节点,若无key对应的节点将返回空
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//用于存储散列桶
Node<K,V>[] tab;
//p为散列桶中索引为i的头节点或迭代时的节点
Node<K,V> p;
//n为散列桶长度,
//index为(n - 1) & hash
int n, index;
//判断散列桶是否为空且长度大于0
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node用于存储匹配的的节点
//e节点用于存储p节点的下一节点
Node<K,V> node = null, e; K k; V v;
//判断
//1.头节点的hash值是否与传入键的hash值一致
//2.(头节点的key是否与与传入key的内存地址相同)或(传入key不为空且传入键与已存在键相同)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))){
node = p;
}else if ((e = p.next) != null) {
//头结点的下一节点不为空
//判断是否为红黑树
if (p instanceof TreeNode){
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
}else {
//迭代e下一节点
do {
//判断
//1.e节点的hash值是否与传入键的hash值一致
//2.(e节点的key是否与与传入key的内存地址相同)或(传入key不为空且传入键与已存在键相同)
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//判断
//1.是否匹配到key对应的节点node
//2.是否删除不匹配的值或node对应的值与传入值相同
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){
//当node == p仅发生在p为头结点时
//即直接将index对应的头结点置为node的下一节点
tab[index] = node.next;
}else{
//p此时为node的上一节点,要删除node,直接将p的下一节点跳过node而指向node的下一节点
p.next = node.next;
}
++modCount;
--size;
//在HashMap中该方法只做了声明,未实现任何逻辑
afterNodeRemoval(node);
return node;
}
}
return null;
}