HashMap底层源码解析

目录

一、属性

1.1 公有属性

1.2 链表节点

1.3 红黑树节点

二、构造方法

2.1 无参构造

2.2 有参构造

三、put方法

3.1 hash方法

3.2 newNode

3.3 put

3.4 putVal

3.5 resize

3.6 treeifyBin

3.7 split

3.8 untreeify

3.9 treeify

四、remove

4.1 removeNode

五、修改

六、get

6.1 getNode

七、size

八、clear


HashMap采用key-value方式存储数据;

JDK1.7中HashMap底层采用Entry数组+链表结构存放数据,添加元素采用头插法。

JDK1.8开始采用Node数组+链表+红黑树结构存放数据,添加元素采用尾插法;当链表的元素个数达到8后,添加第9个元素时会先添加进去,然后判断所有元素>=64个,然后变成红黑树,若所有元素<64时,用数组扩容来替代变树;当树型的元素个数<=6时,红黑树就会变成链表结构。

一、属性

1.1 公有属性

DEFAULT_INITIAL_CAPACITY:默认的数组容量为16;

MAXIMUM_CAPACITY:设置的数组最大容量为2的30次方;

DEFAULT_LOAD_FACTOR:默认扩容因子(负载因子)为0.75。即元素达到数组长度的3/4时,再添加新元素就会触发扩容,选择0.75的原因是在0.5到1之间,0.75是最合适能够乘以2的n次方得到整数的小数;

TREEIFY_THRESHOLD:链表变成红黑树的阈值为8个元素,当超过阈值且所有元素总个数超过64就把链表变成树结构,若没超过64则采用扩容方式来代替变树;

UNTREEIFY_THRESHOLD:红黑树变成链表的阈值为6个元素,当树上的元素个数少于6个时,就把树变成链表结构;

注意:链表变树和树变链表的阈值不同,是为了防止在同一个临界点频繁切换而影响效率;

MIN_TREEIFY_CAPACITY:最小变成树的容量为64,即链表变成树时还需要数组长度(容量)>=64(为了防止前期阶段频繁扩容和树化过程冲突);

table:Node数组;即存储所有节点的数组;

size:所有节点的个数;

modCount:修改元素的次数(增、删、改),在多线程中根据该参数判断是否被修改,用于Fail-Fast机制;

threshold:扩容临界点(容量*扩容因子),当元素个数>扩容临界点时会扩容数组的容量(为原来的2倍);

loadFactor:扩容因子;

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
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;
transient Node<K,V>[] table;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;

1.2 链表节点

Node:定义的一个静态内部类,将添加的元素封装为Node节点类,且赋予相应的属性;实现Map.Entry接口;

hash:存储当前节点根据key计算出的hash值(HashCode值);

key:存储当前节点的key值(名称);

value:存储当前节点的value值;

next:存储当前节点指向的下一节点地址;

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;
    }
​
    //此处省略部分源码,该部分源码是实现Entry接口的方法
}

1.3 红黑树节点

TreeNode:一个静态内部类,根据情况将元素从Node节点变为TreeNode节点类,且赋予相应的属性;继承Node类;

parent:当前节点的上一节点;(父亲节点)

left:

right:

prev:

next:继承的Node类中属性,存储下一节点地址;

root():用于返回根节点(顶层节点);

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;
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }
​
    /**
     * Returns root of tree containing this node.
     */
    final TreeNode<K,V> root() {
        for (TreeNode<K,V> r = this, p;;) {
            if ((p = r.parent) == null)
                return r;
            r = p;
        }
    }

二、构造方法

2.1 无参构造

当使用无参构造创建HashMap时,会给扩容因子初始化为默认值0.75;

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

2.2 有参构造

有参构造相当于只有一个,因为当你需要指定容量创建HashMap时,底层会自动调用另一个有参构造,传入指定的容量和默认的扩容因子0.75;

使用有参构造创建HashMap时,初始化扩容临界点为>=所指定容量的二次方数(tableSizeFor方法返回),当指定了扩容因子时,会初始化扩容因子为所指定的参数;当没有指定时,则会初始化为默认的扩容因子;

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);
}
​
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

2.2.1 tableSizeFor

该方法用来求出>=指定容量的最小的2的次方数,并返回。

为什么需要使用2的次方数来作为容量大小,其他的不允许吗?

