面试你一定得知道的HashMap上线

1、HashMap的概述

1.1 什么是HashMap

Hashmap是一种非常常用的、应用广泛的数据类型。HashMap 是基于哈希表的 Map 接口是实现的。此实现提供所有可选操作,并允许使用 null 做为值(key)和键(value)。HashMap 不保证映射的顺序,特别是它不保证该顺序恒久不变。此实现假定哈希函数将元素适当的分布在各个桶之间,可作为基本操作(get 和 put)提供稳定的性能。

1.2 HashMap的继承关系

在这里插入图片描述
代码继承关系

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

(1)AbstractMap:Map的骨架实现,用来减少子类实现Map接口要做的工作
(2)Map:值可重复,键不能重复,允许键为null
(3)Cloneable:一种标记接口,实现改接口能够实现克隆,若不实现该接口,用Object.clone会报错
(4)Serializable:一种标记接口,实现改接口,他支持序列化,能够通过序列化进行传输

2、HashMap的数据结构

(1)jdk1.8之前:数组+链表
链表的节点存储的是一个 Entry 对象,每个Entry 对象存储四个属性(hash,key,value,next),若遇到哈希冲突,则将冲突的值加到链表中即可。
在这里插入图片描述

(2)jdk1.8之后:数组+链表|红黑树
当连表长度大于阈值(或者红黑树的边界值,默认为8)并且当前的数组长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。
在这里插入图片描述
数组:采用一段连续的存储单元来存储数据。是HashMap的主题
链表:主要为了解决哈希冲突
红黑树:一种平衡的二叉树
哈希冲突:当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了(两个对象调用的 hashCode 方法计算的哈希码值一致导致计算的数组索引值相同)

3、源码分析

3.1 成员变量
	// 默认初始容量 jdk1.7结果直接是16,没有位移
 	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
 	// 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
	// 默认装载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
	// 链表转红黑树的一个界限值(后面会进行介绍) jdk1.8之前无
    static final int TREEIFY_THRESHOLD = 8;
	// 红黑树转链表的一个界限值(后面会进行介绍) jdk1.8之前无
    static final int UNTREEIFY_THRESHOLD = 6;
	// 链表转红黑树的另一个界限值(后面会进行介绍) jdk1.8之前无
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 底层数组
    transient Node<K,V>[] table;
	// 存放具体元素的集,可用于遍历map集合
    transient Set<Map.Entry<K,V>> entrySet;
	// 元素个数
    transient int size;
	// 修改次数
    transient int modCount;
    // 阈值,当size大于该值时会进行扩容
    int threshold;
	// 装载因子,用来计算threshold
    final float loadFactor;

(1)<<:表示左移,左移几位就是乘以2的几次幂
(2)>>:表示右移,右移几位就是除以2的几次幂

3.2 构造方法

(1)无参构造

	public HashMap() {
		// 初始化this.loadFactor为0.75
       this.loadFactor = DEFAULT_LOAD_FACTOR;
    }

(2) HashMap(int)

 	public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

(3)HashMap(int, float)

	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);
        // 初始化loadFactor和threshold
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

(4)HashMap(Map)

	public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        // 将m中的所有元素添加到该HashMap中
        putMapEntries(m, false);
    }
  
  
	final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
		// 获取元素的个数
        int s = m.size();
         // 当m中有元素的时候,需要将Map中的元素放入此HashMap实例中
        if (s > 0) {
        	 // 判断 table是否已经初始化
            if (table == null) { // pre-size
            	// 未初始化,s是m的元素个数
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                 // 判断得到的值是否大于阈值,如果大于阈值,则初始化阈值
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
        //判断待插入的 Map 的 大小(size),如果size > threshold ,则先进行 resize() 扩容
            else if (s > threshold)
                resize();
            //然后开始遍历,待插入的 Map ,将每一个<Key,Value> 插入到该HashMap实例中
            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);
            }
        }
    }
	//对于给定的目标容量,返回两倍大小的幂
	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;
    }

1、>>与 >>> 的区别

(1)>>:带符号右移。正数右移高位补0,负数右移高位补1。比如:
4 >> 1,结果是2;-4 >> 1,结果是-2。-2 >> 1,结果是-1。
(2)>>>:无符号右移。无论是正数还是负数,高位通通补0。

逐步分析以上代码

假如传入的值为10

  • int n = cap - 1; ------> n=9
  • n |= n >>> 1; ------> 13
n = 9
n              ----------  0000  1001
n >>> 1        ----------  0000  0100
n |= n >>> 1   ----------  0000  1101

|=:只要有一个为1,|=的结果就为1

  • n |= n >>> 2; ------> 15
n             ----------  0000  1101
n >>> 2       ----------  0000  0011
n |= n >>> 2  ----------  0000  1111

此时低位全部为1,后面再进行或运算,低位也不会变成其他数

  • return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    通过三目运算得到最终值为16

关于为什么cap首先减1?

