原帖地址:http://blog.csdn.net/roderick2015/article/details/52563782,转载请注明。
1. 简单介绍
众所周知HashMap以键值对的形式存储数据,那什么是键值对呢?我们举个栗子,你带着银行卡和钞票去银行存钱,办完事你卡留着,下次通过这个卡就能拿到上次存进去的钞票,这种卡和钞票的关系在HashMap中叫做映射。
起初呢,HashMap只是一个小小的数组,它拿到key之后,通过一个叫hash的算法,得到与该key对应的数组下标,并把key和value都存储到这个位置上,这就是HashMap的一个Node,计算这个下标的算法受数组长度和key值的影响,只要两者不变,位置就是固定的,所以在查询时花费的时间复杂度是O(1)。当你的key越来越多,出现两个甚至多个key被hash到同一个位置的情况,这就是hash碰撞,怎么办?我这只能存一个键值对啊。
为了解决该问题,HashMap就引入了两个方案,一个是扩容,一个是链表,扩容就是Node到了一定数量后,我就扩大数组,增大你们Hash到不同位置上的机会,但是扩容后之前存储的Node又得重新Hash找位置,这个就是性能问题了。链表是在碰撞的位置上,建个链表,最早来的那个Node作为链首,再碰撞就依次往后排。但是下次查询这些key的时候,就得遍历链表,时间时间复杂度成了O(n),当然n其实也能接受,除非你真有这么多键值对产生了碰撞,但是在Java8中,为了优化链表问题,引入了红黑树,当链表长度超过某个值时,就转为红黑树存储,这样时间复杂度就下降到了O(log N),讲到这大家就清楚HashMap里借用了数组,链表和红黑树三种数据结构进行数据的存储啦,容貌如下图所示。
2. 继承体系
首先呢,我们了解下HashMap的继承体系,如下图所示。
其中HashMap实现的Cloneable和Serializable接口好理解,是为了提供克隆和序列化支持,而它继承的抽象类AbstractMap以及实现的Map接口是干嘛的呢,而且AbstractMap又去实现了Map接口,这有点绕啊。其实很好理解,我们来讲个故事。
我叫HashMap,是Map门派的直系弟子,学过Java的肯定都认识我。我的师父,人称AbstractMap,真正接触过他的人并不多,毕竟掌门嘛神龙见首不见尾,苦活累活还是咱这些弟子来干。相传我派创立之初,Map祖师爷传下了一套心法口诀,凡我派弟子必须修行此心法 (就是Map接口里的抽象方法啦,那是一套标准必须遵守,实现了Map接口,你就能名正言顺的修行它)。怎奈这口诀过于高深,晦涩难懂,却没有配套的修行功法。好在我师父乃是百年难遇的奇才,为大部分口诀配了一套详细的功法 (抽象类中的具体实现),这下好了,我们对着学就行了 (是一套公众标准,子类可以直接使用),不用走火入魔啦。可是和大家功法都一样,还怎么成为师傅的得意门生?师傅说过想要牛B,必须走出自己的道路,所以我结合自身的特性,在师傅功法的基础之上又创立了自己的功法 (子类在父类基础上进行扩展),自此我HashMap才能被你们耳闻,像其他师兄弟比如ConcurrentHashMap、TreeMap、IdentityHashMap可都是走出了自己独特的道路哦。
看完上面这个故事我想大家对之前提出的问题有了一个自己的理解,Map接口定义的是一套抽象标准,实现这个接口的类都属于这个体系,而且需要提供标准的具体实现,而AbstractMap是Map接口的直接实现,同时是抽象类无法实例化,它则提取出公共的实现方法,或者一套标准流程。子类可以直接使用,也可以在此基础上扩展,这是对开闭原则的很好实现。另外一个好处就是子类继承抽象类,就不用实现Map接口中所有的抽象方法了,按需食用是不是很棒。有兴趣的读者可以去熟悉一下Map和AbstractMap再接着往下看,会有更多的收获。
3. 基本成员
变量
/**
* Node数组,hashmap存储映射数据的容器,后面简称容器数组
*/
transient Node<K,V>[] table;
/**
* 用于缓存Entry集合
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 记录实际存储的元素个数,注意与table(容器)的大小区分
*/
transient int size;
/**
* 记录对HashMap结构性改变的次数,就是在增加、删除或者扩容的时候modCount+1,
* 所以对value的修改其实是不记录的。
*
* 因为HashMap是非线程安全的,所以在迭代数据时会比较这个值,如果不等说明有其他线程进行了修改,
* 于是抛出ConcurrentModificationException,这个叫fail-fast机制,但也因为这个机制你在
* 迭代Map数据的过程中,做了结构性的修改后继续迭代,那么也会抛出这个异常。
*/
transient int modCount;
/**
* 临界值 = table(容器)的大小 * loadFactor,当实际元素的个数(size)大于它的时候,
* HashMap就会开始扩容
*/
int threshold;
/**
* 加载因子,比如默认值是0.75,那么实际元素个数超过容器大小3/4的时候就会开始扩容,
* 这个值可以在实例化的时候修改,但是一般咱用不着,而且它是final声明的,你只能赋值1次。
*/
final float loadFactor;
变量对应的各种默认值,就是你不指定,那我就用它们。
/**
* table(容器)的默认初始大小,这里使用的是位运算,
* 对二进制进行直接操作,左移4位就是0001 0000 = 2<sup>4</sup> = 16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* table(容器)的最大容量
* 左移30位,2<sup>30</sup>是int最大值+1的一半 <br>
* 我们知道1个int类型占4个字节(byte),1个字节8位(bit),所以它的范围是-2<sup>31</sup>到2<sup>31</sup>-1
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 加载因子的默认值
*/
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;
- 构造函数
/**
* 一般常用的就是这个构造函数啦,所有值均采用默认值
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* 这个构造函数可以让你在实例化HashMap的时候指定容器的初始容量。
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 可以指定容器的初始容量和加载因子。
* 比如你打算存13个键值对,而HashMap默认的threshold是12,这样就会执行一次扩容操作,
* 所以你可以指定一个大于18的初始容量。
*/
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;
//注意取得容器大小后是给threshold赋值,也就是说现在table(容器)依旧为null,
//进一步的操作我们可以在put方法中找到
this.threshold = tableSizeFor(initialCapacity);
}
我们再看下第三个构造函数中的tableSizeFor方法。
/**
* 根据你给定的容量大小,返回一个2的n次方的值,
* 也就是对你设定的值进行一个修整,至于干嘛这么做在hash方法中我们会讲到,
* 比如上面我们说的想存储13个键值对,如果你指定18的初始容量,那么会被修改为32,而你直接指定32容量的话,就不会被修改了。
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1; //首先n无符号右移1位,再与n本身作位或运算
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16; //最后高位移低位,位或运算后确保结果为2的n次方
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
- 节点
/**
* 链表节点,其实数组用的也是这个节点
*/
static class Node<K, V> implements Map.Entry<K, V> {
final int hash;
final K key;
V value;
Node<K, V> next;
//其余代码省略.....
}
/**
* 树节点
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
//其余代码省略.....
}
4. 常用方法
- put方法
/**
* 将键(key)和值(value)以映射关系存入HashMap中,
* 用于新增或修改(覆盖)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
这里是先通过hash方法,对key进行了改造,再传入的putVal方法。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//看容器是不是空的,要是空的就开始扩容,顺带把容器数组的长度赋给n,数组引用赋给了tab。
//当你塞入第一个键值对得时候,他当然就是空的啦,就算你在实例化时,指定了一个初始容量,
//也只是暂时赋给了threshold。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//通过(n - 1) & hash 立马获取到key值在数组中对应的下标,
//啥,这样就拿到下标了?别急,请看后面的hash方法分析。
if ((p = tab[i = (n - 1) & hash]) == null)
//如果这个位置的节点是空的,那就是新增操作,new一个新节点,
//注意这里next参数传的是null:
//我只是个数组元素又不是链表,干嘛去记下个节点。
tab[i] = newNode(hash, key, value, null);
else {
//最后一种情况就是,这个位置已经有节点了,那该怎么处理呢?
Node<K,V> e; K k;
//如果你两相等,那就赋给引用e不管了,最后都是对e判断是否覆盖当前的值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//好家伙,你是个树节点?那就交给putTreeVal方法了。
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) {
//都遍历到链表尾部了,那说明是新值,new一个新节点。
p.next = newNode(hash, key, value, null);
//再检查下当前链表的长度是不是可以晋升为树型了(老长了,遍历你可费劲了,速速变身)。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); //链表转树方法
break; //跳出循环,收工下班。
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break; //在链表某个位置,找到了相同的节点,那咱也别干了,收工下班。
p = e;
}
}
if (e != null) { //如果上面执行的是新增操作,那e的值是null。
V oldValue = e.value;
//onlyIfAbsent为true时,不会执行覆盖操作,但之前存的是null值,得覆盖。
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//这个和afterNodeInsertion方法都是留给LinkedHashMap的方法,
//HashMap中未实现,供子类扩展。
afterNodeAccess(e);
return oldValue; //修改操作,返回旧值,后面的代码不执行了。
}
}
++modCount; //修改次数+1
if (++size > threshold)
//如果元素个数大于临界值,执行扩容操作。
resize();
afterNodeInsertion(evict);
return null; //新增操作,啥也不给。
}
最后我们再看看它对key做了些什么手脚。
static final int hash(Object key) {
int h;
//无符号右移16位,即hash值本身高位与低位的异或运算。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这个方法是对键(key)的哈希值进行位运算,然后在put方法中通过(n - 1) & hash计算出散列在数组中的位置。这里需要面对3个问题:
1.需在很短的时间(O(1))内计算出索引位置且必须在容器内,不能出现越界。
2.必须散列均匀,如果hash碰撞过多,那就成链表查询了。
3.当数据达到一定量时,必将会有更多的碰撞,那链表越来越长时,该如何处理?
第一个问题我们可以使用key与table.length(容器数组的长度)作取模运算获得,HashMap也是这么干的,但是它干的更好。我们需要知道当n(容器数组的长度)的长度是2的n次方时,(n - 1) & hash是等价于hash % n的,这也就是为什么HashMap会对你指定的初始容量进行修改,而位于运算显然更优于取模运算。
第二个问题HashMap是通过hash方法以及适当的扩容来实现优化的,该方法的重点是让高位也参与了计算,这样可以得到更多、更均匀的结果,比Java7中HashMap的优化更进一步。
第三个问题在Java7中并没有优化处理,因此在碰撞较多的情况下,效率并不理想。Java8中则将超过一定长度的链表转为红黑树存储,时间复杂度由n减少到了log N。因此碰撞越多优化效果越明显,但代码复杂度也是直线上升啊。
- get方法
public V get(Object key) {
Node<K,V> e;
return (e = 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, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//先在容器数组的索引位置找,如果没找到,说明经过hash碰撞已经成链表或者树了,也有可能没这个键值映射。
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<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);
}
}
//根本就没有,你骗我,给你个null。
return null;
}
- containsKey方法
/**
* 调用get方法寻找Node,找到则返回true
*/
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
- remove方法
/**
* 移除指定键值对,返回被移除的value
*/
public V remove(Object key) {
Node<K,V> e;
return (e = 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, index;
//判断该索引位置上,有没有node
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;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p; //这个node的key和给定的key相等,那就是它了。
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);
}
}
//matchValue如果传的是true,则通过后面条件判断,也就是value的值相等时,才会执行删除操作
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;
++modCount; //修改次数+1
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
- keySet方法
transient volatile Set<K> keySet;
/**
* 返回HashMap中的键集合
*/
public Set<K> keySet() {
Set<K> ks;
return (ks = keySet) == null ? (keySet = new KeySet()) : ks;
}
首次调用keySet方法时,变量keySet的值是null,也就是说在你调用该方法的时候,HashMap才会生成KeySet对象,再由keySet变量缓存起来供下次使用,那么KeySet是一个怎样的类呢?
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
//其余代码省略.....
}
原来KeySet就是一个集合类,里面的其他方法都好理解,iterator()方法稍微绕了一下,里面new了一个KeyIterator对象,他其实就是一个key的迭代器,用于HashMap中key的遍历,而且这个方法也是我们经常使用到的,平时使用的for循环,也是拿到这个迭代器,然后对key进行迭代的。相对应的还有values()和entrySet()方法原理都是一样的,因为这些迭代器不过是抽象父类HashIterator的扩展,有兴趣的读者可以去尝试一下,这里就不铺开了。
好了HashMap就暂且讲到这,上面的知识已经足够你灵活使用HashMap啦,而它的扩容机制以及红黑树的操作,我会放到下篇帖子中讲解!
对本帖代码感兴趣的读者可以点击此处查看。