HashMap源码详解

我习惯了无所谓,却不是真的什么都不在乎。                 请关注:源码猎人

目录

简介

位运算

进制转换

常用位运算

位运算技巧

HashMap 源码解读

属性

构造函数

获取Hash

计算长度

添加方法

内部类 Node类

Node属性

Node构造函数

Node方法

内部类 TreeNode类

TreeNode属性

TreeNode构造函数

TreeNode 方法

常见面试题


简介

HashMap的主干是一个Entry数组,Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对,采用拉链法解决Hash碰撞,链表长度超过8可能会转红黑树,这谁都知道,甚至我们都知道HashMap的put过程和扩容过程,但是我们在面试时,都说对了为啥面试官还是不满意呢?因为这些都是一个程序员最起码应该知道的,面试官在意的是你是否真的明白HashMap核心思想以及内部算法。先抛开红黑树来说,HashMap中至少有4中算法,抛开拉链法还有两种解决Hash冲突的办法,抛开HashMap来说,有很多框架、分库分表策略、请求漏油等或多或少都使用了它的思想。

位运算

不懂位运算,你怎么看HashMap算法?(这里只讲下面需要用到的位运算)

进制转换

在面试过程中我发现很多人尽然不知道进制转换,这里粗略讲一下十进制转二进制

以上是0到9十个数的二进制

十进制整数转换为二进制整数采用"除2取余,逆序排列"法。具体做法是:用2整除十进制整数,可以得到一个商和余数;再用2去除商,又会得到一个商和余数,如此进行,直到商为小于1时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。

常用位运算

&    按位与    

对应位同为1时,才为1,否则全为0(对应位只要有0,全为0,否则为1)

|    按位或    

对应位只要有1时,即为1,否则全为0(对应位只有全是0时,结果才是0,否则为1)

~    按位非    

对每位进行取反

^    按位异或    

只要对应为不同即为1

<<    左移    

m<<n即在数字没有溢出的前提下,对于正数和负数,左移n位都相当于m乘以2的n次方

>>    右移    

m>>n即相当于m除以2的n次方,得到的为整数时,即为结果。如果结果为小数,此时会出现两种情况:

  • 如果m为正数,得到的商会无条件 的舍弃小数位;
  • 如果m为负数,舍弃小数部分,然后把整数部分加+1得到位移后的值。

>>>    无符号右移    

符号右移>>> 与 右移>> 的区别就是无论操作数是正数还是负数,高位都是补0

位运算技巧

整数m大于0时,m>>n 相当于 m/(2^n)

任意整数m,m<<n 相当于 m*(2^n)

任意整数m,m&0 结果为0

任意整数m,m|0 结果为原值

任意整数m,m&(2^n) 结果只有0或者(2^n)

任意整数m,想要使m末位变为1,可以用 m|1;想要使m后两位变为1,可以用(m|2)|1

2^n 转换为二进制后,从右往左只有在n+1位为1,其他位都是0

2^n-1 转换为二进制后,从右往左数n位都是1,也就是第n位左边为0,右边为1

整数m大于0时,m/(2^n) 相当于m>>n,移掉的就是余数

整数m大于0时,想要取m后n位,可以使用 m&(n个1),n个1可以用2^n -1替换,即m&(2^n -1),m的后n位就是m>>n抹掉的数字,即m/(2^n) 的余数

HashMap 源码解读

public class HashMap<K,V> extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable

属性

// 默认数组长度 16
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;
// 将数据转换成set的另一种存储形式,这个变量主要用于迭代功能
transient Set<Entry<K,V>> entrySet;
// 数组长度
transient int size;
// 修改次数
transient int modCount;
// 扩容阈值
int threshold;
// 实际加载因子
final float loadFactor;

构造函数

public HashMap(int initialCapacity, float loadFactor) {
    // 判断长度是否越界
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 判断加载因子不能小于等于0,也不能是无穷值NAN
    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);
}

此构造函数调用上面构造函数初始化

