HashMap源码

构造方法

阅读HashMap源代码,首先看他的构造方法,大概知道要怎么用他:
在这里插入图片描述
构造方法有4个:

// 设置初始化容量和loadFactor2个参数,map还是空的
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);
}
// 用的最多的构造方法,他只是将loadFactor设置了默认值,其他的字段都设置成本来的默认值,map还是空的
public HashMap() {
     this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 设置一个初始化容量大小,map还是空的
public HashMap(int initialCapacity) {
     this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// loadFactor设置了默认值,同时向集合中放入一些数据,map就不再是空的了  
public HashMap(Map<? extends K, ? extends V> m) {
     this.loadFactor = DEFAULT_LOAD_FACTOR;
     putMapEntries(m, false);
}

以上4个构造方法里面本质上是在初始化2个参数,负载因子loadFactor 和阈值threshold
在这里插入图片描述
这里有2个地方需要说明一下
第一个,从构造方法HashMap(int initialCapacity, float loadFactor)的实现可以看出来,HashMap的最大初始化容量是static final int MAXIMUM_CAPACITY = 1 << 30; 1<<30 的值是2的30次方即1073741824;最小容量是0。
第二个,就是初始化容量时调用的tableSizeFor()方法,这个方法会把传入的参数“格式化”成与传入参数最接近的2的N次方,比如:tableSizeFor(15) = 16 ,tableSizeFor(1000) = 1024 ,方法具体说明在后面。

构造方法看完之后我们观察一下代码,会发现作者把代码分成了若干个部分,按作者的注释分成了:

  • 静态代码块 (/* ---------------- Static utilities -------------- */)
  • 字段定义(/* ---------------- Fields -------------- */)
  • 公共方法区(/* ---------------- Public operations -------------- */)
  • 克隆和序列化(// Cloning and serialization)
  • 迭代器(// iterators)
  • 拆分器(// spliterators)
  • 对LinkedHashMap的支持(// LinkedHashMap support)
  • 树操作的封装(// Tree bins)
  • 红黑树的操作方法,左旋、右旋、平衡树插入等(// Red-black tree methods, all adapted from CLR)

一、静态代码块(Static utilities)

静态代码块中有4个方法:`int hash(Object key)、comparableClassFor(Object x)、int compareComparables(Class<?> kc, Object k, Object x)、int tableSizeFor(int cap)`
hash(Object key) 方法

计算hash值的方法
看方法注释,第一句Computes key.hashCode() and spreads (XORs) higher bits of hash to lower 计算key的hashCode,并且通过XOR(异或)的方式把高位传到低位。为什么要这样计算,注释后面详细的说明了这么做是为了减少hash碰撞,使得计算出来的hash值能够更加好的分布在数组上。具体操作就是将key的hash值与其无符号右移16位后的值进行异或运算。
使用(h = key.hashCode()) ^ (h >>> 16)计算hash值的根本原因?
hash值的计算的目的是为了决定当前这个Node应该放在数组的哪个位置上,从HashMap的putVal方法中可以看到计算方式实际上是将计算出的key的hash值与数组长度减去1取与运算(i = (n - 1) & hash)得出位置的,也就是说甭管你hash值有多大,最终起作用的实际上只有n-1那一段,所以如果不去将hash值高位与低们做一定的运算的话,hash值前面一大截相当于用不起作用了,另外与(&)、或(|)操作都会使得结果偏向0或者1 ,所以用异或(^)运算一下,当然设计一个更高级方式运算也不是不可以,但是这里运算的目的仅仅是为了让高位参与一下运算以便根据hash值计算数组位置时能稍微更分散一些,没必要搞的那么复杂。画图示意一下:
HashMap位置计算
这里注意几点:

  • 首先这里计算的是Node对象中的key的hash值;
  • 如果key为null,他会返回个0,说明如果HashMap的key值为null,会默认计算(写死)他的hash值是0,把他放到第0个位置上面;
  • 如果key不是null,则调用他的hashCode()方法,并将hashCode()方法计算出的值的高位与低位进行一个异或运算从而得出最终的hash值。
Class<?> comparableClassFor(Object x)方法

从注释可以看出来这个方法的作用是返回一个类类型,如果这个类实现了Comparable接口的话。

int compareComparables(Class<?> kc, Object k, Object x)方法

返回k和x比较的结果

int tableSizeFor(int cap) 根据给定值计算HashMap容量大小
    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;
    }

此方法的目的是不论传什么值进来,最终返回的容量大小都是2的N次方(比如:tableSizeFor(5)=8、tableSizeFor(1025)=2048),此方法的计算方式如下:
tableSizeFor()方法示意图
为什么这里要先把cap减个1再去作与运算?因为不论传多大的值进来只要他不大于最大值MAXIMUM_CAPACITY static final int MAXIMUM_CAPACITY = 1 << 30; 经过最多5次与操作运算后得到的结果二进制的表达形式一定是所有位上都是1,即计算结果一定是111…111,所以最后要进行加1操作结果才会是2的N次方,所以才提前去减1。

二、 关键字段(Fields)

除了上面构造方法中提到的2个字段外,Fields下面还有4个字段,Node类型的table数组,Set类型的entrySet集合,HashMap大小size以及HashMap结构修改次数modCount(modified count 修改次数)
在这里插入图片描述Node<K,V>[] table 节点
此节点乃是真正存放数据的节点,Node对象的定义如下:
Node对象
他包含了key、value、hash值和指向下一个节点的next节点对象。这个对象中只有一个next字段,说明HashMap中的链表是单向链表
Set<Map.Entry<K,V>> entrySet集合
主要用于保存缓存的entrySet()。
size就是当前HashMap的大小
modCount HashMap结构修改次数
结构修改是指改变HashMap中映射的数量或以其他方式修改其内部结构。该字段用于使HashMap的collection -view上的迭代器快速失败

三、 公共方法(Public operations)

公共方法中构造方法已经看过了,再往下是几个简单的方法int size()boolean isEmpty() 。size()方法直接返回了类中的变量size的值,说明HashMap在变化的时候他的size值肯定是一起在变化的。所以isEnpty()方法也可以通过size的值来作判断。再往下是HashMap中最核心的方法之一,get(Object key)方法。

HashMap的get方法 public V get(Object key)

HashMap的get方法中调用了getNode()方法,如果取到了Node,则返回Node节点中的value值,否则返回null。

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

getNode()方法传了2个参数,一个是根据key值计算的hash值,一个是key,所以看一下getNode()方法:

    final Node<K,V> getNode(int hash, Object key) {
    	// 定义变量
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 给变量赋值,
        //tab赋值当前的hash表,n赋值为数组的长度,first根据hash值与数组长度减1取与操作,计算出key值在数组中的位置,所以取变量叫first
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        	// 如果刚好first节点就是要取的值(hash一致,key值一致),直接返回first
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 如果fisrt节点不是要取的值,并且下个节点不为空,就有2种情况了,此时根据hash值计算的节点要下有可能是个链表也有可能是个红黑树
            if ((e = first.next) != null) {
            	// 如果first节点是TreeNode类型的,说明他已经是红黑树了,此时就调用树的查询方法去取值,
            	// 注意TreeNode实际上继承了HashMap中的Node类的所以才直接返回
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 不是TreeNode类型的话就肯定是链表了,链表的话就直接不停的next去查询就可以了
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        // 最后查不到,则会返回null
        return null;
    }

所以HashMap的get方法总结一下就是:

  • 根据key值计算出来hash值;
  • 然后根据hash值和数组长度计算当前这个key对应的Node应该在数组的哪个节点上,并判断数组上的第一个元素是不是刚好就是要找的,如果刚好是要找的就直接返回;
  • 如果不是要找的那么肯定hash冲突了,所以这个节点上有可能是个红黑树或者是个链表;
  • 判断是不是树结构,是的话根据树节点的查找方法去查询;
  • 不是的话那肯定就是链表了,直接遍历链表。
HashMap的put方法 public V put(K key, V value)

put方法里面调用了putVal()方法,

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

真正核心的是putVal()方法:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    	// 根get方法类似的,定义要用变量
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 当数组为空或者数组长度为0的时候要扩容
        if ((tab = table) == null || (n = tab.length) == 0)
        	// 调用resize()方法扩容,并将容量(数组长度)赋值给n
            n = (tab = resize()).length;
        // 根据数组长度和hash值计算应该放在数组的哪个节点上,同时判断节点如果不为空的话就将节点赋值到变量p上
        if ((p = tab[i = (n - 1) & hash]) == null)
        	// 如果节点为空的话,直接将当前的对象放到这个节点上(即new一个Node)
            tab[i] = newNode(hash, key, value, null);
        // 当前这个节点不为空的情况下
        else {
            Node<K,V> e; K k;
            // 判断p的hash值与key值是不是与当前计算的一致,一致就将p赋值给变更e,这里跟get方法中的找fisrt是一样的
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 不一致的话判断p是不是树类型的,如果是树类型的,则调用树的put方法去往树上加节点
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 不是树类型的那就是链表了,链表就麻烦点了
            else {
            	// 从0开始循环,for循环中间没有条件
                for (int binCount = 0; ; ++binCount) {
                	// 找到链表上节点的next节点是null的节点,将新添加的对象放到next节点上去
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 如果链表的长度大于或8了,将就链表转成红黑树,binCount的值从0开始,计算到7的时候链表长度为8   
                        // TREEIFY_THRESHOLD 默认值是 8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果key在HashMap中已经存在了,就跳出循环遍历
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 如果key值在HashMap中已经存在,那么putVal()方法会返回已存在的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 完成添加元素后修改次数+1
        ++modCount;
        // HashMap的size大小+1,加1后判断当前大小有没有达到阈值,如果达到了就要扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

put方法总结一下就是:

  • 进来先判断一下表是不是为空,如果为空就调用resize()方法给他扩容一下;
  • 根据数组长度和hash值计算应该放在数组的哪个位置上,如果这个位置刚好是空的那就直接把对象放上去;
  • 如果不为空,那么就跟get方法类似,得先判断一下当前这个数组位置上保存的节点是树(TreeNode)类型还是链表(Node)类型;
  • 如果是树就调用树的putTreeVal()方法去树上加一个节点,树的方法比较复杂可能涉及到红黑树的旋转操作;
  • 如果是链表,就在链表最末端添加新元素;
  • 添加完新元素后判断链表长度是不是大于8了,如果是就将链表转成红黑树;
  • 添加时如果key已经存在,则返回原来的value值;
  • 添加完成后修改次数加1;
  • HashMap的size大小加1,加1后判断当前大小有没有达到阈值,如果达到了就要扩容。
HashMap的resize()方法 final Node<K,V>[] resize()

在putVal()方法中多次提及了resize()方法,resize()方法的作用他注释上写了:Initializes or doubles table size 初始化或者将表扩大两倍。可以看到resize()方法他是没有参数的,所以他肯定是只能固定的扩大,比如注释里面说的初始化(用默认参数进行初始化)、扩大两倍。如果要扩大3倍、5倍那是做不到的。看源代码。

    final Node<K,V>[] resize() {
    	// oldTab参数赋值
        Node<K,V>[] oldTab = table;
        // 旧的容量赋值,如果oldTab为空那就是0,否则就是他的数组长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 旧的阈值赋值
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
        	// 如果旧的容量值大于等于最大容量值(MAXIMUM_CAPACITY=1<<30 不可能大于,只可能等于),那就没救了,扩不了空了,所以直接返回旧的表
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 新的容量值等于旧的乘以2,他也得小于最大容量值,同时如果旧的容量值大于默认初始化容量值(1<<4 = 16)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 新的阈值等于旧的阈值乘以2
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        // 使用默认参数进行初始化,仅仅在为0的时候才作这个初始化操作
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 如果一通操作下来,新的阈值为0的话,就自己计算一下新的阈值,公式是新阈值=新的容量*加载因子,ag: newThr = 16 * 0.75 = 12
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
       	// 最后把计算的新的阈值给赋值回参数 threshold 上。
        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;
                    // 如果元素的next为空,说明只有数组上有一个节点,这个节点上没有发生过hash冲突
                    if (e.next == null)
                    	// 那么就直接用这个位置上的元素的hash值和新的数组长度来重新计算这个元素要放到新的数组中的位置
                        newTab[e.hash & (newCap - 1)] = e;
                    // e.next上不为空,并且e的类类型是TreeNode,说明这个位置已经是红黑树了,
                    else if (e instanceof TreeNode)
                    	// 如果是红黑树,就将树拆分成高位树hi和低位树lo,拆完后分别判断2个树的大小(与UNTREEIFY_THRESHOLD = 6相比)如果小于	                                               
                    	// 等于6则把树转成链表,否则就把半截树树化(旋转)一下,转完之后低位树(或链表)放在与原来位置相等的位置上,高位树放
                    	// 在原来的位置加老的数组长度的位置上,如下图所示。
                        ((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值与旧的容量取与操作看他是不是为0,因为hash表的容量一定是2的N次方,所以oldCap转成2进制后一定是只有1
                            // 位上是1其他位上都是0,所以与hash值取与时只要容量位为1的位置对应的hash值的位置是0结果就是0,是1结果就非0。结
                            // 果为0的放在低位的链表上
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 结果为1的放在高位的链表上
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } 
                        // 具体的往链表上加节点的操作
                        while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

HashMap扩容示意图
再往下看就是比较简单的一些方法了,比如public void putAll(Map<? extends K, ? extends V> m) 往HashMap里面put一个集合,这方法最终的实现还是靠调用putVal()方法;下一个是remove(Object key)方法,这个方法要看一下。

HashMap的remove()方法 public V remove(Object key)

可以看到,remove()方法具体的实现调用的是Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable)方法。

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;
}

先看他的参数,首先2个是根据key值计算出来的hash值和key,这2个参数不用多说,定位Node节点必须的2个参数。后面2个参数是value值和是否匹配value值的参数matchValue,这个值如果为true,则只有value值也匹配的上才删除(从方法调用上就能看到,value传的null,matchValue传的false,与这2个参数关系不大),最后一个参数movable,如果为false,则在移除节点时不移动其他节点(上面的调用传的是true,说明节点移除时可能会移动其他节点)。下面是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;
        // 与get方法一样的操作(要删除之前可不得先找到他):
        // tab赋值当前的hash表,n赋值为数组的长度,p根据hash值与数组长度减1取与操作,计算出key值在数组中的位置
        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;
            // 如果刚好p节点就是要取的值(hash一致,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);
                }
            }
            // 如果找到了节点并且满足后面的附加条件(此处matchValue = false,value = 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;
                // 再一种情况就是不是链表头,那就直接删掉这个节点,链表后面的往前移一个
                else
                    p.next = node.next;
                // 完成删除元素后修改次数+1
                ++modCount;
                // 大小减1
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

要我说,这remove()方法上面半截查找Node节点的地方直接调用getNode()方法得了,他代码其实是一样的,也不知道为什么不这么做。移除这里是单个元素(没有hash冲突)或者是链表的都没什么,如果是红黑树的话要注意一个就是:添加的时候他是添加满了8个的时候会把链表转成红黑树,删除的时候是删到6个的时候会把红黑树又转成链表。
再往下又是几个比较简单的方法,清空方法public void clear() 直接size=0内容全部置为null,看指定的值是不是存在的方法boolean containsValue(Object value),取所有key的方法Set<K> keySet() 再往下又有需要重点关注的JDK8重载的一些方法:

HashMap中重载了JDK8的一些方法

前面的几个简单的就不说了,就是调用已有的方法,换了些参数传一下而已。
重置JDK8的方法
再往下就是几个lamada风格很明显的方法了:
lamada风格的方法

如果不存在就计算 public V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction)方法

方法作用是如果在HashMap中找不到key值对应的节点,或者key对应的节点的value值为null,就把以key为key,后面的mappingFunction计算出来的值当作value保存到HashMap中去,如果找到了,就直接返回key值对应的value,看一下源代码:

    @Override
    public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
    	// 计算函数他不能为空,否则要抛异常了
        if (mappingFunction == null)
            throw new NullPointerException();
        int hash = hash(key);
        Node<K,V>[] tab; Node<K,V> first; int n, i;
        // 当数组上对应的类型是链表时,记录下遍历次数即链表长度
        int binCount = 0;
        TreeNode<K,V> t = null;
        Node<K,V> old = null;
        // 这里跟put方法类似,如果大小达到阈值或者数组为空或者数组长度为0的时候要扩容一下
        if (size > threshold || (tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 下面的一大段都是看过好几遍的代码了,他就是根据key来找Node,找到了就赋值给old这个变量
        if ((first = tab[i = (n - 1) & hash]) != null) {
            if (first instanceof TreeNode)
                old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
            else {
                Node<K,V> e = first; K k;
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) {
                        old = e;
                        break;
                    }
                    // 这里有一丢丢绕,遍历链表时如果找到了对应的节点就直接返回了节点的value值了,只有找不到的时候binCount参数才会起作用,此
                    // 时链表一定是遍历完了,所以binCount的值实际上只有要用到他的时候他才真正的起作用
                    ++binCount;
                } while ((e = e.next) != null);
            }
            V oldValue;
            // 如果根据key找到了节点,并且节点value有值,直接返回节点对应的value值
            if (old != null && (oldValue = old.value) != null) {
                afterNodeAccess(old);
                return oldValue;
            }
        }
        // 函数计算得到value值
        V v = mappingFunction.apply(key);
        if (v == null) {
            return null;
        // old != null 然后代码能走到这里来,说明已找到的节点上value值是null,这里就把计算出的value赋值到节点上
        } else if (old != null) {
            old.value = v;
            afterNodeAccess(old);
            return v;
        }
        // 代码能走到这里来说明old=null, t!=null 说明节点是个树,所以就是从这个红黑树上查找没找到
        else if (t != null)
        	// 调用树的添加方法把节点加上
            t.putTreeVal(this, tab, hash, key, v);
        // 这里t=null了,说明这里不是红黑树,那就是链表了(也有可能他就只有数组位置上有一个元素,即没有发生hash冲突)
        else {
        	// 不论是链表还是只有数组位置上有一个元素,此处相当于都是在链表头插入元素
            tab[i] = newNode(hash, key, v, first);
            // 如果链表长度到了要树化的长度,就转成红黑树
            if (binCount >= TREEIFY_THRESHOLD - 1)
                treeifyBin(tab, hash);
        }
        // 修改次数加1
        ++modCount;
        // 大小加1
        ++size;
        afterNodeInsertion(true);
        return v;
    }

总结一下,这里的几个重点:

  • 根据方法名,如果不存在就计算,这里的不存在有两层意思,一个是根据key值查找不存在,另一个是key值存在,但是value为空,这两种情况都会计算并添加;
  • 根据参数的定义Function<? super K, ? extends V> mappingFunction 这里的函数的入参类型一定是跟key一样,计算的返回值一定要跟value一样;
  • 如果不存在,这个方法实际上就跟put方法有点像,添加完成后也作了++modCount操作;
  • 如果是链表的添加,这里跟put方法不一样的是put方法是在链表末尾添加,这里是在链表头添加。
如果存在就计算 public V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction)方法

这个方法跟上面那个反着来的,如果根据key值查找存在,就计算新的value值并替换掉原来的value值,但是这里要注意的是如果计算出来的新value值为null,他要直接把这个节点删掉。看一下源代码:

	@Override
    public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        // 计算函数不能为空,否则要抛异常了
        if (remappingFunction == null)
            throw new NullPointerException();
        Node<K,V> e; V oldValue;
        int hash = hash(key);
        // 这里直接调用getNode()方法去查找
        if ((e = getNode(hash, key)) != null &&
            (oldValue = e.value) != null) {
            // 这里的函数是个BiFunction,他可以传2个入参,计算新的value值
            V v = remappingFunction.apply(key, oldValue);
            // 计算的value如果不为空就把value替换一下并返回
            if (v != null) {
                e.value = v;
                afterNodeAccess(e);
                return v;
            }
            // 狠就狠在如果计算的结果是null,他要把这个节点干掉
            else
                removeNode(hash, key, null, false, true);
        }
        return null;
    }

这里注意两点:

  • 一个是这里的函数是可以传2个参数的BiFunction BiFunction<? super K, ? super V, ? extends V> 2个参数前面一个跟key一个类型,后面一个跟value一个类型,返回值跟value一个类型;
  • 另一个是如果计算结果是null,这个节点就要被删除了。
计算方法 public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) 方法

这个计算方法算是集上面2个又方法于一体了,他是如果找不到并且计算的新value值不为null,就添加。如果找到了并且计算新value值不为null就替换,如果计算的值为null就干掉这个节点。看一下代码:

    @Override
    public V compute(K key,
                     BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        if (remappingFunction == null)
            throw new NullPointerException();
        int hash = hash(key);
        Node<K,V>[] tab; Node<K,V> first; int n, i;
        int binCount = 0;
        TreeNode<K,V> t = null;
        Node<K,V> old = null;
        // 根据key值查找节点
        if (size > threshold || (tab = table) == null ||
            (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((first = tab[i = (n - 1) & hash]) != null) {
            if (first instanceof TreeNode)
                old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
            else {
                Node<K,V> e = first; K k;
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) {
                        old = e;
                        break;
                    }
                    ++binCount;
                } while ((e = e.next) != null);
            }
        }
        V oldValue = (old == null) ? null : old.value;
        // 函数计算新的value值
        V v = remappingFunction.apply(key, oldValue);
        // 如果根据key值查找到了节点
        if (old != null) {
        	// 并且根据函数计算得出的值不为null
            if (v != null) {
            	// 把函数计算出的值赋值给查出来的节点的value值
                old.value = v;
                afterNodeAccess(old);
            }
            // 计算出的结果如果是null,干掉查出来的节点
            else
                removeNode(hash, key, null, false, true);
        }
        // 根据key查不到节点,并且函数计算出的值不为空的时候就直接用key值和函数计算出来的值来添加一个新对象到HashMap(相当于调了put()方法)
        else if (v != null) {
            if (t != null)
                t.putTreeVal(this, tab, hash, key, v);
            else {
                tab[i] = newNode(hash, key, v, first);
                if (binCount >= TREEIFY_THRESHOLD - 1)
                    treeifyBin(tab, hash);
            }
            ++modCount;
            ++size;
            afterNodeInsertion(true);
        }
        return v;
    }
合并方法 public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)方法

合并方法的逻辑是:当根据key值找到了节点时,如果节点的value值不为null,则调用函数计算出一个新的value并赋值给v;如果节点的value为空则使用第二个参数里面传进去的value并赋值给v。上面2种操作之后,如果v值不为空就把v值赋值给刚刚找到的节点的value值上;如果v值仍然为空,那没救了,把节点删掉 。如果根据key值,找不到节点,那么就看参数中传的value是不是为空,如果不为空就用参数上的key和value添加到HashMap里面去,注意这里的添加跟comput类似,如果是链表的话是插入链表头。源码如下:

    @Override
    public V merge(K key, V value,
                   BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
        if (value == null)
            throw new NullPointerException();
        if (remappingFunction == null)
            throw new NullPointerException();
        int hash = hash(key);
        Node<K,V>[] tab; Node<K,V> first; int n, i;
        int binCount = 0;
        TreeNode<K,V> t = null;
        Node<K,V> old = null;
        // 这里判断一下是不是需要扩容,如果需要就扩容一下
        if (size > threshold || (tab = table) == null ||
            (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 下面这一部分是根据key查找节点
        if ((first = tab[i = (n - 1) & hash]) != null) {
            if (first instanceof TreeNode)
                old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
            else {
                Node<K,V> e = first; K k;
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) {
                        old = e;
                        break;
                    }
                    ++binCount;
                } while ((e = e.next) != null);
            }
        }
        // 如果查到了
        if (old != null) {
            V v;
            // 并且节点的value不为空
            if (old.value != null)
            	// 计算新的value值
                v = remappingFunction.apply(old.value, value);
            // 否则取参数中传的value值
            else
                v = value;
            // 上面的2种情况下value如果不为空就把最终得出的结果赋值给节点的value
            if (v != null) {
                old.value = v;
                afterNodeAccess(old);
            }
            // 如果上面2种情况下还是为空那就没救了,把找到的节点干掉
            else
                removeNode(hash, key, null, false, true);
            return v;
        }
        // 如果找不到节点,并且参数里面传的value不为空,就添加节点
        if (value != null) {
            if (t != null)
                t.putTreeVal(this, tab, hash, key, value);
            else {
                tab[i] = newNode(hash, key, value, first);
                if (binCount >= TREEIFY_THRESHOLD - 1)
                    treeifyBin(tab, hash);
            }
            ++modCount;
            ++size;
            afterNodeInsertion(true);
        }
        return value;
    }
增强for循环 public void forEach(BiConsumer<? super K, ? super V> action) 方法

这是对Map的增强for循环,这里有两个点要注意,一个是参数是函数BiConsumer,他传2个参数分别是key类型的和value类型的;另一个是在for循环之后,做了modCount是否有变化的判断,如果在for循环中有对HashMap作添加、移除、计算、合并等会影响modCount值的操作这里就会直接抛出ConcurrentModificationException异常来快速失败。看代码:

    @Override
    public void forEach(BiConsumer<? super K, ? super V> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e.key, e.value);
            }
            // 看看modCount是否被改动了,一旦被改动了就抛出ConcurrentModificationException异常
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值