package demo.JavaJdk8;
import java.util.HashMap;
import java.util.Map;
/**
* @author Xch
*/
public class MapDemo{
public void putDemo(){
Map mapDemo=new HashMap<>(2);
mapDemo.put("one",1);
Integer one=mapDemo.get("one");
System.out.println(one);
}
}
上面的代码是我们平时对HashMap最简单的使用:
1、new 一个实例对象。
2、之后调用 put() 方法为集合添加一个键值对。
3、之后我们再调用 get() 方法得到一个键的值。
无论是 JDK7 还是 JDK8 都是这样的使用,但 JDK8 对 HashMap 进行了更加“优美”的优化。
以下所有代码、解读都基于 JDK8 ,为了方便查看源代码,我是用了IntelliJ_IDEA开发工具。
HashMap的初始化,很简单的赋予这个HashMap一个初始化长度为2。
我们看看数组初始化做了什么?
让我们按键Ctrl+鼠标放在HashMap<>(2)上,之后鼠标左击,便会进入HashMap的默认构造函数源码中:
/**
* Constructs an empty HashMap with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
之后按键Ctrl+鼠标放在this上,之后鼠标左击(同上查看源码操作,以后不再详解),便会进入HashMap的另一个构造函数源码中:
/**
* Constructs an empty HashMap with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
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;
this.threshold = tableSizeFor(initialCapacity);
}
这里我们需要驻足,详细解析HashMap这个构造函数:
两个参数:初始化数组大小(initialCapacity)和 加载因子(loadFactor默认值为0.75)
如果:初始化数组大小(initialCapacity)< 0
抛出一个IllegalArgumentException异常。
如果 :初始化数组大小(initialCapacity)> MAXIMUM_CAPACITY ( = 1 << 30 = 1*2^30 )
初始化数组大小(initialCapacity)= 2^30
如果:加载因子(loadFactor)<= 0
抛出一个IllegalArgumentException异常。
之后为加载因子(loadFactor)赋值。
最后一行代码:this.threshold = tableSizeFor(initialCapacity);----------(深入):
查看tableSizeFor()方法源码:
/**
* Returns a power of two size for the given target capacity.
*/
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;
}
这段算法会返回一个 距离 参数cap 最近的并且没有变小的 2 的幂次方数,比如传入10 返回 16,就是这么神奇!
给出算法的过程:
cap = 10;
n = 10 - 1;
n = 9; (1001)
1001 >>> 1 = 0100;
1001 或 0100 = 1101;
1101 >>> 2 = 0011;
110 或 0011 = 1111;
1111 >>> 4 = 0000;
1111 或 0000 = 1111;
1111 >>> 8 = 0000;
1111 或 0000 = 1111;
1111 >>> 16 = 0000;
1111 或 0000 = 1111;
1111 == 15;
15 + 1 = 16;
threshold便是HashMap的阈值,但此时的这个阈(yu)值,只是初始化时给定的,不是最终的。
HashMap的初始化到此结束!
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with key, or
* null if there was no mapping for key.
* (A null return can also indicate that the map
* previously associated null with key.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
先查看hash(key)的源码:
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
// 更好的均匀散列表的下标
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
继续查看putVal(hash(key), key, value, false, true); 的源码:
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
// 如果全局变量table为null,或者长度为0,那么需要为tab初始化数组。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 如果通过hash值计算出的下标的地方没有元素,根据给定的key 和 value 创建一个元素
if ((p = tab[i = (n - 1) & hash]) == null) // <--1-->
tab[i] = newNode(hash, key, value, null);
// 如果hash冲突(新的hash我们称之为 新hash,被冲突的已经存在的hash我们称之为 旧hash.
// 其他元素也照此称之)
else {
Node e; K k;
// 如果新hash和 旧hash 值相等并且(旧key和新key相等 (地址相同,或者equals相同)),
// 说明新key和旧key相同,那么我们把旧p赋值给e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果p的类型是树类型,则让红黑树追加这个键值对,赋值给e
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
// 如果key不相同,且hash冲突,且不是树,则只能是链表
else {
// 循环链表
for (int binCount = 0; ; ++binCount) {
// 如果链表元素的next为空,表明链表到尾巴了
if ((e = p.next) == null) {
// 创建新节点,赋值给已有的next属性上.(把新键值对追加到链表尾巴上)
p.next = newNode(hash, key, value, null);
// 如果链表长度大于7,也就是等于8时
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 则将链表改为红黑树 (jdk8新特性)
treeifyBin(tab, hash); // <--2-->
// 结束循环
break;
}
// 如果新hash值和next的hash值相同且(key也相同)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 结束循环
break;
// 如果新hash值不同或者key不同。则将next值赋给 p,开始下次循环
p = e;
}
}
// 综合上面所有的判断,如果e不是null,那么该元素已经存在了(也就是新旧的key相等)
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 这里的onlyIfAbsent 是false.如果 value 是null
if (!onlyIfAbsent || oldValue == null)
// 将新值 替换掉 老值
e.value = value;
afterNodeAccess(e);
// 返回被替换掉的旧值
return oldValue;
}
}
//如果e== null,迭代器的计数加一,为迭代器遍历使用
++modCount;
// 如果数组长度大于了阀值
if (++size > threshold)
// 重新散列
resize(); // <--3-->
afterNodeInsertion(evict);
// 返回null
return null;
}
使用数组长度减一 &运算 hash 值。这行代码就是为什么要让前面的 hash 方法移位并异或(详看hash(key)的源码)。
假设有一种情况:如果数组长度是 16,也就是 15 (1111)
对象 A 的 hashCode :1000010001110001000001111000000 & 1111 = 0
对象 B 的 hashCode :0111011100111000101000010100000 & 1111 = 0
&运算这两个数, 你会发现结果都是 0。这样的散列结果太让人失望了。很明显不是一个好的散列算法。
但是如果我们将 hashCode 值右移 16 位,也就是取 int 类型的一半,刚好将该二进制数对半切开。并且使用位异或运算(如果两个数对应的位置相反,则结果为 1,反之为 0),这样的话,就能避免我们上面的情况的发生。
参考链接:https://hacpai.com/article/1514646296615
查看treeifyBin()源码:
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
// 如果数组等于null 或 数组长度小于 64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 重新散列,使得链表变短
resize();
// 如果hash冲突,且数组长度大于 64,则只能使用红黑树结构
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);
}
}
// For treeifyBin
TreeNode replacementTreeNode(Node p, Node next) {
// 返回一个新的红黑树
return new TreeNode<>(p.hash, p.key, p.value, next);
}
重新散列函数resize()源代码:
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 如果旧容量大于0
if (oldCap > 0) {
// 如果旧容量大于等于2^30
if (oldCap >= MAXIMUM_CAPACITY) {
// 阀值等于 Integer的最大值
threshold = Integer.MAX_VALUE;
// 返回旧数组,不扩充
return oldTab;
}// 如果旧容量*2 小于 最大容量 且 旧容量 大于等于 默认容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新阀值 = 旧阀值*2
newThr = oldThr << 1; // double threshold
} // 如果旧阀值 大于 0
else if (oldThr > 0) // initial capacity was placed in threshold
// 新容量 = 旧阀值
newCap = oldThr;
else { // 如果容量是0,阀值也是0,认为这是一个新的数组,使用默认容量16 和 默认阀值12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新的阀值是0,重新计算阀值
if (newThr == 0) {
// 新容量 * 负载因子(0.75)
float ft = (float)newCap * loadFactor;
// 如果新容量 小于 最大容量 且 阀值小于最大
// 则新阀值等于刚刚计算的阀值,否则新阀值为 int 最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将新阀值 赋值 给当前对象的阀值。
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 创建一个Node 数组,容量是新数组的容量
//(新容量 要么是 旧容量,要么是 旧容量*2,要么是16)
Node[] newTab = (Node[])new Node[newCap];
// 将新数组 赋值给 当前对象的数组属性
table = newTab;
// 如果旧数组 不是null
if (oldTab != null) {
// 循环旧数组
for (int j = 0; j < oldCap; ++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)
// 调用红黑树split()函数,将树的数据重新 散列 到数组中
((TreeNode)e).split(this, newTab, j, oldCap);
// 如果不是树,next 节点也 不为空,则是链表,
//注意,这里将优化链表重新散列(jdk8 的改进)
else {
// jdk8前,是并发操作,所以会出现环状链表,但jdk8 优化了此算法。
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
// 这里的判断需要引出一些东西:oldCap 假如是16,
// 那么二进制为 10000,扩容变成 100000,也就是32.
// 当旧的hash值 &运算 10000,结果是0的话,
// 那么hash值的右起第五位定是0,那么该于元素的下标位置也就不变。
if ((e.hash & oldCap) == 0) {
// 第一次进来时给链头赋值
if (loTail == null)
loHead = e;
else
// 在链尾巴赋值
loTail.next = e;
// 重置该变量
loTail = e;
}
// 如果不是0,那么就是1,也就是说,如果原始容量是16,
// 那么该元素新的下标就是:原下标 + 16(10000b)
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 可将原链表拆成2组,优化查询。
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
查看源代码:
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
*
A return value of {@code null} does not necessarily
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
Node e;
// key依旧被hash()函数处理过
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
// 如果table不为空,且 table长度大于0,且下标:数组长度-1 与 key的hash,的值不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 数组的第一个节点的键和hash都等于传递进来的key和hash,则返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果数组的第一个节点的next属性不为空
if ((e = first.next) != null) {
// 如果是树结构,则使用树获取值
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
// 如果是链表结构,则使用while循环,获取值
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 返回null
return null;
}
这篇文章,除了介绍JDK8的hashmap的源码,其实也是在演示如何使用IntelliJ_IDEA来看我们想看的源码,很简单。
1、Ctrl + 鼠标点击方法/类 我们就可以看到对应的源码。
2、jdk8引入了树结构,来优化 链 过长所带来的性能低化的问题。
3、还有HashMap的初始容量总会是 2 的幂次方,因为HashMap的性能非常依赖这个 2 的幂次方。
容我再仔细想想总结,你们可以评论,我加上!
到此结束!
---------------------------------------------------------------------------不关注我“象话”吗?
如有疑惑,请评论留言。
如有错误,也请评论留言。
---------------------------------------------------------------------------
参考文章:
HashMap为什么初始容量为2的次幂:https://blog.csdn.net/ig_xdd/article/details/79065717
深入理解 hashcode 和 hash 算法:https://hacpai.com/article/1514646296615
深入理解 HashMap put 方法(JDK 8 逐行剖析):https://hacpai.com/article/1514726612565