public HashMap() {
    // 设置默认加载因子
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

最常用的构造函数,除了设置默认加载因子,其实什么事也没干

public HashMap(Map<? extends K, ? extends V> m) {
    // 设置默认加载因子
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

调用putMapEntries方法,方法如下

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    // 获取m长度
    int s = m.size();
    // 判断长度大于0
    if (s > 0) {
        // 当前table为空
        if (table == null) {
            // 长度除以加载因子(5/0.75=6.666...),算长度时只能算整数所以加1
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                    (int)ft : MAXIMUM_CAPACITY);
            // 以m算出来的数组长度大于扩容阈值,则修改扩容阈值
            // 当前table为空,所以会用这个阈值当数组初始化长度
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        else if (s > threshold)
            // 长度大于扩容阈值,先扩容一次
            resize();
        // 遍历参数m的所有entry
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            // 向当前map中添加元素
            putVal(hash(key), key, value, false, evict);
        }
    }
}

基础方法

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

获取Hash

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

从源码中可以看出,hash算法实际上就键的hashCode与hashCode无符号右移16位异或,为什么要搞这么麻烦呢?hashCode范围-2147483648到2147483648,这么大的数HashMap根本放不下,那么他是如何确定元素所在的数组下标呢?没错就是通过余数,正常情况下我们取余通过%,例如 

9%2=1
9%4=1

在特殊情况下针对这种除数是2的n次幂还可以用&,例如 

1001&0001=1(9%2或者9&(2-1))
1001&0011=1(9%4或者9&(4-1))

二进制中,一个数右移1位相当于除以2的商,而恰巧被移除出去的那一位就是除以2得到的余数,例如

9>>1 二进制 1001>>1=100,结果为4,9除以2等于4,1001向右移1位被移除的是1,9模2等于1

不仅是除以2,对于一个数要除以2的n次幂,也就是相当于把这个数向右移n位,而被移出去的n位即正好是我们要求是余数。例如

9>>2 二进制 1001>>2=10,结果为2
    9除以4(2的2次幂)等于2,1001向右移2位被移除的是1,9模4等于1
19>>3 二进制 10011>>3=10,结果为2
    19除以8(2的3次幂)等于2,10011向右移3位被移除的是3,19模8等于3

对于除数是2的n次方的算式,我们只需要得到被除数的低n位就可以了,而正好,对于2的n次方这样的数,我们将其转换为二进制之后,它就是第n+1位为1,其余低位都为0的数,因此我们将其减1,就得到了第n+1位为0,而其他位都为1的数,用此数与被除数k进行位与运算,就得到了被除数的低n位二进制数

若一个数m满足:m=2n,那么k % m = k & (m-1)

通过上面可以的值hashCode&(16-1)可以得到余数,把前面换成hashCode&1111可以看出,实际上只有hashCode后4位参与运算,前面都是无效数据(都被0替换)。这样算出来的散列效果并不好,有没有办法把前面也参与运算?于是就有了高位与地位先异或一次,让结果中包含高位特征,然后在取余。

计算长度

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的幂次方整数,先排除cap - 1以9为例

9>>>1 二进制1001>>>1=100即4,9|4二进制1001 | 100=1101即13
13>>>2 二进制1101>>>2=11即3,13|3二进制1101 | 11=1111即15
15>>>4 二进制1111>>>4=0即3,15|0二进制1111 | 0=1111即15
15>>>8 二进制1111>>>8=0即3,15|0二进制1111 | 0=1111即15
15>>>16 二进制1111>>>16=0即3,15|0二进制1111 | 0=1111即15

最后n+1即16,这里你发现了什么?9的二进制1001最后全变成了1111,实际上就是把这个数所有为都变成1,最后在加1一个是2的n次幂(2的n次幂,转换二进制就是n+1位为1其他位都是0,2的n次幂减一,转换二进制就是从n位开始都是1)。如果cap为2的n次幂时,n+1位都会变成1,这样都超过我们想要的值了,所以要cap-1。(负数补码最终都会变成-1)

键取值

