put 方法的源码解析
put方法是操作HashMap是最常用的方法,它的就用就是将数据放到HashMap中,其流程图如下所示:
如上所示主要有一下几个步骤:
首先判断散列表是否为空,为空的话则先初始化数组。
根据键值key计算hash值并得到插入的数组索引
如果索引值没有被占用则直接插入键值对
如果索引值被占用则判断key是否存在,存在的话则直接覆盖value,不存在的话则判断当前节点是否是TreeNode。如果是的话则走红黑树直接插入键值对。
插入完元素之后,则判断当前的数据容量是否大于传入的数组大小,如果大于的话则进行扩容。
因为put方法只是调用putVal方法,所以,我们只需要分析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; //如果table还没未被初始化,则直接进行初始化,一般在HashMap被定义后,首次调用put方法时被触发 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) //如果计算得到的位置没有被占用,则直接存放。 tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //判断是否是同一个key if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //标记冲突的头结点e //是否已经树形化过 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) { 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; } } //相同的key,用新的value替换原来的value,并返回原来的value if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } //modCount自增1,用于fast-fail ++modCount; //达到扩容阀值,进行扩容 if (++size > threshold) resize(); //put操作evict为true,仅当构造方法内evict为false。 afterNodeInsertion(evict); return null; }
其中modCount的作用的记录设置的新的key的次数,用于fast-fail。
get方法的源码解析
get方法是根据传入的key,从HashMap中取出相应的value。如果找不到则返回null,能找到的话则返回找到的value。
流程图如下:
如上流程图:主要的流程说明是:
1.首先判断传入的key,计算得到的数组下标是否为空,为空的话直接返回null。
2.不为空的话,则查找位置上的第一个元素是否符合,如果符合的话则返回第一个元素的node
3.如果不符合的话,则接着判断结点是否是TreeNode,是的话则从红黑树中搜索对应的key。
4.如果不是话则遍历链表,知道找到需要的传入的key,最后返回node。
源代码如下:
get 代码只是定义了一个Node,然后调用了getNode方法 public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
getNode的源码如下所示:
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //根据传入hash值做按位与运算(取模运算)在哈希桶中的位置是否为空,如果为空则返回null if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //总是检查位置上的第一个元素,如果第一个元素符合则直接返回, // 不用管这个桶的位置上是链表还是红黑树 if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) return first; //执行到这,表示这个位置上至少是一个链表了。 if ((e = first.next) != null) { //判断这个位置是否已经树形化过 if (first instanceof TreeNode) //从红黑树中搜索对应的key,时间复杂度O(logn) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //还未树形化,则遍历链表,直到找到对应的key。时间复杂度是0(n) do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
remove方法的源码解析
remove 方法可以移除指定的key的元素。与get方法类似,其方法内部也是调用了一个removeNode主体方法来处理元素的移除,removeNode方法的源代码如下:
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; //数组不为空,找到要移除的key对应的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; //哈希值相等,且与key为同一对象,记录结点node if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; //next不为空,证明key与其他对象发生哈希冲突 else if ((e = p.next) != null) { //链表已经树形化过 if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { //仅为链表,未树形化 //遍历找到要移除的结点node do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //确定要移除的node,开始根据不同的数据结构移除的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) //不冲突移除 tab[index] = node.next; else //链表移除 p.next = node.next; ++modCount; //modCount自增,记录修改次数 --size; //size做相应减少 afterNodeRemoval(node); return node; //返回移除结点node } } return null; }
总结
HashMap 扩容操作比较耗时,所以,如果事先知道HashMap的大小的话,最好指定大小。
HasMap所以操作都没有加锁,所以其是线程不安全的容器,在多线程环境下,请使用并发容器concurrentHashMap。
参考
JDK1.8的源码