util包源码(二):HashMap源码笔记

阅读时间:2019.1.11–1.17
java版本:1.8

一、概述

在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
在这里插入图片描述
在JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值8时,将链表转换为红黑树,这样大大减少了查找时间
在这里插入图片描述

二、关键属性

transient Node<K,V>[] table;//hash桶数组
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;//修改次数
int threshold;//临界值   当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量
final float loadFactor;//加载因子

三、节点Node

1.节点Node

HashTable中的节点Node<K, V>,每个hash桶都是这些Node的一个单向链表。是一个Map.Entry<K,V>的实现,所以可以用for(Map.Entry<K,V> e:map)来遍历所有的Node

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
    //设置新的value 同时返回旧value
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    //判断两个node是否相等,若key和value都相等,返回true。可以与自身比较为true
    public final boolean equals(Object o) {
        if (o == this)//自己和自己比较,一定是ture
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;//key等,value也相等,返回true
        }
        return false;
    }
}
2.hash算法

(1)首先获取对象的hashCode()值,然后将hashCode值右移16位,然后将右移后的值与原来的hashCode做异或运算,返回结果。(其中h>>>16,在JDK1.8中,优化了高位运算的算法,使用了零扩展,无论正数还是负数,都在高位插入0)。key.hashCode()是调用C的算法

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
3.resize()扩容算法

1).在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;
2).每次扩展的时候,都是扩展2倍;
3).扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。

方法分为两个步骤:
第一步:确定新表的容积newCap,新表的临界值newThr
1.如果oldCap旧容量>0,来确定新table的临界值;如果oldCap*2<最大值&&oldCap>默认值16扩容一倍。
2.如果oldThr旧临界值>0,(即此时是用map来初始化新表,不进行扩容)
3.其他情况:newCap新容量=16,新临界值=0.75(加载系数)*16(默认容量)

第二步:扩容:遍历所有节点
1.节点为叶子结点e.next == null,直接把e放入对应的hash桶中
2.节点是红黑树节点,(e instanceof TreeNode),通过红黑树的方法拆分并挂到新的table桶上
3.其他节点:依据(e.hash & oldCap) 是否等于0,把原hash桶内的链表拆分成两个链表loHead和hiHead
loHead放在原hash桶里,hiHead放到扩容后的对称的hash桶里

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;//oldTab指向hash桶数组
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    ---//第一步
    if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空
        if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值
            threshold = Integer.MAX_VALUE;
            return oldTab;//不再进行扩容,返回
        }else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 
                   oldCap >= DEFAULT_INITIAL_CAPACITY){
         //如果当前hash桶数组的长度在2倍扩容后小于最大容量 && oldCap>默认值16
            newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold
        }
    }else if (oldThr > 0){ // initial capacity was placed in threshold
        newCap = oldThr;
    }else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    ---//用map初始化时(上面的else if),需要计算出表的对应临界值
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    ---
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//新建hash桶数组
    table = newTab;//将新数组的值复制给旧的hash桶数组
    ---//第二步
    if (oldTab != null) {//进行扩容操作,复制Node对象值到新的hash桶数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {//如果旧的hash桶数组在j结点处不为空,复制给e
                oldTab[j] = null;//将旧的hash桶数组在j结点处设置为空,方便gc
                if (e.next == null)//如果e后面没有Node结点
                    newTab[e.hash & (newCap - 1)] = e;//直接对e的hash值对新的数组长度求模获得存储位置
                else if (e instanceof TreeNode)//如果e是红黑树的类型,那么添加到红黑树中
                    ((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 {//做一个循环,把桶内数据根据(e.hash & oldCap) == 0分到两个链表里,
                        //一个以loHead为头节点,一个以hiHead为头节点
                        next = e.next;//将Node结点的next赋值给next
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //把loHead链表插到原hash桶里,把hiHead链表插到扩容后对称的桶里
                    if (loTail != null) {//如果loTail不为空
                        loTail.next = null;//将loTail.next设置为空
                        newTab[j] = loHead;//将loHead赋值给新的hash桶数组[j]处
                    }
                    if (hiTail != null) {//如果hiTail不为空
                        hiTail.next = null;//将hiTail.next赋值为空
                        newTab[j + oldCap] = hiHead;//将hiHead赋值给新的hash桶数组[j+旧hash桶数组长度]
                    }
                }
            }
        }
    }
    return newTab;
}