public V get(Object key) {
    // 声明节点
    Node<K,V> e;
    // 调用getNode获取节点,不为空时取值
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
    // 声明变量
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 判断数组是否为空,判断元素个数是否等于0,判断数组中头元素是否为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
        // 判断头元素hash是否一样,一样时判断键是否一样
        if (first.hash == hash &&
                ((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 {
                // hash一样,键一样就返回当前元素
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            // 给e赋,为空时退出循环    
            } while ((e = e.next) != null);
        }
    }
    // 没找到返回null
    return null;
}

添加方法

public V put(K key, V value) {
    // 调用putVal方法
    return putVal(hash(key), key, value, false, true);
}
// onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值
// evict 如果为false,则表处于创建阶段
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // 声明变量
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 判断数组是否为空,判断数组长度是否为0
    if ((tab = table) == null || (n = tab.length) == 0)
        // 开始初始化(使用下次扩容阈值扩容并返回长度)
        n = (tab = resize()).length;
 
    // 通过取余得到索引,通过索引找到槽,判断槽中是否有元素
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 没有元素时,构建元素并存入
        tab[i] = newNode(hash, key, value, null);
    else {// 槽中有元素的逻辑(p不为空)
        // 声明临时变量
        Node<K,V> e; K k;
        // 如果键跟头元素一样,则将e指向该键值对
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            // 一样赋值给e
            e = p;
        // 如果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) {
                    // 能进入到这儿,e一定为空,表示没有重复建
                    p.next = newNode(hash, key, value, null);
                    // binCount从0开始,大于等于7就会尝试链变树
                    // 判断前已经添加,所以binCount=7时实际元素是8
                    // 前面又加了一个,所以从第9个开始尝试变树
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        // 尝试变树
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果键跟e一样,则跳出循环
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // 当前轮e赋值给p
                p = e;
            }
        }
        // 当e不为空,也就是有重复值
        if (e != null) {
            V oldValue = e.value;
            // 原值为空时,无论onlyIfAbsent是什么,都会被新值覆盖
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 此方法在HashMap中是空方法,LinkedHashMap中会讲
            afterNodeAccess(e);
            // 返回原值
            return oldValue;
        }
    }
    // 能走到这儿,至少说明没有重复建(有重复前面已经返回了)
    // 并且添加新元素成功,修改次数加1
    ++modCount;
    // size加1,并判断是否达到下次扩容阈值
    if (++size > threshold)
        // 扩容
        resize();
    // 此方法在HashMap中是空方法,LinkedHashMap中会讲
    afterNodeInsertion(evict);
    // 添加成功返回空
    return null;
}

从putVal源代码中我们可以知道,当插入一个元素成功的时候size就加1,若size大于threshold的时候,就会进行扩容(1、替换元素不会触发扩容,2、先添加元素在判断扩容)。按数组默认长16,扩容法值12,当我们加完第13个元素后开始扩容,若果中间有hash冲突可能只用了10个槽,一样会扩容,HashMap是否会空槽,跟hash算法有关。扩容会遍历所有的元素,时间复杂度很高,但是扩容处理后,元素会更加均匀的分布在各个槽中,会提升访问效率。

链表转树前置方法

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 只有在添加时,链表长度超过8才会调用这个方法
    // 数组为空或者,数组长度小于64,不管有没有达到扩容阈值都会扩容一次
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // 槽中链表头元素不为空
    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 {
                // 设置新节点前面节点为当前轮tl
                p.prev = tl;
                // 当前轮临时结点下一个结点设置为新节点
                tl.next = p;
            }
            // 节点下移
            tl = p;
        } while ((e = e.next) != null);// 原链表中节点末尾退出
        // 替换槽中整条链,当新双向链表不为空时(节点已经替换成树节点)
        if ((tab[index] = hd) != null)
            // 双向链表转红黑树
            hd.treeify(tab);
    }
}

扩容

