HashMap源码解析

HashMap的结构是什么

  • JDK1.7

    底层使用数组+链表,维护了一个叫做Entry的内部类

  • JDK1.8

    底层使用数组+链表+红黑树结构,维护了Node(链表)类,实现了Map.Entry接口,TreeNode(红黑树),继承了LinkedHashMap.Entry

    • Node类
    static class Node<K,V> implements Map.Entry<K,V> {
        //Key计算的hash值
        final int hash;
        //Key
        final K key;
    	//Value
        V value;
    	//链表使用
        Node<K,V> next;
    }
    
    • TreeNode类
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        //头节点
        TreeNode<K,V> parent; 
        //左子树
        TreeNode<K,V> left;
    	//右子树
        TreeNode<K,V> right;
        TreeNode<K,V> prev;
        boolean red;
    }
    

HashMap的默认大小是多少

​ 默认大小为16(1 << 4)

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

为什么默认大小的写法是1 << 4,而不是直接写16?

  • 因为位运算比数计算的效率高

为什么默认大小是16?

  • 因为在使用2的幂数字时,Length - 1的二进制值全部为1,此时index的结果就等同于hashcode的后几位的值,当输入值本身的hashcode分布平均,则hash算法的分布就是平均的
  • 这样是为了实现均匀分布

为什么HashMap要重写HashCode方法?

  • 因为当map插入的时候,是根据key的hash计算出的index来插入的,如果两个key的index都为同样的,此时就形成了链表,如果不重写hashcode方法,那就无法保证同一个链表上的hashcode不同,那样在get的时候就会出现问题。
  • 重写hashcode方法,保证相同的对象返回相同的hash值,不同的对象返回不同的hash值。

HashMap的插入方式是什么

数组插入方式

​ 每次进行put的时候,会运行一个hash算法

  • hash算法

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

    根据这个hash算法得到一个index值,根据index的值进行插入,如果数组中index的值已经有了数据,那么就会在index值的位置形成链表

  • putVal方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //第一次运行,使用resize()进行数组初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)//数组中没有节点,直接插入
        //newNode创建一个新的node节点
        tab[i] = newNode(hash, key, value, null);
    else {//当前index有值,转换为链表/红黑树
        Node<K,V> e; K k;
        if (p.hash == hash && 
            ((k = p.key) == key || (key != null && key.equals(k))))//hash相同,且key相同
            //替换原节点
            e = p;
        else if (p instanceof TreeNode) //当前是红黑树结构
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else { //当前是链表
            //遍历链表,采取尾插法
            for (int binCount = 0; ; ++binCount) {
								//循环到最后一个节点,插入新元素
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //TREEIFY_THRESHOLD = 8 链表转换红黑树条件
                    //如果当前的循环次数大于等于7,就将链表转换为红黑树
                    //如果是TREEIFY_THRESHOLD - 1 = -1,则默认为1
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        treeifyBin(tab, hash);
                    break;
                }
                //Key相同,跳出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //保存当前节点,为次循环做准备
                p = e;
            }
        }
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //如果当前集合中的元素大于12个,则进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

链表插入方式

  • JDK1.7

    在jdk1.7及以前的版本,采用头插法,新值插入到头部,原有的值插入到新值的next节点,形成新链表

  • JDK1.8

    在jdk1.8中,改为了尾插法

为什么采用了尾插法

  • 使用头插法,每次插入都会改变链表的顺序,在扩容的时候,可能会出现环形链表,在多线程中,可能会出现死循环
  • 使用尾插法,保证了链表元素原本的顺序

HashMap取值

  • 通过传入的key计算出key的hash值,利用hash值去数组中寻找

  • getNode()

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; 
        Node<K,V> first, e; 
        int n; 
        K k;
        //当前table不是空,且当前tab的长度大于0,且当前hash计算出的index,并取得当前index的值
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //如果当前index的数据的hash与传入的相等,则直接返回
            if (first.hash == hash && 
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //此时为index取得的值可能为链表或红黑树
            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;
    }
    

HashMap扩容

HashMap什么时候扩容

  • 默认大小

    //1 << 4 = 16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    
  • 负载因子

    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    

    当元素存储到13个(16 * 0.75 = 12)的时候,进行扩容