防止本来就是2的次幂数返回2倍,比如cap是8,若不减1,则返回16而不是8了
因为 HashMap 要保证容量是 2 的整数次幂, 该方法实现的效果就是如果你输入的 cap 本身就是偶数,那么就返回 cap 本身,如果输入的 cap 是奇数,返回的就是比 cap 大的最小的 2 的整数幂

为什么要右移和或运算那么多次

由于int类型为32位,所有即使除符号为之外只有第一位为1的情况,也能将所有的位全部变成1

为什么要在前面减1,然后在后面加回来呢?

当输入为0的时候就会发现,方法的输出为1,HashMap的容量只有大于0时才有意义。

3.3 增加元素
	public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

HashMap提供了put用于添加元素,put方法调用了putVal方法。我们可以看到在 putVal() 方法中 key 在这里执行了一下hash()算法,我们分析一下怎么实现的

	static final int hash(Object key) {
        int h;
       // 如果key为null,返回0, 可以看到当key为null的时候也是有哈希值的
       // 如果key不为null, 首先计算出key的hashCode,然后赋值给h,接着,h进行无符号右移16位,再进行异或运算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        // 这里用 h^(h>>>16)使得h的高16位也参与运算,增强散列性
    }

^ 异或运算符,运算规则:相同的二进制数位上,数字相同,结果为0,否则为1
①首先获取对象的hashCode()值,然后赋值给h,
②再将hashCode值右移16位
③将右移后的值与原来的hashCode做异或运算,返回结果

	 // onlyIfAbsent为false,表示条目已存在则用新值覆盖旧值;
	 // onlyIfAbsent为true,表示条目存在不进行覆盖
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; 
        Node<K,V> p; 
		// n存放数组长度。i存放key的hash计算后的值
		int n, i;
		// table 表示存储在Map中的元素的数组,把table赋值给tab,再判断是否为空
		// 判断数组的长度是否为0
        if ((tab = table) == null || (n = tab.length) == 0)
        	// 如果为空就实例化一个数组
            n = (tab = resize()).length;
         // i = (n - 1) & hash 计算当前key所在下标,确定在哪个桶中,并将下标赋值给i
         // n是数组的长度,hash是在hash算法中返回的hash值
         // p = tab(i) 将该位置的元素赋值给p,并且判断是否为null
        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:判断第一个元素的hash与我们传进来的hash是否相等
            // (k = p.key) == key 将第一个元素的key赋值给k,并且判断是否和我们传进来的key相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 第一个元素的key和我们传进来的key值是相等的,初始化e
                e = p;
             // p是TreeNode的实例
            else if (p instanceof TreeNode)
            	// 若key在红黑树中存在,直接返回已存在的节点,不做处理(下面会用新值替换旧值)
                // 若key在红黑树中不存在,putTreeVal中会创建新节点、添加到红黑树中,并返回null
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else { // 桶是单向链表
            	// 遍历链表
                for (int binCount = 0; ; ++binCount) {
                	// 该条件成立时,p.next就是新节点的位置
                    if ((e = p.next) == null) {
                    	// 创建新节点并连接到p.next处
                        p.next = newNode(hash, key, value, null);
                        //超过了链表的设置长度(默认为8)则转换为红黑树
                        // binCount从0开始计算,记录遍历节点的个数,所以后面减一
                        if (binCount >= TREEIFY_THRESHOLD - 1) 5
                        	// 将链表转为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 判断key对应的节点是否已经存在,若存在则直接跳出
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 说明新添加的元素和当前节点不相同,继续找下一个元素。
                    p = e;
                }
            }
           // e不为null表示添加的键已存在,用新值替换旧值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 当最后一次调整之后的Size大于临界值,就需要调整数组容量
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

hash & (length - 1) 来得到该对象的保存位,等价于 hash % length,但是 & 效率比 % 要高

3.4 数组扩容
	final Node<K,V>[] resize() {
		// 先拿到旧的hash桶
        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) {
            	// 临界值就等于Integer类型最大值
                threshold = Integer.MAX_VALUE;
                // 不扩容,直接返回旧数组
                return oldTab;
            }
           // oldCap扩大到2倍之后赋值给newCap,判断newCap是否小于最大容量并且原数组长度大于等于数组初始化长度
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 进行2倍的扩容
                newThr = oldThr << 1; // double threshold
        }
         // 旧容量为0,当前临界值不为0,让新的临界值等于当前临界值
        else if (oldThr > 0) 
            newCap = oldThr;
        // 当前容量和临界值都为0
        else {  
        	//使用默认的加载因子(0.75)             
            newCap = DEFAULT_INITIAL_CAPACITY;
            //新增的临界值也就为 16 * 0.75 = 12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 经过上面对新临界值的计算后如果还是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];
        // 赋值给hash桶
        table = newTab;
        // 如果数组不为空,将原数组中的元素放入扩容后的数组中
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                // 将旧桶的当前下标位置元素赋值给e,并且e不为null
                if ((e = oldTab[j]) != null) {
                	// 置空
                    oldTab[j] = null;
                    // 下一个节点如果为空
                    if (e.next == null)
                    	// 如果没有下一个节点,说明不是链表,当前桶上只有一个键值对,直接计算下标后插入
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)// 桶内是红黑树
                    	// 将以e为根节点的红黑树分裂成两部分,这两部分可能会退化为链表
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // 桶内仍是链表,将链表分裂为两部分
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.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);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