final Node<K,V>[] resize() {
    // 原数组
    Node<K,V>[] oldTab = table;
    // 原数组长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 原扩容阈值
    int oldThr = threshold;
    // 新长度,新扩容阈值
    int newCap, newThr = 0;
    // 原数组长度大于0
    if (oldCap > 0) {
        // 是否达到上限
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 达到上限不在扩容
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 原长度扩容一倍,并且新长度小于上限,原长度大于16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 新扩容阈值也增加一倍
            newThr = oldThr << 1; // double threshold
    }
    // 原扩容阈值大于0
    else if (oldThr > 0)
        // 原数组长度为0,原扩容阈值大于0,只有在初始化时存在
        newCap = oldThr;
    else {
        // HashMap新建状态赋值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
 
    // 新扩容阈值为0(只有在初始化时,走到这儿才会新扩容阈值等于0)
    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];
    table = newTab;
    // 以下逻辑是元素迁移逻辑
    if (oldTab != null) {
        // 遍历原数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // e这时为槽里第一个元素(无论是链表还是红黑树,找到头元素,下面的都可以获得)
            if ((e = oldTab[j]) != null) {
                // 头元素拿出来后,当前槽清空
                oldTab[j] = null;
                // 只有一个元素
                if (e.next == null)
                    // 只有一个元素时,直接放入新数组槽中
                    newTab[e.hash & (newCap - 1)] = e;
                // 红黑树
                else if (e instanceof TreeNode)
                    // 请看TreeNode的split方法
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 链表
                else {
                    // loHead 原槽中头节点,loTail 原链当前节点
                    Node<K,V> loHead = null, loTail = null;
                    // hiHead 扩容槽中头节点,hiTail 扩容链当前节点
                    Node<K,V> hiHead = null, hiTail = null;
                    // 下级元素
                    Node<K,V> next;
                    // 遍历链表上所有元素
                    do {
                        // 先取出当前元素的下级元素
                        next = e.next;
                        // 判断是否可以放在原槽中(0可以,oldCap需要移动)
                        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);
                    // 原链不为空
                    if (loTail != null) {
                        // 这一步主要作用是清空原结构冗余链
                        loTail.next = null;
                        // 整个头结点放入槽中
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        // 这一步主要作用是清空原结构冗余链
                        hiTail.next = null;
                        // 整个扩容头结点放入扩容后槽中
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    // 返回新数组
    return newTab;
}

很多人对(e.hash & oldCap) == 0可能会有疑问,这里解释一下。它的结果只能是0或者oldCap,认真看来hash获取的都知道取余hash&oldCap-1,那么扩容后取余方式hash&newCap-1,举个实际的例子
12%4,二进制1100 &   11 = 0    10进制0
12%8,二进制1100 & 1 11 = 100  10进制4
实际上,扩容后取余方式就是在前面又补1,后面的11都没用上(对于2n次幂肯定一样);那么扩容后这个元素是否需要移动到扩容后槽中,其实就看(newCap-1)最高位就行。既然我们只需要看这一位那我就把hash其他位全变成零,1 11中后面11换成0根hash位与操作就行,刚好(newCap-1)除了高位一外其他位换0就是oldCap。所以这er要么是0要么就是oldCap,需要往新槽里面移动的,只需要在原槽基础上加oldCap就可以了。

删除

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}
public boolean remove(Object key, Object value) {
    return removeNode(hash(key), key, value, true, true) != null;
}
// matchValue 为true,则表示删除它key对应的value,不删除key,
// movable 为false,则表示删除后,不移动节点
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;
    // 哈希数组不为null,且长度大于0,然后获得到要删除key的节点所在是数组下标位置
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
        // nodee 存储要删除的节点,e 临时变量,k 当前节点的key,v 当前节点的value
        Node<K,V> node = null, e; K k; V v;
        // 如果数组下标的节点正好是要删除的节点,把值赋给临时变量node
        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不再是数组下标的节点,而是要删除结点的上一个结点
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 找到要删除的节点后,判断!matchValue,我们正常的remove删除,!matchValue都为true
        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;
            // 长度减1
            --size;
            // 此方法在hashMap中是为了让子类去实现,主要是对删除结点后的链表关系进行处理
            afterNodeRemoval(node);
            // 返回删除的节点
            return node;
        }
    }
    // 返回null则表示没有该节点,删除失败
    return null;
}

清除重置

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;
    }
}

可以看出,HashMap并没有清除所有元素,只是清空所有槽,并且不会改变槽个数

void reinitialize() {
    table = null;
    entrySet = null;
    keySet = null;
    values = null;
    modCount = 0;
    threshold = 0;
    size = 0;
}

清空所有数据

创建、转型节点

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

创建新普通节点

Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
    return new Node<>(p.hash, p.key, p.value, next);
}

其他节点转换为普通节点

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

创建树节点

TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

普通节点转换为树节点

替换

public boolean replace(K key, V oldValue, V newValue) {
    Node<K,V> e; V v;
    // 根据key查找节点,如果节点的值不为oldValue时放弃修改
    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;
}