HashMap怎么进行扩容

  • 1.创建新的空数组,大小为原数组的2倍大小

  • 2.遍历原数组,将元素组的数据重新Hash到新数组

  • resize方法

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //原数组的大小
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
    	//原数组扩容位置
        int oldThr = threshold;
        int newCap, newThr = 0;
        //如果oldCap大于0,表示为扩容
        if (oldCap > 0) {
            //MAXIMUM_CAPACITY = 1 << 30 (1073741824‬) map存储最大值
    		//Integer.MAX_VALUE = 0x7fffffff
            //如果大于最大值,则将返回旧的数组
            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
        }
        //此时是初始化,使用非默认构造方法
        else if (oldThr > 0) 
            newCap = oldThr;
        //此时是初始化,使用默认构造方法
        else {
            //使用空构造方法,则设置默认值
            //数组大小设置为16
            newCap = DEFAULT_INITIAL_CAPACITY;
    		//数组扩容点设置为16*0.75,为12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //使用非空构造方法,则在此处计算扩容点
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        //初始化数组或创建扩容后的新数组
        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;
                //判断当前数组的数据是否为空
                if ((e = oldTab[j]) != null) {
                    //原数组置空
                    oldTab[j] = null;
    				//此时为数组
                    if (e.next == null)
                        //e.hash & (newCap - 1)计算数据在新数组中的index
                        newTab[e.hash & (newCap - 1)] = e;
    				//此时为红黑树
                    else if (e instanceof TreeNode)
                        //调用TreeNode中的split方法
                        ((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);
                        //低位置的节点不是null,放入低位置
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //高位置的节点不是null,放入高位置
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    

为什么要重新刷新原数组的hash

  • 因为数组扩容后,Length-1就发生了改变,hash计算的规则也发生了改变,所以重新刷新

HashMap构造方法

/**
 * 默认构造方法(空)
 */
public HashMap() {
    //DEFAULT_LOAD_FACTOR = 0.75f
    //未传参数,则负载因子设置为默认值
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

/**
 * 构造方法(初始化Map大小)
 * @param:initialCapacity(Map数组大小)
 */
public HashMap(int initialCapacity) {
    //若初始化时传递了Map的大小,设置map的大小为initialCapacity,负载因子为默认值
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * 构造方法(初始化Map大小,负载因子)
 * @param:initialCapacity(Map数组大小)
 * @param:loadFactor(负载因子)
 */
public HashMap(int initialCapacity, float loadFactor) {
    //如果map的大小小于0,则直接抛出异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    //MAXIMUM_CAPACITY = 1073741824
    //如果map的大小大于设置的最大值
    if (initialCapacity > MAXIMUM_CAPACITY)
        //让map的大小等于最大值
        initialCapacity = MAXIMUM_CAPACITY;
    //如果负载因子小于等于0或负载因子为非法数值,直接抛出异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    //设置负载因子
    this.loadFactor = loadFactor;
    //设置扩容参数
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * 返回大于输入参数且最接近的2的幂数
 * @param:cap
 */
static final int tableSizeFor(int cap) {
    int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

/**
 * 根据传入的Map初始化HashMap
 * @param: Map<? extends K, ? extends V> m
 */
public HashMap(Map<? extends K, ? extends V> m) {
    //设置默认负载因子
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

/**
 * 根据传入的Map初始化HashMap
 * @param:cap
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //获取Map里的参数个数
    int s = m.size();
    //如果s大于0,才进行初始化
    if (s > 0) {
        //如果Node数组为空
        if (table == null) { 
            float ft = ((float) s / loadFactor) + 1.0F;
            int t = ((ft < (float) MAXIMUM_CAPACITY) ?
                     (int) ft : MAXIMUM_CAPACITY);
            if (t > threshold) {
                //设置数组扩容值
                threshold = tableSizeFor(t);
            }
        } else if (s > threshold) { //如果s的个数大于扩容值
            //数组扩容
            resize();
        }
        //数组初始化
        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);
        }
    }
}

HashMap删除元素

/**
 * Map删除元素
 */
public V remove(Object key) {
    Node<K, V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

/**
 * Map删除元素的真正方法
 * @param: matchValue 如果为true,只在值相等的时候删除
 * @param: 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;
    //如果当前的map不为空,且长度大于0
    if ((tab = table) != null && (n = tab.length) > 0 &&
        //(index = (n - 1) & hash) -> 确定当前的index值,等同于putVal()中的
        //当前key去出的值不为空
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K, V> node = null, e;
        K k;
        V v;
        //p为当前Key取出的Node对象
        //这里的第一个if为数组情况
        //判断p取出来的Key与传入的Key是否相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k)))) {
            //node为临时变量
            node = p;
        } else if ((e = p.next) != null) {//这里说明不在数组中,向下在链表/红黑树寻找
			//此时为红黑树
            if (p instanceof TreeNode) {
                //node赋值
                node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
            } else { //此时为链表
                //链表循环
                do {
                    //e为链表循环的临时变量
                    if (e.hash == hash && 
                        ((k = e.key) == key || (key != null && key.equals(k)))){
						//找到参数,跳出循环
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //!matchValue = true
        //node为要删除的元素
        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;
            }
            //HashMap修改次数+1
            ++modCount;
            //HashMap中元素-1
            --size;
            //空方法
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

HashMap其他方法

/**
 * 判断当前的Key是否存在于Map中
 * @param: key
 */
public boolean containsKey(Object key) {
    //根据Key的Hash值与Key值查找
    return getNode(hash(key), key) != null;
}

/**
 * Map清空
 */
public void clear() {
    Node<K, V>[] tab;
    //Map操作次数+1
    modCount++;
    if ((tab = table) != null && size > 0) {
        size = 0;
        //置空数组
        for (int i = 0; i < tab.length; ++i) {
            tab[i] = null;
        }
    }
}

/**
 * Map遍历 JDK1.8后新增
 * 使用了Consumer -> 消费型接口
 */
public final void forEach(Consumer<? super K> action) {
    Node<K, V>[] tab;
    if (action == null) {
        throw new NullPointerException();
    }
    if (size > 0 && (tab = table) != null) {
        int mc = modCount;
        for (Node<K, V> e : tab) {
            for (; e != null; e = e.next) {
                action.accept(e.key);
            }
        }
        if (modCount != mc) {
            throw new ConcurrentModificationException();
        }
    }
}

HashMap总结

HashMap的底层数据结构?

  • JDK1.7:数组+链表
  • JDK1.8:数组+链表+红黑树

HashMap的存取原理?

  • 每次put的时候调用hash方法,利用key的hashcode计算出数组存放的index,当数组大于12时扩容,若当前index有值,则转为链表,元素插入链表尾部,当链表元素大于7时,转为红黑树。

Java7和Java8的区别?

  • 链表插入从头插法转换为尾插法
  • 优化了链表,新增红黑树的数据结构

为啥会线程不安全?

  • 操作元素的put和get方法都没有加锁,在多线程同时操作的情况下,无法保证get出的值就是之前put的值

有什么线程安全的类代替么?

  • HashTable 方法上加锁,效率低(悲观锁)
  • ConcurrentHashMap 代码中加锁,效率高(乐观锁)

默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?

  • 默认大小16
  • 因为16属于折中方案
  • 因为使用2的幂次数可以保证后几位都为(0000),保证了数据均匀分配

HashMap的扩容方式?负载因子是多少?为什是这么多?

  • 创建二倍原数组大小的新数组,复制原数组至新数组
  • 0.75f
  • 提高空间利用率,减少查询成本,hash碰撞最小
    • 加载因子过高(1):减少空间开销,提高空间利用率,增加查询成本
    • 加载因子过低(0.5):减少查询承包,降低了空间利用率,加大了扩容几率

HashMap的主要参数都有哪些?

  • 初始默认大小

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //16
    
  • 数组最大大小

    static final int MAXIMUM_CAPACITY = 1 << 30; //1073741824
    
  • 负载因子

    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;
    
  • HashMap修改次数

    //当循环map的时候,modCount与expectedModCount不相等,则直接抛出异常
    //针对于并发问题,使用fail-fast策略
    transient int modCount;
    

什么是hash碰撞?

  • 当两个不同的元素计算出的index值相同,此时称为hash碰撞

HashMap是怎么处理hash碰撞的?

  • 在hash算法中有以下几步操作

    1. 计算出key的hashcode(h = key.hashCode())
    2. h的二进制右移16位(h >>> 16)
    3. h原值与右移后的数据进行异或运算(h ^ (h >>> 16))
    4. 结果等于数组大小 - 1 和第三步的结果进行与运算(index = (n - 1) & (h ^ (h >>> 16)))
  • hash()核心代码

    static final int hash(Object key) {
        int h;
        //这里是第三步
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
  • 确定index的代码

    //这里的n-1为数组大小 - 1,例如当前数组大小为16,此处就为16-1=15
    //这里的hash为上面代码计算出的hash值
    tab[i = (n - 1) & hash]
    

hash的计算规则?

  • 计算出key的hashcode,令此值的前后16位进行异或运算

当HashMap到达扩容临界点,两个线程同时到达,此时HashMap会发生什么

  • 两个线程分别进行扩容,当扩容完成后,会形成一个环形链表
  • 当此时,对环形链表的位置进行一个Get操作,且Get的Key值是不存在的,那么就会出现死循环,形成死锁
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值