JAVA1.6版本
一、HashMap结构图
可以看出
- 1.HashMap是一个数组+链表的结构,数组的下标在HashMap中称为Bucket值,每个数组项对应的是一个List
- 2.每个List中存放的是一个Entry对象,这个Entry对象是包含键和值的
二、HashMap存放对象的流程
HashMap使用put(key,value)函数存放对象,当调用put(key,value)方法的时候会发生以下步骤
- 1.通过key的hashCode()函数计算key的hash值,再通过hash值得出Bucket值(如何通过hash值的出Bucket值,继续玩下阅读)
- 2.得出Bucket值后,如果该Bucket值对应的list是一个空列表,那么将生成entry对象,插入到list中,做为list的第一个元素
- 3.如果该Bucket值对应的list中已经有其它对象了(如果两个key的hash值一样就会发生),这个时候就发生了碰撞。
- 4.发生了碰撞后,新插入的key就会和链表中的其它entry的key进行比较,比较过程源码如下,需要同时用到equals()方法
p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))
- 从源码中可以看出如果两个key的hashcode一样,并且equals方法比较也相同,那么HashMap就判断这两个key完全一样
如果想更多了解hashCode和equals方法参看《Java equals和HashCode方法总结》 - 5.如果两个key比较结果一样,由于HashMap不允许同时存在两个相同的key,那么新插入的entry就会覆盖旧entry
- 6.如果比较不一样,那么将新的entry插入到list的末尾 总体put源码如下:
public V put(K key, V value) {
// 处理key为null,HashMap允许key和value为null
if (key == null)
return putForNullKey(value);
// 得到key的哈希码
int hash = hash(key);
// 通过哈希码计算出bucketIndex
int i = indexFor(hash, table.length);
// 取出bucketIndex位置上的元素,并循环单链表,判断key是否已存在
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 哈希码相同并且对象相同时
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
// 新值替换旧值,并返回旧值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// key不存在时,加入新元素
modCount++;
addEntry(hash, key, value, i);
return null;
}
三、rehashing
1.初始容量和加载因子。
默认初始容量是16,加载因子是0.75。容量是哈希表中数组的长度,如文章最开始给的图其容量就为16,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。
2.如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
3.重新调整HashMap大小存在什么问题
当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在LinkedList中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在LinkedList的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。
所以不要在多线程的环境下使用HashMap
五、为什么容量一定为2的幂
1.HashMap的初始化函数
public HashMap(int initialCapacity, float loadFactor) {
......
// Find a power of 2 >= initialCapacity
// 设置最重的容量为小于initialCapacity的2的整数次幂的最小数
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
......
}
所以可以看出capacity被设计成了一个一定为2的整数幂的数
如果initialCapacity设置为11
那么最终的capacity就为8
2.为什么这么设计?
HashMap中的数据结构是数组+单链表的组合,我们希望的是元素存放的更均匀,最理想的效果是,Entry数组中每个位置都只有一个元素,这样,查询的时候效率最高,不需要遍历单链表,也不需要通过equals去比较K,而且空间利用率最大。那么怎么才能将分布最大的均匀化呢?那就是取余运算%,哈希值 % 容量 = bucketIndex
SUN的大师们是否也是如此做的呢?我们阅读一下这段源码:
static int indexFor(int h, int length) {
return h & (length-1);
}
上面这段代码就实现了取余操作
当容量一定是2^n时,h & (length - 1) == h % length。
举个直观的例子,看看这么设计的好处:
我要将hash值分别为2,4,6的key插入到HashMap中
如果容量为10那么 (length - 1)的二进制为:1001
那么h & (length-1)的结果分别为
2:10 & 1001 = 0
4:100 & 1001 = 0
6:110 & 1001 = 0
所以这3个数的hash值都为0,都发生碰撞了
如果容量为8那么 (length - 1)的二进制为:111
那么h & (length-1)的结果分别为
2:10 & 111 = 2
4:100 & 111 = 4
6:110 & 111 = 6
3个数都没有发生碰撞
五、总结
HashMap的工作原理
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。
当两个不同的键对象的hashcode相同时会发生什么?发生碰撞了,它们会储存在同一个bucket位置的LinkedList中。键对象的equals()方法用来找到键值对。
一些优秀的开发者会指出使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。
参考文章:
HashMap深入解析
HashMap工作原理
JAVA1.8版本
一、前言
在JDK1.6中,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里。但是当位于一个链表中的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用数组+链表+红黑树来实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。(关于红黑树)
二、TreeNode
JDK1.8中引入了TreeNode类型的节点,当链表长度超过8时就将链表转换为红黑树
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
...
三、插入节点
public V put(K key, V value) {
return 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, i;
//如果table为空或长度为0,则初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(n - 1) & hash:相当于 hash值%数组长度,取余找到put位置,如果这个位置还为空,则直接创建Node作为该位置链表的第一位
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//如果在这个位置已经有练表了
Node<k,v> e; K k;
//如果练表的第一个节点的key就和插入值的key相同,那么位置找到,不然就要继续寻找
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 {
//不是红黑树就是链表,binCount计数,如果大于8就要转换成红黑树
for (int binCount = 0; ; ++binCount) {
//p第一次指向表头,以后依次后移
if ((e = p.next) == null) {
//e为空,表示已到表尾也没有找到key值相同节点,则新建节点
p.next = newNode(hash, key, value, null);
//新增节点后如果节点个数到达阈值,则将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
//查找练表中key和插入key是否一致,一致的话位置找到
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//更新hash值和key值均相同的节点的Value值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
四、删除节点
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, index;
if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//根据key的hash值找到对应位置的练表
//如果练表的第一个元素的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 {
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;
//如果找到的节点不是链表的第一个节点,那么此时p是node的前一个节点(观察前面在链表中遍历查找位置的代码就可以发现)
//那么直接通过p.next = node.next跨过node节点,达到删除效果
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
可以看到jdk1.8中对Put的处理流程比1.6版本中多引入了红黑树的概念达到加快查询的效果