只有在key存在,并且key对应的值跟oldValue一样时,才会替换

public V replace(K key, V value) {
    Node<K,V> e;
    // 根据key查找节点
    if ((e = getNode(hash(key), key)) != null) {
        V oldValue = e.value;
        // 新值覆盖原值
        e.value = value;
        afterNodeAccess(e);
        // 返回原值
        return oldValue;
    }
    return null;
}

找到节点就覆盖,找不到就返回空

内部类 Node类

static class Node<K,V> implements Map.Entry<K,V>

Node属性

// hash值
final int hash;
// 键
final K key;
// 值
V value;
// 下一个元素
Node<K,V> next;

Node构造函数

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

Node构造函数只负责初始化内部属性

Node方法

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

可以看出只能更改值不能更改键

public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
}

hashcode方法比较特殊,键的hashcode和值的hashcode异或

public final boolean equals(Object o) {
    if (o == this)
        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;
    }
    return false;
}

equals方法其实就是比较键和值是否一样

内部类 TreeNode类

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>

不理解红黑树,看TreeNode源码比较吃力,这里顺带介绍下红黑树。
红黑树的五大特性:

1、每个结点是黑色或者红色。
2、根结点是黑色。
3、每个叶子结点(NIL)是黑色。(这里叶子结点,是指为空(NIL或NULL)的叶子结点!)
4、如果一个结点是红色的,则它的子结点必须是黑色的。
5、每个结点到叶子结点NIL所经过的黑色结点的个数一样的。(确保没有一条路径会比
其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!)

红黑树基本操作:

左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,
    右子结点的左子结点变为旋转结点的右子结点,其左子结点保持不变。
右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,
    左子结点的右子结点变为旋转结点的左子结点,其右子结点保持不变。
变色:结点的颜色由红变黑或由黑变红。

LinkedHashMap.Entry类

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    // 调用HashMap.Node构造函数
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

HashMap.Node前面已经讲过了,往上翻

TreeNode属性

// 父节点
TreeNode<K,V> parent; 
// 左节点
TreeNode<K,V> left;
// 又结点
TreeNode<K,V> right;
// 前面结点
TreeNode<K,V> prev;
// 当前结点颜色
boolean red;
// LinkedHashMap.Entry中上一个元素(双向链表)
Entry<K,V> before; 
// LinkedHashMap.Entry中下一个元素(双向链表)
Entry<K,V> after;
// hash值
final int hash;
// 键
final K key;
// 值
V value;
// 下一个元素
Node<K,V> next;

TreeNode构造函数

TreeNode(int hash, K key, V val, Node<K,V> next) {
    // 调用LinkedHashMap.Entry中构造函数
    super(hash, key, val, next);
}

TreeNode 方法

TreeNode查找root结点

final TreeNode<K,V> root() {
    // 这里声明两个变量r、p,当前结点赋值给r,然后迭代
    for (TreeNode<K,V> r = this, p;;) {
        // 取当前结点的父节点赋值p,判断是否为空
        if ((p = r.parent) == null)
            // 没有父节点就是root结点
            return r;
        // p赋值给r继续迭代
        r = p;
    }
}

TreeNode的find方法

// h 为键的hash值,k 为键,kc 为比较器(实现了Comparable接口)
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
    // 声明变量p,并把当前节点赋值给它
    TreeNode<K,V> p = this;
    do {
        // 声明ph存放临时节点hash,dir 临时比较的结果,pk临时节点键
        int ph, dir; K pk;
        // pl 左节点,pr右节点,q下轮要找的节点
        TreeNode<K,V> pl = p.left, pr = p.right, q;
        // 把当前节点hash赋值给ph,判断当前节点hash是否大于h
        if ((ph = p.hash) > h)
            // 大于说明要找的节点在左边
            p = pl;
        else if (ph < h)
            // ph 小于h,说明要找的在右边
            p = pr;

        // 能走到这,至少说明hash一样了
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            // 把当前节点键赋值给pk,然后判断是否一样,一样直接就返回
            return p;

        // 能走这儿,说明hash一样key不一样
        else if (pl == null)
            // 左边为空赋值右边,希望寄托下一轮
            p = pr;
        else if (pr == null)
            // 右边边为空赋值左边,希望寄托下一轮
            p = pl;

        // 能走这儿,说明hash一样,key不一样,还都有子节点
        // comparableClassFor 获取键的比较器
        // compareComparables 获取比较结果
        // 判断比较器是否为空,为空获取k的比较器,然后比较大小
        else if ((kc != null ||
                (kc = comparableClassFor(k)) != null) &&
                (dir = compareComparables(kc, k, pk)) != 0)
            // 比较结果小于0走左边,大于零走右边,等于0还是左右不分
            p = (dir < 0) ? pl : pr;

        // 前面比较器比较结果也一样,尝试右边查一把
        else if ((q = pr.find(h, k, kc)) != null)
            // 查到了就返回
            return q;
        else
            // 没查到,从左边继续下一轮查
            p = pl;
    } while (p != null);
    // 都查完了还是空,只能返回没查到
    return null;
}