四、初始化

初始化构造方法有四种:
HashMap(int initialCapacity, float loadFactor) 自定义容量,和加载系数loadFactor
HashMap(int initialCapacity)自定义容量,用加载系数loadFactor=0.75f
HashMap()默认初始容量16,加载系数0.75
HashMap(Map<? extends K, ? extends V> m) 默认初始容量16,加载系数0.75,并用于个map初始化自己

//自定义的
public HashMap(int initialCapacity, float loadFactor) {//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);//获取该容量下的临界值
}
//返回cap最大的2的n次方,例如cap=100时,返回128;当cap=1000时,返回1024;
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;
}

用map来初始化时。因为传入的oldMap的临界值等会与要创建的newMap不一样,所以在不能直接复制,
而是遍历oldMap,按照新的临界值插入新的newMap

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        if (table == null) { // pre-size,hash表为空,计算容量及加载系数
            float ft = ((float)s / loadFactor) + 1.0F;//MAXIMUM_CAPACITY=1<<30
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                threshold = tableSizeFor(t);
        }else if (s > threshold)//如果传入map的size>目前的临界点,重新
            resize();//重新计算临界点等,基本逻辑:
            //(1)如果目前oldTab==null,计算一下临界值和加载系数,给newTab就好了;
            //(2)如果目前oldTab!=null(覆盖旧表),要根据新的临界值,遍历所有newTab节点,把
            //	  把旧表删掉,按新的临界值挂节点
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);//见五。重要方法的“增”类方法
        }
    }
}

五、重要方法

主要public方法

//功能性
public boolean containsKey(Object key)
public boolean containsValue(Object value)
//增
public V put(K key, V value) 
public void putAll(Map<? extends K, ? extends V> m)
public V putIfAbsent(K key, V value)
//删
public V remove(Object key)
public void clear()
public boolean remove(Object key, Object value)
//改
public boolean replace(K key, V oldValue, V newValue)
public V replace(K key, V value)
//查
public V get(Object key)
public V getOrDefault(Object key, V defaultValue)
//函数式
public V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction) 
public V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction)
public V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction)
public V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remappingFunction)
public void forEach(BiConsumer<? super K, ? super V> action)
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
1.功能性方法

containsKey(Object key)是否存在key,getNode方法在其他方法中也多次使用

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

final Node<K,V> getNode(int hash, Object key) {
     Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
     //(表不空&&长度>0) && (key的hash所得到的目标hash桶不为空)
     if ((tab = table) != null && (n = tab.length) > 0 &&
         (first = tab[(n - 1) & hash]) != null) {
         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 {//不是红黑树,是链队列,就遍历找是否有对应的key
                 if (e.hash == hash &&
                     ((k = e.key) == key || (key != null && key.equals(k))))
                     return e;
             } while ((e = e.next) != null);
         }
     }
     return null;
 }

containsValue(Object value)是否存在值,两层循环,外层循环hash表,内层循环遍历每个hash桶

public boolean containsValue(Object value) {
    Node<K,V>[] tab; V v;
    if ((tab = table) != null && size > 0) {
        for (int i = 0; i < tab.length; ++i) {
            for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                if ((v = e.value) == value || (value != null && value.equals(v)))
                    return true;
            }
        }
    }
    return false;
}
2.“增”类方法

V put(K key, V value),最常用的方法。putVal四个地方会用到:put,putIfAbsent,putMapEntries,readObject
putVal分两步:
第一步:插值
如果通过hashMap刚建立,hashtable还是空的,就新建一个hashtable;如果有则下一步插值
插值要区分情况:
1.如果key所对应的hash桶还是空的,就创建新节点,并把它作为该桶的第一个节点
2.否则,按下面的各种情况分别插值:
(1)桶的第一个节点就是应该插入的地方,就把桶第一个节点替换掉
(2)如果桶里是红黑树(桶内已有超过8个节点),按红黑树插入putTreeVal
(3)如果桶里是链表(桶内已有1~7个节点):
如果有相同key的节点,用新节点替换掉;如果没有,就加上新节点;还要判断桶内节点数是否到8,如果到8了,转化成红黑数treeifyBin

第二步:扩容(如果需要)
如果是替换掉了旧节点,不需要扩容,返回旧节点即可;
如果是新加节点,如果hashMap的size超过了临界值,则进行翻倍扩容resize()

