HashMap 源码阅读

声明:本hashmap源码阅读主要基于小刘老师的B站源码视频,地址为:https://www.bilibili.com/video/BV1LJ411W7dP?p=8&spm_id_from=pageDriver。通过结合视频与源码的阅读形成本文档。

1. hashmap介绍

hashmap是通过存储key与value的方式存储数据的,其中Key通过hash操作而不是直接存储。文本博客首先介绍hashmap 的存储结构,介绍主要的方式的执行了流程以及源码讲解。

1.1 hashmap基本信息

先介绍hashmap的继承与实现关系,可以看到继承抽象map类,同时实现了map接口。

在这里插入图片描述

接口map规定了实现的map类需要完成的功能,意味着实现类在该方法上会因为存储结构和操作特点存在区别。下面为map接口的方法。这些方法也是map中常用的方法,这也体现了java的多态的特点,即我们只需要关心map有什么方法,能实现什么功能,从而屏幕不同的实现map的底层差异。

1.2 hashmap存储关系介绍

下图的map中,存在内部类entry,内部类拥有获取key、value等的方法,该类是map存储的核心数据结构。jdk中不存在map的数据结构,因此采用内部类(Entry)+数据的方式存储map数据。具体细节会在下面描述。
在这里插入图片描述

1.3 hashmap继承、实现关系介绍

下面为AbstractMap的方法,hashmap继承了AbstractMap,实现了基本map操作方法。当集成的方式重写了后就就会覆盖原方法。具体方法会在下面介绍。

在这里插入图片描述
在这里插入图片描述

2. hashmap存储数据结构

hashmap的存储采用散链表,即数组和链表的组合,通过链表的方式存储相同节点的元素,当量表大于8时,会变为红黑树(红黑树的特点的特点是平衡,因此可以提高查询速度)。

2.1 hashmap存储方式

hashmap存储采用散链表的方式,即通过数据+链表的方式存储元素。散链表结合了数组的查询快速以及链表的新增、删减方便的优势。图中每一个圆圈是一个存储单元,存储单元之间采用链表存储,虚线包裹的部分为数据部分,通过hash取余的方式可以快速找到对应位置。如下图将输入的key通过hash运算(算法不唯一,但具备幂等性)获取对应计算数值,通过对8取余,得到对应的存储位置,如果该位置存在多个元素,则将元素插入链表中,jdk1.8采用尾插法,1.7采用头插法,方法的优劣会在put方法分析。jdk1.8后,存储满足一定条件之后会转为红黑树,细节会在put里讨论。

在这里插入图片描述

2.2 hashmap存储单元

可以看出,hashmap的存储对象为Node类,该类实现了map.Entry这个内部类。查看该类的属性和构造器如下,

// hash与key都是通过final修饰,表名在一个hashmap存储中key是不可变的,hashmap存储采用散链表额方式,可以得出hash和key都是存储时需要采用的存储的方式。
final int hash;  // hash值 寻找数组元素是需要用到
final K key;  // 存储的key值  保证存储的唯一性。
// value 顾名思义是map单个元素的value
V value;
// 由于采用散链表 存储下一个元素的地址
Node<K,V> next;
//  注意node的构造器只有这一个,因此不能通过new Node()构造出对象后再赋值。
Node(int hash, K key, V value, Node<K,V> next) {
    this.hash = hash;
    this.key = key;
    this.value = value;
    this.next = next;
}

在这里插入图片描述

2.3 HashMap属性介绍

hashmap的属性与常量有以下几个:

// 静态final通过名字可得,这个是默认初始容量,1<<4 = 2^4 = 16。hashmap的默认容量为16.
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//通过名称得,该数据为最大容量大小,容量为2的30次方 = 1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;

// 扩展系数 0.75 作为扩容阈值
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 翻译过来为树化阈值,即从链表转为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;

// 翻译过来为树化阈值,即小于该值从树结构变为链表
static final int UNTREEIFY_THRESHOLD = 6;

// 最小树化容量, 数组到达64后 才可以链表转树, 未达到是只采用扩容。
static final int MIN_TREEIFY_CAPACITY = 64;

// hashmap的存储结构核心,每一条记录都是一个node
transient Node<K,V>[] table;

transient Set<Map.Entry<K,V>> entrySet;
// 当前hashmap中的元素个数
transient int size;

// 当前hashmap结构修改次数,也可以理解为操作次数也就是删除和天极爱的次数
transient int modCount;

// 负载因子  threshold = capacity * loadFactor
int threshold;

// 扩容阈值
final float loadFactor;

小结:hashmap 的属性包含了存储结构为Node数组,存在与阈值计算的系数,树化的阈值以及反树化的阈值。同时通过树化的条件可以知道树化只有满足数组长度大于64且单个链表大于8的时候才能树化。