find方法使用比较多所以这里先做分析

TreeNode的getTreeNode方法

final TreeNode<K,V> getTreeNode(int h, Object k) {
    return ((parent != null) ? root() : this).find(h, k, null);
}

这里先判断自己是不是root结点,如果是就从自身开始找,如果不是先找root,然后从root开始找TreeNode添加

final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                               int h, K k, V v) {
    // 比较器
    Class<?> kc = null;
    // 是否搜索过(只有在hash和比较器都不能确定时才会用)
    boolean searched = false;
    // 获取头结点
    TreeNode<K,V> root = (parent != null) ? root() : this;
    // 从头结点开始遍历
    for (TreeNode<K,V> p = root;;) {
        // dir 比较器比较结果,ph 当前临时hash,pk当前临时键
        int dir, ph; K pk;
        if ((ph = p.hash) > h)
            // 当前hash大于h,走左边
            dir = -1;
        else if (ph < h)
            // 当前hash小于h,走右边
            dir = 1;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            // 键已存在
            return p;
        // 相同hash,键不等,通过比较器确定
        else if ((kc == null &&
                (kc = comparableClassFor(k)) == null) ||
                (dir = compareComparables(kc, k, pk)) == 0) {
            // 比较器比较也一样
            if (!searched) { // 没搜索过
                TreeNode<K,V> q, ch;
                // 设置搜索标志
                searched = true;
                // 先搜索左边,没搜到,在搜索右边(find方法前面有将)
                if (((ch = p.left) != null &&
                        (q = ch.find(h, k, kc)) != null) ||
                        ((ch = p.right) != null &&
                                (q = ch.find(h, k, kc)) != null))
                    // 搜索到就返回q
                    return q;
            }
            // 搜索过或者没搜到,只能通过物理hash大小确定左右
            dir = tieBreakOrder(k, pk);
        }

        // 走这儿说明没有重复建
        TreeNode<K,V> xp = p;
        // 这里一定可以确定左右(dir只能是-1或1,为0前面已经转掉了)
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            // 能到这儿,说明有一边为空,
            Node<K,V> xpn = xp.next;// 兼容双向原链表结构
            // 创建新树节点,原下级链表接在此节点后
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            // 新节点上树
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            // 维护双向链表下级节点
            xp.next = x;
            // 维护树父级节点,维护双向链表前面结点
            x.parent = x.prev = xp;
            // 当前结点下级几点不为空时,需要把下级节点前面指向x
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
            // 平衡红黑树
            moveRootToFront(tab, balanceInsertion(root, x));
            // 添加成功返回空
            return null;
        }
    }
}

TreeNode删除