treeifyBin非常精妙,他不会对所有超过8的hash桶进行转换,而是会根据哈希表的容量采用两种方式:
第一种:当容量<64(4*16)时,只扩容。默认初始化容量为16,所以当第一次和第二次触发红黑树转化时,会先进行2倍容量扩容:即16->32,32->64。扩容时,会把桶内的链表拆成两个,这样就不超长了
第二种:当容量已经>64时,才会真正的转化成红黑树

public V put(K key, V value) {
	//参数4:如果是true则仅仅做查询不插入
	//参数5:如果是false,指该方法是在创建hash时被调用
    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;
    //第一步
    if ((tab = table) == null || (n = tab.length) == 0)//hashtable还没建起来,就建起来
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)//如果新节点的key对应的桶为空,就新建一个桶和节点
        tab[i] = newNode(hash, key, value, null);
    else {//对应的桶已经有值
        Node<K,V> e; K k;
        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 {
            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;
            }
        }
        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;
}
//表转红黑树方法,在有添加entry的方法中均会调用
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();//table为空,或者table长度<64(4*16)为什么?
        //因为刚创建table时,是16,但如果一个桶内有至少8个元素了,就要进行扩容
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> 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);//真正的转化成一颗红黑树,返回根节点,在红黑树模块再解读
    }
}

void putAll(Map<? extends K, ? extends V> m)
把参数m里所有元素复制进当前的hashmap,如果新map与旧map有重复的key,就用新map替换。
例如原:mapA={(1:a),(2:b)},新的mapB={(1:k),(3:c)},最终结果:{(1:k),(2:b),(3:c)}

public void putAll(Map<? extends K, ? extends V> m) {
    putMapEntries(m, true);//见初始化时的,用另一个map初始化当前map的构造函数
}

V putIfAbsent(K key, V value) 当且仅当key不存在时,才会把value插入到对应的key中。
put方法是,当key存在时,会用新值覆盖旧值。putIfAbsent核心方法与put相同,只是参数不同

public V putIfAbsent(K key, V value) {
    return putVal(hash(key), key, value, true, true);
}
3.“删”类方法

boolean返回值的remove既要检查key,也要检查value,都相等时才会移除
V返回值的remove返回key对应的value,同时移除该对象

public boolean remove(Object key, Object value) {
    return removeNode(hash(key), key, value, true, true) != null;
}
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}
public void clear() {
    Node<K,V>[] tab;
    modCount++;
    if ((tab = table) != null && size > 0) {
        size = 0;
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}

核心方法removeNode
第三个参数:比较key同时比较value是否与传入参数相同
第四个参数:是否比较value
第五个参数:删除元素后,是否移动其他节点(只有当桶内是红黑树时才会生效)
分为两个步骤:
第一步:找到要删除的节点,与putVal类似,找到的节点为note
先找到对应的hash桶,再根据情况
1.如果桶内头节点即是要找的key,则取出该节点node=p
2.如果桶内是个红黑树,用红黑树的方法找对应的树节点
3.如果桶内是链表,遍历去找
第二步:删除对应的节点node
1.如果node是红黑树节点,用红黑树方法移除节点removeTreeNode
2.如果node是hash桶的头节点,要把头节点指向它的next,然后再删除操作
3.其他情况,直接删除node

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) {//先找到对应的hash桶
       ---//第一步
       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;
       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;
           else
               p.next = node.next;
           ++modCount;
           --size;
           afterNodeRemoval(node);
           return node;
       }
   }
   return null;
}
4.“改”类方法

这两个方法类似remove
boolean返回值的remove既要检查key,也要检查value,都相等时才会用新value替换旧value
V返回值的remove返回key对应的旧value,同时把旧value替换成新value
不再复述

public boolean replace(K key, V oldValue, V newValue) {
    Node<K,V> e; V v;
    if ((e = getNode(hash(key), key)) != null &&
        ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
        e.value = newValue;
        afterNodeAccess(e);
        return true;
    }
    return false;
}
public V replace(K key, V value) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) != null) {
        V oldValue = e.value;
        e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    return null;
}
5.“查”类方法

核心方法getNode,在功能类方法中已经做了剖析

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
public V getOrDefault(Object key, V defaultValue) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
6.函数式方法
(1)compute / computeIfAbsent / computeIfPresent