不允许,一个是因为我们的扩容因子0.75*容量(2的次方数)是可以得到扩容临界点(整数);另一个就是因为求下标的时候要使用(数组容量-1)&hash替代%运算,提高效率。,

static final int tableSizeFor(int cap) {
    int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

三、put方法

put方法是先把节点添加进去再进行判断是否达到变树的条件!!!

3.1 hash方法

hash方法传入的参数为当前操作节点的key值;

方法中优先判断key值是否为null,为null时返回计算后的hash值为0;

当不为null时,先将key值的hashCode值算出,并赋值给h变量,在通过无符号右移16位得到高16位(注意:int类型 占4byte 32位),然后与h进行异或运算。(JDK1.8通过右移然后异或让其高位也参与了hash值的运算,进一步降低hash冲突的几率)

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

3.2 newNode

创建Node节点时使用该方法创建,new一个新的Node对象返回。

Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

3.3 put

添加元素,调用putVal方法,传入计算后key的hash值,key,value,false,true;

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

3.4 putVal

接收参数:

onlyIfAbsent:布尔类型,有相同元素时,为true时表示不更改value值;

evict:布尔类型,当为false时表示处于创建中;

为什么使用(n - 1) & hash来求出存放的数组下标(索引值)?

(数组长度-1)&hash只会求出0-(数组长度-1)的数,其实使用(数组长度-1)&hash等价于hash%数组长度求出的余数,因为位运算效率高所以采用&计算;

例如:数组长度为16,那么存放hash值为18的元素的下标为

(16-1) 0000 0000 0000 0000 0000 0000 1111

& 18 0000 0000 0000 0000 0000 0001 0010

存放下标为 0000 0000 0000 0000 0000 0000 0010(2)

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //临时变量:tab用于存放数组,p存放当前节点,n用于存放数组此时长度(容量),i用于存放当前下标(索引值)
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        //当添加元素,判断数组为null(没有元素)或数组没初始化长度(第一次添加元素),就先进行扩容数组,初始化tab和n
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        //根据 (n - 1) & hash求出当前key应存放的数组下标,初始化p和i,判断数组该位置是否有元素,若没有就直接把添加的元素当做第一个元素,并转为node节点
        tab[i] = newNode(hash, key, value, null);
    //若数组中该下标有元素时
    else {
        //临时变量e存放下一节点,k存放下一节点的key值
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //判断第一个元素和添加元素的hash地址是否相同&&key是否指向同一个地址||(添加的key是否为null&&两个key是否相等)
            e = p;
        else if (p instanceof TreeNode)
            //判断第一个节点是否为树节点,若是树节点就采用putTreeVal方法添加元素
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //若该下标有元素,且不是树结构,就遍历该下标所有节点
            //binCount从0开始,用于记录当前下标有多少节点
            for (int binCount = 0; ; ++binCount) {
                //判断下一节点是否为null
                if ((e = p.next) == null) {
                    //用于判断当前节点是否为尾结点,若是尾结点,就将添加的元素转为Node节点并存放到下一节点位置。
                    p.next = newNode(hash, key, value, null);
                    //判断节点的个数是否达到树的要求>=(8-1),这里>=7是因为for循环是从第二个节点开始判断且binCount=0,所以当binCount达到7是代表已经有了7+1个节点,新添加的为第9个节点
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //treeifyBin方法进入后,还会接着判断所有节点个数是否<64,小于则扩容退出该方法
                        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
            //记录原有的value值
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                //更新value值
                e.value = value;
            //空方法,该方法用于LinkedHashMap中
            afterNodeAccess(e);
            //返回原来的value
            return oldValue;
        }
    }
    //修改次数+1
    ++modCount;
    //元素个数+1后判断是否大于扩容临界点
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    //添加元素成功后,返回值为null
    return null;
}

3.5 resize

该方法用于扩容数组(原来数组的2倍),当第一次添加元素时会调用,或节点个数超过扩容临界点时会调用。

e.hash & (newCap - 1)和e.hash & oldCap和(数组长度-1)&hash?

三个运算是等价的,e.hash & oldCap只需要比较一位(因为oldCap一定是二的幂),而不用再取hash值的低位了加快了运算速度。

例如:当有一个数组容量为16,那么扩容后数组为32,那么(16-1)和(32-1)的二进制为(1111)和(11111),二进制差一位且为1,若e.hash从右向左第五位为0时,此时的e.hash在新老数组求出的下标位置都是相同的,若e.hash从右向左第五位1时,此时e.hash在新老数组求出的下标位置不同(相差从右向左第五位的1,也就是oldCap容量),所以我们只需要求出扩容前后数组相差的那一位在e.hash中的数字是0还是1就可得出下标位置。当为0时下标位置不变,当为1时用之前的下标+oldCap容量。

final Node<K,V>[] resize() {
    //oldTab存放扩容前的数组
    Node<K,V>[] oldTab = table;
    //oldCap存放扩容前数组的长度(容量)
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //oldThr存放扩容前的扩容临界点
    int oldThr = threshold;
    //newCap存放存放扩容后数组的长度(容量),newThr存放扩容后数组的扩容临界点
    int newCap, newThr = 0;
    if (oldCap > 0) {
        //判断老数组长度是否为0(第一次添加元素时长度为0)
        if (oldCap >= MAXIMUM_CAPACITY) {
            //判断老数组长度是否超过最大容量
            //超过就将int最大值设置为扩容临界点,让其无法再扩容
            threshold = Integer.MAX_VALUE;
            //返回老数组,因为无法扩容
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //判断新数组(老数组2倍)是否规范,初始化newCap,在默认的容量(16)和最大容量(2的30次方)之间,在就直接扩容2倍到newThr
            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);
    }
    //判断新数组有无扩容临界点(针对于有参构造第一次添加元素或新数组扩容后不规范)
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        //求出新的扩容临界点并判断是否规范(新数组容量是否超出最大容量,扩容临界点是否超出最大容量)
        //若规范就将新数组扩容临界点设置为新求的扩容临界点,若不规范,就将int最大值设置为新数组扩容临界点,使其无法再次扩容
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //新数组扩容临界点赋值到公有属性threshold
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //new出新的数组到newTab
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //newTab赋值到公有属性table
    table = newTab;
    //判断老数组是否为null,若老数组有元素我们就需要将老数组元素都移动到新数组中
    if (oldTab != null) {
        //遍历老数组所有元素,并移动,j代表当前操作的下标
        for (int j = 0; j < oldCap; ++j) {
            //e存放当前节点
            Node<K,V> e;
            //判断老数组下标中第一个节点是否为null,null的话就不用移动到新数组,直接跳到下次循环
            if ((e = oldTab[j]) != null) {
                //将老数组的这个节点置为null,相当于移动到了新数组中,为了方便老数组被GC回收
                oldTab[j] = null;
                //判断当前下标是否只有一个节点(当前下标就一个节点)
                if (e.next == null)
                    //若就一个节点,将其移动到e.hash & (newCap - 1)下标处然后跳到下次循环
                    newTab[e.hash & (newCap - 1)] = e;
                //判断第一个节点是否为树节点
                else if (e instanceof TreeNode)
                    //调用split方法分割树然后再移动
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //若不是树且有不止一个节点
                else { // preserve order
                    //loHead存放下标属于低位链表头结点,loTail存放低位链表尾结点
                    Node<K,V> loHead = null, loTail = null;
                    //hiHead存放下标属于高位链表头结点,hiTail存放高位链表尾结点
                    Node<K,V> hiHead = null, hiTail = null;
                    //next存放下一节点
                    Node<K,V> next;
                    //遍历改下标所有节点
                    do {
                        next = e.next;
                        //比较扩容前后数组的相差的那位是0还是1,分成两个链表(一个存放e.hash相差那位为0的,一个存放为1的)
                        if ((e.hash & oldCap) == 0) {
                            //判断尾结点是否为空
                            if (loTail == null)
                                //将存放的第一个节点置为低位头结点
                                loHead = e;
                            else
                                //当前节点赋给尾结点的下一节点(在跳出循环后会将下一节点置为null,因为头结点不能改变所以只能使用尾结点一直连接后面的节点)
                                loTail.next = e;
                            //当前节点赋给尾结点
                            loTail = e;
                        }
                        else {
                            //原理与上面一样
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //判断低位链表是否为空
                    if (loTail != null) {
                        //将尾结点最后置为null然后把低位链表放到新数组下标不变的位置
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        //将尾结点最后置为null然后把高位链表放到新数组下标+oldCpa的位置
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    //返回扩容后,且元素已经移动完成的新数组
    return newTab;
}

3.6 treeifyBin

根据传入的hash,判断是否达到变树的条件,达到则变平衡的红黑树,没达到则用扩容代替变树。

final void treeifyBin(Node<K,V>[] tab, int hash) {
    //n存放数组长度,index存放当前的数组下标,e存放当前节点
    int n, index; Node<K,V> e;
    //判断数组是否为null || 数组长度(容量)是否小于设置的最小树的容量64
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        //小于则用扩容替代树化(说明节点不是很多,只是节点都存放到了同一个位置)
        resize();
    //超过64则树化,求出hash存放的下标,判断是否有节点,初始化index,e
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //
        TreeNode<K,V> hd = null, tl = null;
        do {
            //replacementTreeNode将当前节点转为树节点,且null传入next属性
            //p中存放当前树节点
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                //当t1为null则表示当前尾结点为第一个树节点
                hd = p;
            else {
                //当前树节点p的上一节点指向t1
                p.prev = tl;
                //上一树节点的下一节点属性指向p
                tl.next = p;
            }
            //t1一直切换到当前树节点
            tl = p;
        } while ((e = e.next) != null);
        //将第一个树节点存放到求出的index下标处,替换原来的链表,判断是否为null
        if ((tab[index] = hd) != null)
            //treeify方法得到树的根节点、判断红黑节点以及旋转红黑树使其平衡的红黑树
            hd.treeify(tab);
    }
}

3.7 split

移动红黑树。

扩容数组后,先分割红黑树,根据在新数组的下标分为低位和高位,然后判断分割后的低位和高位树是否达到变为链表的条件(树的节点个数<=6),达到则变为链表再存入新数组的对应下标处,若没达到就直接将两个树存入对应新数组的下标处。(基本与移动链表到新数组中一致)

传入参数:

map:hashMap对象

tab:新数组

idnex:正在遍历的旧数组下标

bit:扩容前的数组容量

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    //b存放调用split方法的对象(((TreeNode<K,V>)e)对象)
    TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order
    //loHead存放低位头节点,loTail存放低位尾结点
    //hiHead存放高位头节点,hiTail存放高位尾结点。
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    //lc用于记录低位有多少个节点,hc记录高位。
    int lc = 0, hc = 0;
    //循环遍历当前下标处的所有树节点,e存放当前树节点next存放下一个树节点便于切换
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        //将当前节点的下一节点置为null
        e.next = null;
        //这里原理与链表移动时一样,可看resize方法中
        if ((e.hash & bit) == 0) {
            //判断是否为第一个树节点
            if ((e.prev = loTail) == null)
                //赋值第一个树节点
                loHead = e;
            else
                //从loHead的next属性开始连接所有的树节点
                loTail.next = e;
            loTail = e;
            //记录节点个数
            ++lc;
        }
        else {
            //原理如上
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }
    //判断低位头节点是否为null,为null则表示没有低位,全都是高位
    if (loHead != null) {
        //判断是否达到树变链表的条件<=6
        if (lc <= UNTREEIFY_THRESHOLD)
            //达到就将对象变为链表
            tab[index] = loHead.untreeify(map);
        else {
            //若没达到就将第一个节点赋值到新数组原下标处
            tab[index] = loHead;
            //判断高位有没有节点,没有的话相当于扩容前数组该下标的树全是低位树,则不需要平衡
            if (hiHead != null) // (else is already treeified)
                //有元素则低位树还需要平衡出根节点和旋转
                loHead.treeify(tab);
        }
    }
    //高位放入新数组原下标+老数组容量 下标处
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}

3.8 untreeify

树变为链表时触发(树上节点<=6);

//传入map对象
final Node<K,V> untreeify(HashMap<K,V> map) {
    //hd存放头节点,t1临时代替hd
    Node<K,V> hd = null, tl = null;
    //q存放当前树节点,因为Node是TreeNode父类,所以可以用父类引用子类
    for (Node<K,V> q = this; q != null; q = q.next) {
        //将当前树节点转为节点,其中next属性为null
        //p存放当前节点
        Node<K,V> p = map.replacementNode(q, null);
        //判断是否是头节点(第一个节点)
        if (tl == null)
            hd = p;
        else
            //从头节点的next属性开始一直到最后
            tl.next = p;
        //t1临时代替头节点连接后面所有节点
        tl = p;
    }
    //返回头节点,因为通过t1将链表已经与hd连接起来了
    return hd;
}

3.9 treeify

将数组中调用该方法的对象(链表)改为树形结构

final void treeify(Node<K,V>[] tab) {
    //root表示根节点
    TreeNode<K,V> root = null;
    //x存放当前节点,next存放下一个节点,遍历链表
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        //强转下一个节点为树形并赋给next
        next = (TreeNode<K,V>)x.next;
        //将当前节点的left,right定为空
        x.left = x.right = null;
        //判断有没有根
        if (root == null) {
            没有的话将当前节点置为根节点
            x.parent = null;
            x.red = false;
            root = x;
        }
        //若有根就进入else
        else {
            //k,h存放当前遍历的节点的key和hash值
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            //p存放根
            for (TreeNode<K,V> p = root;;) {
                //dir用来判断当前节点该放在根的左边还是右边,比根小放左,大则放右
                int dir, ph;
                K pk = p.key;
                //判断根节点hash是否比当前的节点hash大
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);
​
                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}

四、remove

根据指定key删除节点

public V remove(Object key) {
    //e中存放要删除的节点
    Node<K,V> e;
    //删除成功则返回删除节点的value属性值
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

4.1 removeNode

matchValue: 如果为真,则仅在值相等时删除 movable: 如果为 false,则在移除时不移动其他节点

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    //tab存放数组,p存放当前节点(数组中index下标中的),n存放数组长度,index存放当前下标,
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    //判断数组是否为null或者当前下标是否有节点
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        //node存放要删除的节点,e存放下一节点
        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)
                //使用getTreeNode获取
                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一直存放被遍历节点的上一节点
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //判断是否存在要删除的节点,删除节点的value的指向地址同一个,是否为空,是否值相等
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            //判断是不是树节点,是则调用removeTreeNode删除节点
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            //判断是否为第一个节点相同
            else if (node == p)
                //将null赋值给当前下标的第一个节点
                tab[index] = node.next;
            else
                //若不是树且也不是第一个节点,则直接将删除节点的next属性赋值给删除节点的上一节点的next属性
                p.next = node.next;
            ++modCount;
            //元素个数-1
            --size;
            //该方法为空,在LinkedHashMap中重写该方法
            afterNodeRemoval(node);
            //返回删除的node
            return node;
        }
    }
    //若没有要删除的节点,则返回null
    return null;
}

五、修改

因为hashMap中采用key-value方式存放数据,其中key是唯一的,若key重复则会直接覆盖之前设置的value值,即修改。

    //put中的部分源码
    //判断是否存在相同的节点,有相同则进入
    if (e != null) { // existing mapping for key
        //记录原有的value值
        V oldValue = e.value;
        if (!onlyIfAbsent || oldValue == null)
            //更新value值
            e.value = value;
        //空方法,该方法用于LinkedHashMap中
        afterNodeAccess(e);
        //返回原来的value
        return oldValue;
    }

六、get

获取查询key的value值

public V get(Object key) {
    Node<K,V> e;
    //e中存放数组中查询到key相等的节点,有则返回查询到的节点value值,没有则返回null
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

6.1 getNode

final Node<K,V> getNode(int hash, Object key) {
    //tab存放数组,first存下标第一个节点,e存放当前节点,n存放数组长度,k存放key值
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //判断数组是否为null或者根据传入hash计算出来下标中没有元素
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //判断下标中第一个节点是否为要查询的节点key值,是则直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //判断是否只有一个节点
        if ((e = first.next) != null) {
            //若不止一个节点
            //判断是否为树结构,是则调用getTreeNode方法查询树节点并返回结果
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //若为链表则直接遍历,然后一个个判断key是否相同,有则直接返回节点地址
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    //没有要查询的key值则直接返回null
    return null;
}

七、size

返回元素(节点)总个数

public int size() {
    return size;
}

八、clear

清空数组,删除所有节点

public void clear() {
    Node<K,V>[] tab;
    //修改次数+1
    modCount++;
    //判断数组是否为null&&有没有元素节点
    if ((tab = table) != null && size > 0) {
        //元素置为0
        size = 0;
        //遍历数组,且将所有下标置为null(将下标的指向的地址置为null就相当于删除,因为那些节点没有地址引用了就会被GC垃圾回收)
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值