hashmap的构造器有4个

// 此时的来到了新建map的最后一步,生成对应的数据结构。
    public HashMap(int initialCapacity, float loadFactor) {
        // 安全验证,判断是传入的容量是否存在问题。
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        // 安全验证,当输入的容量大于最大容量时,采用采用最大容量 MAXIMUM_CAPACITY = 1073741824
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // 安全验证,判断传入的loadfactor是否合法
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        // 生成容量 下面会详细介绍
        this.threshold = tableSizeFor(initialCapacity);
    }

   // 构造数据时包含一个字符型参数,该参数表示初始容量,同时采用默认loadFactor=0.75,采用this函数表示向上调用下一个构造器。
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

   // 无参构造器,会将获取默认loadFactor = 0.75 
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

   // 输入一个现有的map 获取该map。
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        // 生成map 下面会详细介绍。
        putMapEntries(m, false);
    }

其中带有容量大小的构造器会调用tableSizeFor()方法,该方法将数字转为大一点的最大二进制,代码如下:

static final int tableSizeFor(int cap) { // 假设传入12 , 12 = 1100B
    int n = cap - 1; // n = 11
    // n >>> 1无符号右移 与n取或运算(全零为0,有1为1)
    n |= n >>> 1;  // n >>> 1 = 110B  n变为1110B
    n |= n >>> 2;  // n >>> 2 = 11B n变为1111B
    n |= n >>> 4;  // n >>> 4 = 0B 
    n |= n >>> 8;
    n |= n >>> 16;
    // 以上移动为1、2、4、8、16,通过1的数量翻倍最终使得所有的数据都是1.如12 = 1100B就变成了1111B = 15.
    // 当n合法,而且不大于最大容量时,返回n+1 = 15 + 1 = 16.因此当设定容量为12是,生成的容量大小为16。
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

传入的参数为map时调用putMapEntries()方法,该方法如下:

public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    // 采用默认的阈值
        putMapEntries(m, false);
    }

// 移动元素由参数中的evict可得这个方法还有别的入口,同时该方法将参数传递给putVal。
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        // 对于初始化map来说,s > 0,并且当前table为空。则在初始化阶段会执行table == null为true的语句。
        if (table == null) { 
            // 概述:通过原本的hashmap的size计算threshold。
            /*
            	我们聊聊当前的复制map的现状,传入的map可能拥有任意多的数量,不一定是2的倍数,所以我们需要找到map中的阈值和容量。
            	阈值是容量0.75倍,因此需要计算阈值。
            	例如原始map的总容量为32,则阈值为24。
            		当输入的元素小于24时,此时阈值为24,容量为32。
            		当输入的元素大于24时,此事阈值为64*0.75 = 48,容量为64。
            	因此不能通过直接向上取2进制的方式获取最大容量。
            	*/
            // 通过反向计算当前数量可能存在的区间,
            //	比如23计算为 23 / 0.75 + 1 = 31.6 向上取2进制数为32
            //	比如23计算为 24 / 0.75 + 1 = 33   向上取2进制数为64
            //	比如23计算为 25 / 0.75 + 1 = 34.3 向上取2进制数为64
            // 正如上面提到的 通过map中现有的数量倒推整个map中 最大值与阈值的信息。
            float ft = ((float)s / loadFactor) + 1.0F;
            // 安全验证不大于最大值。
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
            // 向上取2进制数
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
        else if (s > threshold)
            resize();
        // 可得出该位置是遍历移动元素,
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            // @param evict if false, the table is in creation mode.
            // evict = false表名是创建模式,也就是map的初始化阶段。
            putVal(hash(key), key, value, false, evict);
        }
    }
}

在这里插入图片描述

以上就是hashmap 的主要属性与构造器的解释了,我们总结一下:

  • hashmap存储的方式为node数组,通过链表的方式解决hash冲突。

  • 一个hashmap必须知道正确的最大容量、扩容阈值,

  • hashmap每次扩容,数组增加一倍。上线为2的30次方。

  • hashmap的数组容量为2的倍数,扩容阈值为最大阈值的0.75倍。

3. hashmap方法介绍

在hashmap介绍中说道,实现了map接口,根据不同的存储特点与使用特点实现不同的方法,因此探索源码的主要从map中的的方法开始阅读源码。通过数据hashmap的存储结构可知,hashmap中put方法会涉及比较多的内容,同时需要了解hashmap中hash计算的方式。从上面的构造器也可知,map要时刻处理好阈值与容量的关系,因此我们需要了解扩容

3.1 put 与 putAll

put()方法直接调用了putVal()

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