// 此方法只有HashMap删除时找到应该删除的结点为树节点是才会调用
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                          boolean movable) {
    int n;
    // 数组为空时直接返回
    if (tab == null || (n = tab.length) == 0)
        return;
    // 确定当前元素所在的槽
    int index = (n - 1) & hash;
    // first、root临时当前槽头节点
    TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
    // succ临时当前节点的下级节点, pred临时当前节点前面节点
    TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
    // 删除的节点是头节点
    if (pred == null)
        tab[index] = first = succ;
    else
        // 不是头节点,上级节点下级指向当前下级
        pred.next = succ;
    // 不是尾节点,把下级节点的上级指向当前上级
    if (succ != null)
        succ.prev = pred;
    // --走到这双向链表中该节点已经删除成功--

    // 头节点为空直接返回
    if (first == null)
        return;
    // 头节点上级不为空
    if (root.parent != null)
        // 重新找root节点
        root = root.root();
    if (root == null// 头节点为空
            || (movable
            && (root.right == null // 头节点右为空
            || (rl = root.left) == null // 头节点左为空
            || rl.left == null))) { // 左节点的左节点为空
        // 树链表,树转链表依赖双向链表,不依赖树结构(这种情况无视长度小于6?)
        tab[index] = first.untreeify(map);  // too small
        return;
    }
    // 下面开始从树结构中移除当前元素
    TreeNode<K,V> p = this, pl = left, pr = right, replacement;
    // 左右都不为空
    if (pl != null && pr != null) {
        // 找到右节点下最左边节点(循环左边的左边)
        // 右边所有子节点中,最左边的一定最接近当前节点(可以自己推)
        TreeNode<K,V> s = pr, sl;
        while ((sl = s.left) != null) // find successor
            s = sl;
        // 交换p(当前节点)和s(右边最左下)的颜色
        boolean c = s.red; s.red = p.red; p.red = c; // swap colors
        TreeNode<K,V> sr = s.right;
        TreeNode<K,V> pp = p.parent;
        // 当前节点的右节点没有左节点(简单交换位置)
        if (s == pr) {
            // 当前上级指向s
            p.parent = s;
            // 当前节点放到s右边
            s.right = p;
        }
        else {
            // s节点和当前节点互换位置并设置父节点
            TreeNode<K,V> sp = s.parent;
            if ((p.parent = sp) != null) {
                if (s == sp.left)
                    sp.left = p;
                else
                    sp.right = p;
            }
            if ((s.right = pr) != null)
                pr.parent = s;
        }
        p.left = null;
        // 如果s有右节点,把p设置成sr的父节点
        if ((p.right = sr) != null)
            sr.parent = p;
        // 把p的左节点交接给s
        if ((s.left = pl) != null)
            pl.parent = s;
        // 把p的父节点交接给s
        if ((s.parent = pp) == null)
            root = s;
        else if (p == pp.left)
            pp.left = s;
        else
            pp.right = s;
        // 如果sr不为null替换p的节点就是sr,否则为p
        if (sr != null)
            replacement = sr;
        else
            replacement = p;
    }
    // 左右空的情况
    else if (pl != null)
        replacement = pl;
    else if (pr != null)
        replacement = pr;
    else
        replacement = p;

    if (replacement != p) {
        // 把要删除的移除掉
        TreeNode<K,V> pp = replacement.parent = p.parent;
        if (pp == null)
            root = replacement;
        else if (p == pp.left)
            pp.left = replacement;
        else
            pp.right = replacement;
        p.left = p.right = p.parent = null;
    }

    TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

    if (replacement == p) {  // detach
        // 移除当前节点
        TreeNode<K,V> pp = p.parent;
        p.parent = null;
        if (pp != null) {
            if (p == pp.left)
                pp.left = null;
            else if (p == pp.right)
                pp.right = null;
        }
    }
    // 是否需要平衡树
    if (movable)
        moveRootToFront(tab, r);
}

树转链表

final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    // 这里只依赖链表转换
    for (Node<K,V> q = this; q != null; q = q.next) {
        // TreeNode将转化成Node
        Node<K,V> p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    return hd;
}

TreeNode扩容迁移节点

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // 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;
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
            ++lc;
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }
    if (loHead != null) {
        // 原槽元素个数小于等于6,树转链表
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
            tab[index] = loHead;
            if (hiHead != null)
                loHead.treeify(tab);
        }
    }
    if (hiHead != null) {
        // 扩容槽中元素个数小于等于6,树转链表
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}

双向链表转树
调用此方法前必须先构建双向链表

final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        // 插入的是第一个元素 并给root赋值
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            //插入到红黑树中的位置 逻辑跟putTreeVal类似
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                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;
                }
            }
        }
    }
    // 把root节点移到链表头
    moveRootToFront(tab, root);
}

头节点前移

