HashMap详细讲解

目录

一、HashMap底层用到了哪些数据结构?

二、put方法流程

三、get方法流程 

四、HashMap为什么要用到链表结构?

五、 HashMap为什么要用到红黑树?

六、HashMap如何扩容的?

七、HashMap是线程不安全的

八、HashMap和HashTable的区别


一、HashMap底层用到了哪些数据结构?

        HashMap用到了数组,链表,红黑树。 数组中的每一个元素都是一个Key-Value键值对结构的Node对象/TreeNode对象,数组初始长度为16。

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

	 /** 16 初始容量
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /** 最大容量
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /** 扩容因子,元素个数达到容量的75%开始扩容
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /** 链表节点数达到8,转换为红黑树
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /** 红黑树节点个数小于6转为链表
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;
   
    ...省略...

	//一个 Node 代表一个key-value 键值对,也是数组中的一个元素
	static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//key的hash值	
        final K key;  //键
        V value;      //值
        Node<K,V> next; //指向下一个Node
   }


        结构图: 

二、put方法流程

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

        2.1、hash(key)方法讲解:首先判断key是否为null,如果为null,则返回0;如果不为null,则将获取key的hashCode和h>>>16进行位异或运算,将算出来的结果返回。可以得出个结论:key/键可以为null值。

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

        2.2、然后开始讲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;
        //😊1.如果数组是空的,调用 resize() 初始化数组
        if ((tab = table) == null || (n = tab.length) == 0)
            //😊tab是数组,n为数组的长度
            n = (tab = resize()).length;

        //😊2.计算位置: (n - 1) & hash  等同于   hash % 16 ,计算key的存储位置
        //😊3.取出该位置的元素是否为空
        if ((p = tab[i = (n - 1) & hash]) == null)
            //😊如果为空,就创建一个新Node,把当前的key-value存储进去
            tab[i] = newNode(hash, key, value, null);

        else {
            //😊到这里说明hash冲突了,该位置有元素,那么就有2种执行方案:
            //😊如果有和当前传入key相同的key存住,覆盖值,如果没有和传入的key相同的key,那就添加元素
            Node<K,V> e; K k;
            //😊5.如果key相等,hash值也相等,进行值的覆盖
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

            //😊6.如果不满足第5步,那就要进行链表或者红黑树的遍历了 , 这里在判断是不是红黑树
            else if (p instanceof TreeNode)
                //😊走红黑树的添加流程
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

            else {
                //😊7.到这里就是链表了
                //😊8.通过next一个一个往下遍历,如果某一个node的next为空 ,说明找到最后一个Node了,
               	//就把新的key-value加入最后一个元素的next
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //😊这里在判断是否要做红黑树转变
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //😊9.如果在遍历的过程中,找到了和传入的key一样的key,hash值也一样,那就进行值的覆盖
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
           
            if (e != null) { // existing mapping for key
                //😊10.用新的值覆盖老的值,然后将旧值返回去
                V oldValue = e.value;   
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

        总结一下:首先判断table是否为空,如果为空,则初始化table。如果不为空则进行添加元素,先通过(n-1)&hash计算出在数组的索引值,然后判断该索引是否有数据,如果没有则直接创建一个Node存到该索引上,如果有则说明该索引上已有数据(称为hash冲突),那么就有两种情况:第一种情况是当该索引上的Node的key是否与我们传进来的key相同并且hash相同,如果相同,则覆盖value;如果不相同,则就是第二种情况,先判断是否是红黑树,如果是,则走红黑树的添加流程;如果不是,则走链表的添加流程,通过循环找到next为null的Node,当在循环的过程中,如果找到了和传入的key一样的key,hash值也一样,那么就覆盖value;如果没有,则将传进来的hash、key、value封装到成一个Node对象,使next为null的Node指向该新建的Node,然后进行判断是否需要转换为红黑树。


三、get方法流程

 public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(key)) == null ? null : e.value;
    }

        3.1、如果getNode能get到对应key的值就返回对应的值,如果没有,则返回null值。可以得出一个结论:value可以为null

        3.2、我们来看看它是怎么getNode的

final Node<K,V> getNode(Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
        //😊 1.首先判断数组是否为空,如果为空,则直接返回null
        if ((tab = table) != null && (n = tab.length) > 0 &&
            //😊计算key存储位置,取出该位置的元素,判断该索引上是否有数据,如果无元素则直接返回null
            (first = tab[(n - 1) & (hash = hash(key))]) != null) {
            //😊2.检查是不是第一个元素,比较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) {
                //😊3.如果是红黑树,就遍历红黑树
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    //😊一直next遍历下一个元素,然后比较key,知道遍历完
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        //经过以上操作都没有get到,那么就返回null
        return null;
    }

        总结一下:首先判断数组是否为空,如果为空,则直接返回null;如果不为空,则进行寻找。通过(n - 1) & (hash = hash(key))计算在数组的索引,然后判断第一个元素是否是该元素,如果是就直接返回元素;如果不是,那就再继续寻找。先判断该索引上是红黑树还是链表,如果是红黑树,则遍历红黑树进行寻找元素;如果是链表,则遍历链表进行寻找元素。


四、HashMap为什么要用到链表结构?

         使用链表是为了解决Hash冲突问题,这就要说到HashMap存储元素的位置映射算法了,HashMap通过 hash(key) % 16 算法来计算某个key在数组中的存放位置 ,通过hash算法得到 key的hash值,对数组长度(16)取余数,结果为0到15其中一个数字,正好对应数组的索引位置。

        而Hash冲突就是两个不同的 key ,使用Hash函数得到了相同的Hash值(这是有可能的),这就意味着两个各不同键值对计算出了相同的存储位置(一个位置怎么能放两个键值对呢?)这就是Hash冲突。

        HashMap使用 链地址法 来解决Hash冲突,其实就是把相同位置的元素以链表的方式进行存储,如下:

        你现在应该知道Node节点对象中的 next 的作用了。


五、 HashMap为什么要用到红黑树?

        HashMap使用红黑树是为了解决链表过长,查询变慢的问题大家都知道红黑树的查询性能是及其可观的。

        如果HashMap出现Hash冲突的情况变多,那么链表的长度会越来越长,由于链表的查询方式是从前往后一个一个遍历,查询速度比较慢,所以当链表长度达到 8 的时候 ,HashMap会把链表变成一个红黑树结构来提高查询效率 , 当删除元素,红黑树的节点数小于6又会把红黑树变回链表 , 下面这2个字段就是红黑树和链表的转换阈值:

    /** 链表节点数达到8,转换为红黑树
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /** 红黑树节点个数小于6转为链表
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;


六、HashMap如何扩容的?

        HashMap的扩容因子是0.75,也就是说当数组中的已存储节点数(Node) > 数组容量 ∗ 扩容因子时,就需要扩容,调整数组的大小为当前的 2 倍

 	/** 扩容因子,元素个数达到容量的75%开始扩容
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

七、HashMap是线程不安全的

        HashMap暴露给外界的方法并不是同步的,在多线程环境中是可能会出现线程安全问题,在多线程环境中可以使用HashTable ,或者ConcurrentHashMap。 HashTable直接使用 synchronized 同步锁来保证线程安全,比较古老现在基本不用了。ConcurrentHashMap是JUC并发库中的容器 ,它是专门为多线程环境设计,并发控制使用 Synchronized 和 CAS 来操作,性能良好,在JDK1.8中ConcurrentHashMap也使用到了数组链表红黑树的结构来存储数据。


八、HashMap和HashTable的区别

        HashTable是HashMap的线程安全版本,也是比较老旧的一个map,HashTable方法是加了同步锁,线程安全,性能会受到影响。而HashMap没有加同步锁,线程不安全,但是性能更高。      

        Hashtable的key和value都不支持null , 而HashMap值可以为null,key支持一个key为nul。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

b顶峰相见

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

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

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

打赏作者

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

抵扣说明:

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

余额充值