这里用到hash方法 我们先看看hash是如何实现的

 static final int hash(Object key) {
        int h;
     // (h = key.hashCode()) ^ (h >>> 16) 将高16位与低16位做异或计算,这可以充分利用数据特点。 同时不传入key时候,这之后hash值为0。
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

因此put方法是调用putVal方法

 	 //第一个实参为hash(key)
    // 第二个实参为key、
    // 第三个实参为value,
   //  第四个实参为false  这个形参的的名称指onlyIfAbsent,英文解释if true, don't change existing value 在putIfAbsent用到了,之指的是如果存在重复的key则不替换。
    // 第五个实参为true

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    // tab 值当前存储的单元
    // p 为当前头元素
    // n 数组长度
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 当前的数组为空 或者没有元素 需要初始化。 注意这里的写法(tab = table) == null 无论是否成功,此时的tab就指向了table。
        if ((tab = table) == null || (n = tab.length) == 0)
            //  此时表名 map里面的table没有初始化,也就table里面啥都没有  -> 这里是延迟初始化
            n = (tab = resize()).length;
    // (n - 1) & hash 这个是干掉高位 也就是对数组取模。
    // 此时的p为数组的头元素。
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 头元素为空,证明此时的链表还没有创建。把当前元素封装成为node就可以。
            tab[i] = newNode(hash, key, value, null);
        else {
            // 此时有数据 可能是链表也可能是红黑树。
            // 声明节点元素
            Node<K,V> e; K k;
            // p上面获取的头元素,确认当前的hash与当前元素的hash是同一个值,判断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 {
                // 判断结果为链表 遍历链表 找到想要是否存在相同的key值,
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {// 遍历到最后一个节点了  没有相同的key则新建。并插入
                        p.next = newNode(hash, key, value, null);
                        
                      // 每次Bincount自增1 大于8-1 = 7 时候树化,记住这里是从0开始计算的,进入if如果成立表名之前的循环执行了8次,所以是插入前是8个元素了,插入之后是9个。注意如果是第一次树化,前面有8个元素,加上刚才插入的应该是9个元素树化(注意bincount的增加时机,是循环结束时候) (说实话 本人这对这个9个元素再树化不太理解,二叉树的平衡结构是7个元素,那为什么不在恰好排满3层时候直接树化)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 树化
                            treeifyBin(tab, hash);
                       	// 找到最后一位而且不足8个。
                        break;
                    }
                    // 找到元素
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 经过循环的结果的e有可能是最后一个元素也可能是目标值。
            if (e != null) { // 如果成立说明之前存在key相同的元素
                V oldValue = e.value;
                // 判断是否覆盖,这里的onlyIfAbsent本身用于甄别是否需要替换元素
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
    // 操作数加一
        ++modCount;
        if (++size > threshold) // 判断自增后的扩容阈值 判断是否扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }
3.2 resize

resize是hashmap 的扩容方法。当hashmap中的元素过多时候会导致链表过长,从而影响查询效率,因此需要改变链表的长度,除了树化就是扩大数组的长度,通过增加数组长度从而减少单个链表的长度。

resize的作用,通过增加数组长度,从而分流链表的元素。

final Node<K,V>[] resize() {
    // oldTab 扩容前的表(数组)
    Node<K,V>[] oldTab = table;
    // 获取扩容前的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 获取扩容阈值
    int oldThr = threshold;
    // 新的容量,新的阈值 注意 这里无论是长度还是容量都不能为负数。
    // 容量在什么时候更新的? 初始容量可以通过构造器输入,采用无参构造器可以跳过复制过程。也就是没有容量意味着map还没有初始化。
    // 阈值什么时候初始化的? 与容量初始化位置一致,通过方法 “this.threshold = tableSizeFor(initialCapacity);”获取初值。
    int newCap, newThr = 0;
    //  oldCap 容量大于0,也就是有容量,不为空。
    if (oldCap > 0) {
        // 散列表初始化过了  说明本次的扩容是基操,也就是传统的库容
        if (oldCap >= MAXIMUM_CAPACITY) {// 如果大于定义的说明数组不能再这个增加了,标识这个map不能再resize()
            // 设置下次扩容阈值为无穷大
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 可以扩容, <<1 说明增加了一倍。 同时需要保证 1.扩容后的容量小于最大容量 2.扩容前的容量大于初始值
        // oldCap >= DEFAULT_INITIAL_CAPACITY 这里只的是正常初始化的要求。在制定map大小的方式时可能产生不成立的情况。
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 阈值扩大一倍
            newThr = oldThr << 1; // double threshold
    }
    // oldCap == 0  标识map还没有初始化
    else if (oldThr > 0) // initial capacity was placed in threshold
        // 这种情况什么时候会产生?  通过调用第四个构造器(hashMap(Map map)调用这个构造器) 这时候只计算了阈值,没有计算容量。
        newCap = oldThr;
    // oldThr == 0  && oldCap == 0
    else {               // zero initial threshold signifies using defaults
        // 情况: 调用new hashmap 没有穿任何参数情况。
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    // newThr为表名通过new产生table,同时以上通过分支“else if (oldThr > 0)” 执行完毕。 说明这里的是
    //1.通过new hashmap(Map map)的方式产生了table,同时目前还没有添加元素就进入了扩容。虽然之前计算过传入map的threshold值,但是扩容后需要重新计算。
    //2.通过“if (oldCap > 0) {”成立进入,并且由“newCap = oldCap << 1”已经将容量扩大了一倍,(注意:“if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&” 这句话中无论结果是true还是false都会只执行“newCap = oldCap << 1”,因此此时获取的newCap已经是扩大了的)
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        // 计算一个合理的阈值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    // ----------------------------以上计算了扩容后的参数(阈值和容量)---------------------------------
    // ----------------------------下面开始正式扩容---------------------------------
    /*
    	table里面的数据有链表和红黑树,所以数据转移就存在这三种情况 链表->链表; 红黑树->红黑树; 红黑树->链表
    */
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 判断就table如果为null 表示这时候是初始化。
    if (oldTab != null) {// 表名以前有元素需要进行迁移
        for (int j = 0; j < oldCap; ++j) {// 遍历旧的表
            Node<K,V> e;
            // 获取table每个位置的元素,准备对元素开始迁移。
            if ((e = oldTab[j]) != null) {// 获取到了数组的头结点
                //方便JVM回收
                oldTab[j] = null;
                if (e.next == null)// 只有一个元素
                    // 只迁移该节点
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)// 树节点
                    // 树节点的处理专门的方法。
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                       /*
                     举例: 旧表元素为有16个扩大到32,比如取11 = 1011B,可以确定最后4位。但是倒数第五位并不知道,如果是为0则扩容后还是11,否则为11+16=27,也就是说11位置的链表扩容后只能到11位置和27位置。
                    */
                    // 两个两边的头结点与尾结点
                    Node<K,V> loHead = null, loTail = null; // 低位链表 例子中的11
                    Node<K,V> hiHead = null, hiTail = null; // 高位链表 例子中的27
                    Node<K,V> next;
                    
                    // 遍历链表
                    do {
                        next = e.next;
                        // oldCap为容量对应的2进制就是新扩容的位置的 16容量的table 下表范围为0000-1111 (0-15),16位置正好是10000,即通过上面的我们得到的迁移后的区别正好是与久容量相与的结果。
                        if ((e.hash & oldCap) == 0) {
                            // 例子中的11
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                             例子中的27
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 调整两个表的最后一个元素,并挂到table中。
                    if (loTail != null) {
                        // 这里避免旧的链表next不清理导致存储错误。
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
                
                
            }
        }
    }
    return newTab;
} 

在这里插入图片描述

3.3 get

查询分两步,第一步找到table头结点;第二步找到链表或者红黑树里面的节点。

public V get(Object key) {
        Node<K,V> e;
    // 通过getNode获取目标节点获取目标node,如果有就返回没有就返回null
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

确定一个元素是要的元素,需要对比hash,计算hash与传入的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;
    // 确定table初始化过 同时获取了(n - 1) & hash位置的头元素(first)
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // always check first node  首先检查第一个元素判断是不是要找的元素
        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);
        }
    }
    return null;
}
3.4 remove

相较于get,remove结果需要判断是否反树化。

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

其中removeNode方法为:

// matchValue 为true时,key和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;
    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;
        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);
            }
        }
        // -------------------------上面与getNode方法相似 找到要remove的元素-----------------------------------
        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;
}
3.5 replace
@Override
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;
}
// ---------------------afterNodeAccess----------------------------------------
// 这是对应的函数 是一个补充方法,官方解释为 LinkHashmap用于操作后的操作,类似于一个切面。
// Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
// -------------------------------------------------------------
// 采用与 forEach类似的操作,将核心的部分以function的方式暴露出去。
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
     Node<K,V>[] tab;
     if (function == 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) {
                 e.value = function.apply(e.key, e.value);
             }
         }
         if (modCount != mc)
             throw new ConcurrentModificationException();
     }
 }
3.6 forEach

这里在提一下forEach,这里采用了函数式编程的方式,我们回想一下使用forEach遍历的方式

map.forEach((k,v)->{
    print("key:" + k + "value:" + v);
})

传入的函数是lamdba表达式,这里接收用BiConsumer(Consumer属于一个消费者函数即没有输出形的函数)接收。

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;
        // 遍历了table中的所有元素
        for (int i = 0; i < tab.length; ++i) {
            for (Node<K,V> e = tab[i]; e != null; e = e.next)
                // 执行lamdba表达式
                action.accept(e.key, e.value);
        }
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值