下面三个方法都可以给map新增一个<key,value>的entry,主要区别是:
compute:不管原key是否存在,都直接覆盖旧的或者创建新的(二元函数式)
computeIfAbsent:当且仅当原key不存在时,才会新增;原key存在时,不会覆盖旧值(一元函数式)
computeIfPresent:当原key存在,并且对应value=null时,对该key的覆盖失效(二元函数式)

compute,接收的是BiFunction参数,这是二元函数接口,所需调用时要形如:
mapA.compute(6, (k,v) -> v=“6”);
整体逻辑过程类似putVal,主要区别在,把第一步插值再细拆成了两步,所以一共三步
第一步:先遍历hash表,找到key所对应的位置,旧值赋为oldValue
第二步:执行函数式方法中的方法,即给value赋值,v是新的value
第三步,做覆盖或者新建节点的操作

简化后的伪代码:

public V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    Node<K,V> old = null;
    TreeNode<K,V> t = null;
    ---//第一步,先遍历hash表,找到key所对应的位置,旧节点old
    //省略遍历部分代码,类似putVal
	---//第二步,执行函数式方法中的方法,即给value赋值,v是新的value    
    V oldValue = (old == null) ? null : old.value;
    V v = remappingFunction.apply(key, oldValue);
    ---//第三步,做覆盖或者新建节点的操作
    if (old != null) {//旧节点不为空,要做覆盖操作
        if (v != null) {//新value不为空,做覆盖赋值
            old.value = v;
            afterNodeAccess(old);
        }else//新节点为空,不仅不覆盖,而且要把原节点删去
            removeNode(hash, key, null, false, true);
    }else if (v != null) {//旧节点为空,且新value不空
        if (t != null)//t是桶内的红黑树,(如果有的话)
            t.putTreeVal(this, tab, hash, key, v);
        else {//不是红黑树,插链表,如果桶内节点超过8个,转化为红黑树
            tab[i] = newNode(hash, key, v, first);
            if (binCount >= TREEIFY_THRESHOLD - 1)
                treeifyBin(tab, hash);
        }
        ++modCount;
        ++size;
        afterNodeInsertion(true);
    }
    return v;
}

computeIfPresent:接收的是BiFunction参数,这是二元函数接口,所需调用时要形如:
mapA.computeIfPresent(6, (k,v) -> v=“6”);
当且仅当原key不存在时,才会新增;原key存在时,不会覆盖旧值。所以比comput方法略简单
分为两个步骤:
第一步:找到key对应的节点,并且原value为null,如果不符合条件,直接返回null
第二步:执行函数式方法,如果新的value是null,删除原节点,如果不为null,覆盖原值

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);
    if ((e = getNode(hash, key)) != null &&
        (oldValue = e.value) != null) {
        V v = remappingFunction.apply(key, oldValue);
        if (v != null) {
            e.value = v;
            afterNodeAccess(e);
            return v;
        }
        else
            removeNode(hash, key, null, false, true);
    }
    return null;
}

computeIfAbsent接收的是Function参数,这是一元函数接口,所需调用时要形如:
mapA.computeIfPresent(6, k->“6”);
与compute代码几乎一样,只在一个地方有区别,即在第三步覆盖原节点时。

 public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction){
 	---//第一步,省略代码
 	---//第二步,省略代码
 	---//第三步
     V v = mappingFunction.apply(key);//取出新的value后
     if (v == null) {//如果新的value等于空,直接返回空;compute中是删除节点
         return null;
     } else if (old != null) {
         old.value = v;
         afterNodeAccess(old);
         return v;
     }
     /* comput对应代码
      if (v != null) {//新value不为空,做覆盖赋值
          old.value = v;
          afterNodeAccess(old);
      }else//新节点为空,不仅不覆盖,而且要把原节点删去
          removeNode(hash, key, null, false, true);
	*/
	---//剩余代码省略
 }
(2)merge