图片来自其他博主

什么时候需要扩容

当Hashmap中元素个数超过 数组长度*负载因子 就会进行扩容。也就是说,当HashMap数组长度是16的时候,如果元素个数超过 16 * 0.75=12 的时候,就会给数组扩容,扩容方式就是将原数组扩大2倍,也就是16 * 2=32

当HashMap中的其中一个链表的对象个数达到了8个,此时如果数组长度没有达到64,
那么HashMap也会进行扩容。如果达到了64,那么这个链表会变成红黑树,
节点类型由Node变成TreeNode。

1.7和1.8的扩容有什么区别

(1)在1.7中,扩容之后需要重新去计算其Hash值,因为每个元素放在哪个位置计算(hash值 & length-1),然后Hash值对其进行分发(扩容之后的length是扩容之前的二倍)
(2)在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是0还是1,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

3.5 删除数据

remove(key) 方法 和 remove(key, value) 方法都是通过调用removeNode的方法来实现删除元素的

	// 根据key删除元素 删除的返回值是被删除key所对应的value
	public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.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) {
            // 进入这里面,说明hash桶不为空,并且当前key所在位置的元素不为空
            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;
         	// 如果存在next节点,就说明该数组位置上发生了hash碰撞,此时可能存在一个链表,也可能是一颗红黑树
            else if ((e = p.next) != null) {
            	// 是一个红黑树,那么调用getTreeNode方法从树结构中查找满足条件的节点
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                 // 是一个链表,只需要从头到尾逐个节点比对即可
                else {
                    do {
                    	// 如果e节点的键和key相等,e节点就是要删除的节点,赋值给node变量,跳出循环
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        // 更新p
                        p = e;
                    // 如果e存在下一个节点,那么继续去匹配下一个节点。直到匹配到某个节点跳出 或者 遍历完链表所有节点
                    } while ((e = e.next) != null);
                }
            }
            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);
                // 该node节点就是首节点
                else if (node == p)
                	// 直接将节点数组对应位置指向到下一个节点即可
                    tab[index] = node.next;
                // p是node的父节点,由于要删除node,所有只需要把p的下一个节点指向到node的下一个节点即可把node从链表中删除了
                else
                    p.next = node.next;
                ++modCount; // 修改次数递增
                --size; // 个数递减
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }
3.6 替换数据

(1)replace(K,V)

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

(2)replace(K,V,V)


    public boolean replace(K key, V oldValue, V newValue) {
        Node<K,V> e; V v;
        // 获取key对应的节点,当e.value与oldValue相等时才newValue替换原值
        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;
    }
3.7 获取数据
	public V get(Object key) {
        Node<K,V> e;
        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;
        // table不为空且桶不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // key对应的节点与桶内第一个节点一致,直接返回第一个节点first
            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)
                	// 从红黑树中获取key对应的节点
                    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);
            }
        }
        // 未找到,返回null
        return null;
    }
3.8 转红黑树方法

在前面分析put方法的时候,节点添加完成之后就会判断此时节点个数是否大于8,如果大于则将链表转换为红黑树。

	final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 如果当前数组为空,或者数组长度小于进行树形化的阈值(64)就去扩容,而不是转换为红黑树。
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        // 链表长度达到9且数组长度不小于64,将链表转为红黑树
        else if ((e = tab[index = (n - 1) & hash]) != null) {
        	// hd:红黑树的头结点。tl:红黑树的尾结点
            TreeNode<K,V> hd = null, tl = null;
            // 遍历(单向)链表
            do {
            	// 重新创建一个树节点,内容和当前链表节点e一致
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                	// 将新创建的p节点赋值给红黑树的头结点
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
           // 让桶中第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树,而不是链表
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

为什么数组的长度达到64才进行转为红黑树

如果数组很小,那么转换为红黑树然后遍历效率要低一些,这时候进行扩容,那么重新计算哈希值链表的长度就有可能变短了,数据会放到数组中,这样相对来说效率高一些

4、其他

jdk1.7的HashMap与1.8的HashMap有什么不同?

(1)扩容计算方式不一样(前面已提)
(2)结构不同(前面已提)
(3)扩容后移动链表数据不同
JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法
因为JDK1.7是用单链表进行的纵向延伸,当采用头插法就是能够提高插入的效率,但是也会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题

最后,谢谢泥萌的观看,文中借用了别的博主的图,参考了很多的博客和视频,若有不正确欢迎指正,大家相互学习,嘻嘻

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值