static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
    int n;
    if (root != null && tab != null && (n = tab.length) > 0) {
        // 确定槽位置
        int index = (n - 1) & root.hash;
        TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
        // 如果链表的头不是红黑树的头节点 则把root移到头节点 也就是first的前面
        if (root != first) {
            Node<K,V> rn;
            tab[index] = root;
            TreeNode<K,V> rp = root.prev;
            if ((rn = root.next) != null)
                ((TreeNode<K,V>)rn).prev = rp;
            if (rp != null)
                rp.next = rn;
            if (first != null)
                first.prev = root;
            root.next = first;
            root.prev = null;
        }
        // 检查一下红黑树的各个成员变量是否正常
        assert checkInvariants(root);
    }
}

moveRootToFront的作用就是把root节点move到链表的头.

常见面试题

1、HashMap 默认值问题

HashMap 初始默认大小为16,但是在刚刚new出来时,长度为0。

HashMap 默认加载因子是0.75,它是采用先插入在扩容,当添加完第13个元素后触发扩容

HashMap 链表长度超过8不一定转红黑树,数组长度小于64时直接扩容,当数组长度大于64时才会转红黑树

HashMap 红黑树退链默认值为6

HashMap 键不重复,键可以有一个空直。线程不安全 等

2、hash算法原理是什么?

取key的hashcode,然后混合高低位,hashcode^(hashcode>>>16),因为在取余时实际上只用到了后几位,前面高位都没用上,这要取余结果还是会有比较高的重复概率,通过左16位和右16位取异,相当于是混合高低位,使低位也具有高位的特征,这样取余重复概率小一些。

3、HashMap是如何保证数组长度一定是2的n次幂?

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;
}

HashMap通过tableSizeFor方法保证数组长度一定是2的n次幂,原理就是把高位右边全变为1,然后再加1,结果一定是2的n次幂。但是如果原来就是2的n次幂,结果就会整体左移一位,所以一开始需要先减一。

4、HashMap 是如何通过hash值计算出key所在的位置?

通过取余方式,原理就是hash%数组长度。HashMap要求数组长度为2的n次幂,所以可以使用 hash & (length-1)。

5、HashMap 扩容时它是怎么知道那些元素需要迁移,哪些元素不需要迁移?

它是通过 hash & oldlength 看看是否为0,如果为0不需要迁移,不为0需要迁移,迁移时原下标加原长就是新下标。如果不这么做,那么就需要用hash值对新数组长度取余,即新长度减1做&运算,如果高位不变那么余数跟原来一样,高位为1就需要迁移。

6、红黑树5个原则是什么?

一红一黑,根黑叶子黑,红下两黑。红黑树只有左旋右旋两种操作,查询的时候从右边查起会更省时间。

7、HashMap 内部用到哪几种结构?

数组、数组+链表、数组+双向链表、数组+红黑树,数组加双向链表只是链表转树时的中间状态结构。

世界地图矢量数据可以通过多种网站进行下载。以下是一些提供免费下载世界地图矢量数据的网站: 1. Open Street Map (https://www.openstreetmap.org/): 这个网站可以根据输入的经纬度或手动选定范围来导出目标区域的矢量图。导出的数据格式为osm格式,但只支持矩形范围的地图下载。 2. Geofabrik (http://download.geofabrik.de/): Geofabrik提供按洲际和国家快速下载全国范围的地图数据数据格式支持shape文件格式,包含多个独立图层,如道路、建筑、水域、交通、土地利用分类、自然景观等。数据每天更新一次。 3. bbbike (https://download.bbbike.org/osm/): bbbike提供全球主要的200多个城市的地图数据下载,也可以按照bbox进行下载。该网站还提供全球数据数据格式种类齐全,包括geojson、shp等。 4. GADM (https://gadm.org/index.html): GADM提供按国家或全球下载地图数据的服务。该网站提供多种格式的数据下载。 5. L7 AntV (https://l7.antv.antgroup.com/custom/tools/worldmap): L7 AntV是一个提供标准世界地图矢量数据免费下载的网站。支持多种数据格式下载,包括GeoJSON、KML、JSON、TopJSON、CSV和高清SVG格式等。可以下载中国省、市、县的矢量边界和世界各个国家的矢量边界数据。 以上这些网站都提供了世界地图矢量数据免费下载服务,你可以根据自己的需求选择合适的网站进行下载
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

源码猎人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值