功能大部分与compute相同,不同之处在于BiFunction中apply的参数,入参为oldValue、value,调用merge时根据两个value进行逻辑处理并返回value
注意到,merge方法接收四个参数但是它和compute一样,接收的是BiFunction方法。
例如 mapA.merge(4, “4”, (k,v) -> (v==“5”)?“6”:null);(k=4,v=“4”);如果当前map中key=4的值(oldValue)是否等于“5”来执行后续的操作。(本例中,如果key=4对应的value!=“5”,则删除key=4的entry
差别在第二步:(compute的疑问:所以为什么不用Function接口?使用一个参数就可以阿?)
第一步:先遍历hash表,找到key所对应的位置,旧值赋为oldValue
第二步:执行函数式方法中的方法,即给value赋值,v是新的value
第三步,做覆盖或者新建节点的操作

 public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
	---//第一步,省略代码,先遍历hash表,找到key所对应的位置,旧值赋为oldValue
 	---//第二步,省略代码,
	if (old.value != null)
		//执行函数式方法中的方法,根据传入的函数式方法,判定旧值和新值(第二个参数V value)的逻辑关系
		v = remappingFunction.apply(old.value, value);
	/* compute中的对应处理,实际上由于compute只有三个参数,所以为什么不用Function接口?
	 V v = remappingFunction.apply(key, oldValue);
	*/
	---//剩余代码省略
(3)forEach

forEach的函数式方法BiConsumer同compute,是一个二元函数,较简单

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;
        //外层循环遍历hashtable,内层循环遍历hash桶
        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);
        }
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}
(4)replaceAll

replaceAll同arrayList的replaceAll功能,都是通过一个函数式方法,对所有value处理,后覆盖回去。
二元函数BiFunction。代码与forEach只有一个差距,就是调用完函数方法后,是否把原值覆盖。

public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
	//省略代码
	//与forEach的区别在此,是否把计算后的新value覆盖原值
    e.value = function.apply(e.key, e.value);
    //省略代码
}

六、内部类

Hashmap的内部类包括三种
1.容器类:虽然是容器类,但不是真实的容器,核心是迭代器,指向的仍然是HashTable中的对象
KeySet,Values,EntrySet
2.迭代器:包括KeyIterator,ValueIterator,EntryIterator,以上三种均实现虚拟类HashIterator
3.并行迭代器:包括KeySpliterator,ValueSpliterator,EntrySpliterator 。以上三种均实现虚拟类HashMapSpliterator
4.红黑树:放到第七章详细讲
entry最复杂,故按entry进行解读

1.EntrySet

public方法,主要是用于管理两个迭代器,没有太难理解的地方,就不解读了

public final int size()
public final void clear()
public final boolean contains(Object o)
public final boolean remove(Object o)
public final Iterator<Map.Entry<K,V>> iterator()//新建一个EntryIterator
public final Spliterator<Map.Entry<K,V>> spliterator()//新建一个EntrySpliterator
public final void forEach(Consumer<? super Map.Entry<K,V>> action)
public boolean equals(Object o)
public int hashCode()
public boolean removeAll(Collection<?> c)//用迭代器移除所有entry
2.HashIterator

常规迭代器,没有什么特殊的

3.EntrySpliterator

并行迭代器,与arrayList类似主要提供三种服务
trySplit:把原HashMap拆成两个Map,分别用一个EntrySpliterator管理
tryAdvance:对单个entry执行处理,如果有剩下元素未处理返回true,否则返回false
forEachRemaining:对剩余的entry进行处理

public EntrySpliterator<K,V> trySplit()
public void forEachRemaining(Consumer<? super Map.Entry<K,V>> action)
public boolean tryAdvance(Consumer<? super Map.Entry<K,V>> action)

七、测试

public static void main(String[] args) {
	HashMap<Integer,String> initMap = new HashMap<Integer,String>();
	for(int i=0;i<5;i++) {
		initMap.put(i, String.valueOf(i));
	}		
	
	//1.用map初始化
	System.out.println("测试组1:测试用map初始化,");
	HashMap<Integer,String> mapA = new HashMap<Integer,String>(initMap);
	mapA = new HashMap<Integer,String>(initMap);
	mapA.forEach((k,v)->System.out.println("    key = " + k + ", value = " + v));
	System.out.println(mapA.size());
	
	mapA.put(7, null);//添加一个entry<7,null>作为测试
	
	//2.测试computeIfAbsent,当原key已经存在时,不会覆盖旧的
	System.out.println("测试组2:computeIfAbsent,添加一个未存在key的entry:<5,‘5’>,一个已存在key的entry:<3,'13'>");
	System.out.println("一个未存在key的entry:<5,‘5’>添加成功,一个已存在key的entry:<3,'13'>添加不成功");
	mapA.computeIfAbsent(5, k->"5"); //新的key:新的value  --->新建节点  成功
	mapA.computeIfAbsent(3, k->"13");//旧的key:新的value  --->覆盖节点  失败
	mapA.computeIfAbsent(4, k->null);//旧的key:新value=null   --->不会删除原节点
	mapA.forEach((k,v)->System.out.println("    key = " + k + ", value = " + v));
	
	//3.测试compute,不管原key:value什么样,都直接覆盖旧的或者创建新的
	System.out.println("测试组3:compute,添加一个未存在key的entry:<6,‘6’>,一个已存在key的entry:<3,'13'>,一个原value=null的entry");
	System.out.println("三个entry均添加成功,不管原key:value什么样,都直接覆盖旧的或者创建新的");
	mapA.replace(3, "3");
	mapA.replace(7, null);
	
	mapA.compute(6, (k,v) -> v="6");  //新的key:新的value  --->新建节点  成功
	mapA.compute(3, (k,v) -> v="13"); //旧的key:新的value  --->覆盖节点  失败
	mapA.compute(7, (k,v) -> v="7");  //旧的key:旧value=null  --->覆盖节点  失败
	mapA.compute(4, (k,v) -> v=null); //旧的key:新value=null --->删除原来的节点
	mapA.forEach((k,v)->System.out.println("    key = " + k + ", value = " + v));
	
	//4.测试computeIfPresent
	System.out.println("测试组4:computeIfPresent,添加一个未存在key的entry:<8,‘8’>,一个已存在key的entry:<3,'13'>,一个原value=null的entry");
	System.out.println("原value=null时,会失败");		
	mapA.replace(3, "3");		
	mapA.replace(7, null);
	mapA.put(4, "4");
	mapA.computeIfPresent(8, (k,v) -> v="8"); //新的key:新的value     --->新建节点  失败
	mapA.computeIfPresent(7, (k,v) -> v="7"); //旧的key:原value=null  --->覆盖节点  失败
	mapA.computeIfPresent(3, (k,v) -> v="13");//旧的key:新的value      --->覆盖节点  成功
	mapA.computeIfPresent(4, (k,v) -> v=null);//旧的key:新value=null      --->删除原来的节点
	mapA.forEach((k,v)->System.out.println("    key = " + k + ", value = " + v));
	
	
	//5.测试merge
	System.out.println("测试组5:merge,修改key=4的entry");	
	mapA.replace(3, "3");		
	mapA.replace(7, null);
	mapA.put(4, "4");
	mapA.merge(4, "6", (key,newV) -> ("4"==newV)?"4":newV);
	mapA.forEach((k,v)->System.out.println("    key = " + k + ", value = " + v));
	
	//6.测试replaceAll
	System.out.println("测试组6:replaceAll,用函数方法处理所有的entry,再覆盖回原map");	
	mapA.replace(3, "3");		
	mapA.replace(7, null);
	mapA.put(4, "4");
	mapA.replaceAll((k,v) -> v+v);
	mapA.forEach((k,v)->System.out.println("    key = " + k + ", value = " + v));
	
	//7.测试Spliterator
	System.out.println("测试组7:Spliterator,拆分原map");	
	Spliterator<Map.Entry<Integer,String>> split = mapA.entrySet().spliterator();
	Spliterator<Map.Entry<Integer,String>> mapB = split.trySplit();
	System.out.println("分割后前一半:");	
	split.forEachRemaining(e->System.out.println("    key = " + e.getKey() + ", value = " + e.getKey()));
	System.out.println("分割后后一半:");
	mapB.forEachRemaining(e->System.out.println("    key = " + e.getKey() + ", value = " + e.getKey()));
					
}

八、总结

1.HashMap线程不安全
2.默认初始化设置:hashTable的值为16,加载系数0.75
3.如果设置hashTable值,则实际hashTable的容量是大于设置值的最小二进制数。
例如,传参19,实际32
4.扩容后,新hashtable是原来的2倍,并且会把原桶内拆分成两个链表hi和lo,
lo放在当前桶里,hi放在扩容后对称的那个桶里(tab[j+oldCap] )
5.当某个桶内元素达到8个时,不是直接转成红黑树,而是先尝试把hashtable扩容到64,从而减少每个桶里的元素;当已经扩容到64后,才会转化成红黑树

红黑树的部分待之后结合TreeMap源码

猜你喜欢

转载自blog.csdn.net/jh19900712/article/details/86497459