Java—HashMap介绍及源码分析

HashMap数据结构

HashMap是一种散列表

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

HashMap数据的数据结构:数组 + 链表 + 红黑树(jdk1.8)。jdk1.7是用的数组+链表,1.8增加了链表和树的转换。

在这里插入图片描述

简单操作流程:当设置值时,把传入的key(key.hashCode())通过hash算法算出这个key在数组哪个下标位置,然后判断此位置的元素的key是否与传入的key相等(“newKey”.equals(“oldKey”)),相等则代表是该元素返回,如果不是,则走链表或者红黑树的查询其余节点逻辑,继续判断key是否相等。所以我们能影响到HashMap的内部源码的逻辑的只有传入key的hashCode()方法和equals()方法,如果有特殊需求需要实现,可以重写hashCode()和equals()进行取值判断。

需要了解的知识点:链表、红黑树。这里不做介绍,只分析HashMap源码和逻辑

HashMap中属性值介绍

  • DEFAULT_INITIAL_CAPACITY :默认容量值,HashMap中数组的大小,默认16
  • MAXIMUM_CAPACITY:数组的最大值,扩容的时候来限制不能超过最大值,默认1073741824
  • DEFAULT_LOAD_FACTOR:默认负载因子,扩容需要用到的值,可以理解为一个百分比,数组用到的容量超过这个会触发扩容操作,默认0.75
  • TREEIFY_THRESHOLD:树化阈值,当链表长度超出这个阈值,可能会(转换红黑树还受MIN_TREEIFY_CAPACITY限制)转换为红黑树,默认8
  • UNTREEIFY_THRESHOLD:树降级阈值,当树的长度小于这个值,红黑树会退货为链表,默认6 。(树化和树降级的阈值相差2,为了防止频繁的树化和退化,影响性能。)
  • MIN_TREEIFY_CAPACITY:树化的阈值,当HashMap中的数组长度大于该值,才允许转换为红黑树。默认64

HashMap内的字段

  • size:hash表中元素的个数
  • modCount:hash表被操作的次数(插入和删除,插入相同key不算)
  • threshold:扩容阈值(扩容阈值 = 数组长度 * 负载因子(DEFAULT_LOAD_FACTOR)),当hash表中长度超过扩容阈值触发扩容
  • loadFactor:负载因子,创建HashMap如果没传入负载因子直接等于默认的负载因子DEFAULT_LOAD_FACTOR

HashMap构建方法及使用场景

HashMap提供的4中构造方法

  1. HashMap(int initialCapacity, float loadFactor):自定义数组大小和负载因子

    自定义数组大小和负载因子,不建议使用,正常需求情况下没必要去传入它的负载因子。当然如果有特殊需求,不想让他超过默认的负载因子就进行扩容可以用这个构造方法。

    /**
     * initialCapacity初始化数组大小
     * loadFactor 负载因子 默认0.75f
     */
    public HashMap(int initialCapacity, float loadFactor) {
        //数组大小不可以小于0
        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;
        //数组的长度必须是2的多少次方,tableSizeFor方法把传进来的数组处理
        this.threshold = tableSizeFor(initialCapacity);
    }
    
  2. HashMap(int initialCapacity):传入数组大小

    这个方法如果熟悉HashMap应该经常被使用到,因为有些需求可能已经明确了会存入多少个key,或者预计有多少个key,也有可能有一些大量数据需要放入一个HashMap中然后通过key去判断是否存在,把初始化数组大小传入估计值,可以避免频繁扩容,影响性能。

    /**
     * initialCapacity 初始化数组大小
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
  3. HashMap():不传入任何参数,一切使用默认值

    这个经常使用到,没什么好说的。

    /**
     * 不传任何参数,所有属性用默认值
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    
  4. HashMap(Map<? extends K, ? extends V> m):根据一个Map去构建一个Map

    根据一个已有map去构建一个新的map,底层逻辑就是去循环已有map的所有值放入新的map中,不建议使用,遍历的时候如果达到扩容阈值还会有扩容的操作,不如自己写一个。

    /**
     * 根据一个Map去构建一个Map
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        //遍历设置值
        putMapEntries(m, false);
    }
    

小结:HashMap在构建的时候,并没有去创建里面的散列表,仅仅是创建了一个对象,设置了操作中用到的值。创建方法4是例外,它本身就是在创建的时候去设置之前map的值了。

HashMap中的put方法介绍

HashMap在调用put方法时会判断散列表是否被创建,如果没被创建则会调用扩容方法去创建一个散列表。创建完散列表后(也可能没走创建逻辑)根据HashMap的哈希算法算出数组的下标位置,如果下标位置为null,则把当前节点封装成Node对象(链表)存入该位置。如果数组下标位置不为null(哈希冲突导致),则判断是否为红黑树(p instanceof TreeNode),如果是红黑树,去走红黑树取结点的逻辑,取出传入的key对应的红黑树中的结点,如果不是红黑树则走链表逻辑取出传入的key对应链表的结点,在链表逻辑中会判断结点是否大于等于树化阈值和数组长度大于64,如果成立则要进行树化,然后根据取出的结点,去设置新值,返回旧值。如果是新节点插入(没有与新传进来的key相等),会判断数组大小是否大于扩容阈值,如果大于,进行扩容操作。

@Override
public V putIfAbsent(K key, V value) {
   return putVal(hash(key), key, value, true, true);
}


/**
 * put方法,算出hash值调用putVal
 * 与putIfAbsent比较 传入的onlyIfAbsent不同
 */
public V put(K key, V value) {
    //(hash(key)算出hash值,
    return putVal(hash(key), key, value, false, true);
}

/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent 如果传true,则key有值不设置值
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //tab : 散列表的引用
    //p : 设置值时,通过hash值找到的数组位置的Node
    //n : 散列表数组长度
    //i : 路由寻址结果
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果table为null或者长度为0则去初始化散列表。第一次往HashMap中put数据时,才会初始化散列表
    if ((tab = table) == null || (n = tab.length) == 0)
        //创建散列表
        n = (tab = resize()).length;
    //如果put值的地方为null,则直接将传进来的key,val构建一个Node,tab[i] 指向新创建的Node
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        /*put值的位置不为null的情况*/
        
        //e : 数组中寻到node结点。不为null,表示hash值的数组位置已经有元素
        //k : 在数组中寻到结点的key
        Node<K,V> e; K k;
        //如果hash值相等,切key值相等,则直接e指向p。(链表头结点或者树的跟结点就是要操作的位置)
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
            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) {
                //如果链表下一个元素为null,说明到链表末尾,创建一个新的Node加到末尾
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //如果元素大于等于树化的值,转为红黑树。从0开始循环,所以TREEIFY_THRESHOLD - 1
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //如果找到了hash值一样,切key相同的元素,跳出循环,执行下面替换逻辑
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //如果e不为null,说明HashMap里已经有该key的值,这里做替换值,和返回之前的值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);//空方法,可以继承重写此方法
            return oldValue;
        }
    }
    //modCount表示散列表结构被修改(插入、删除)次数,替换元素不算
    ++modCount;
    //如果散列表元素个数大于扩容的阈值,则进行扩容操作
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);//空方法,可以继承重写此方法
    //插入返回null
    return null;
}

/*
 * 作用:让key的hash值的高16位也参与路由运算
 */
static final int hash(Object key) {
    int h;
    //key是null时,hash值为0,在数组第一位
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

put方法小结:HashMap调用put方法是会初始化HashMap的散列表。HashMap的put方法,是通过key的hash值算出所在数组下标,如果有元素则去判断key值是否相等,有元素的情况是因为哈希冲突。插入新元素会根据扩容阈值判断是否需要对HashMap中的散列表进行扩容。

HashMap中get方法介绍

HashMap中的get方法,获取key的value值,通过key算出hash值,然后算出所属的数组位置,获取的该数组位置的元素,如果元素的key值等于传入的key值,则返回该元素,如果key值不相等则判断有下一个结点么,然后判断是红黑树走红黑树获取结点的逻辑,是链表走链表获取结点的逻辑,本质上与put差不多。

/**
 * 算出key的hash值调用getNode方法
 */
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
 * Implements Map.get and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @return the node, or null if none
 */
final Node<K,V> getNode(int hash, Object key) {
    //tab:引用当前hashMap的散列表
    // first:散列表位置元素的引用 
    // e:临时引用
    // n:数组长度(n = tab.length)
    //k;;临时引用key
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //判断散列表不为空,然后根据key算出的hash值找到散列表中的第一个元素(这个元素可能是链表或者红黑树)
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //第一种情况:散列表中对应下标位置的元素第一个元素(链表或树的第一元素)为想要的元素(key.equals(k)),直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //第二种情况:当前数组下标位置不为一个元素且第一个元素的key与找出来的node元素不相等
        if ((e = first.next) != null) {
            //如果是红黑树,走红黑树逻辑
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            
            //不是红黑树就是链表jdk1.8,链表下一个元素不为null,一直循环查找,找到返回,未找到最后返回null
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

HashMap中扩容介绍

扩容的目的:为了防止hash冲突太多,链化严重影响查询效率,所以要进行扩容

扩容代码逻辑总共分为两部分:

  1. 计算扩容后的散列表大小和扩容阈值
  2. 扩容后把旧散列表数组赋值到新散列表上
/**
 * 为了防止hash冲突太多,链化严重影响查询效率,所以要进行扩容
 */
final Node<K,V>[] resize() {
    //oldTab扩容之前的哈希表
    Node<K,V>[] oldTab = table;
    //扩容之前的数组大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //触发本次扩容的阈值
    int oldThr = threshold;
    //newCap代表扩容之后数组大小,newThr扩容之后,下次扩容的阈值
    int newCap, newThr = 0;
    
    /*------------------------计算扩容后的散列表大小和扩容阈值---------------------------------*/
    
    //oldCap说明散列表已经初始化过,正常的扩容操作
    if (oldCap > 0) {
        //如果散列表数组大小已经是最大值,则不进行扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //oldCap左移移位,实现数值翻倍,并赋值给新的数组大小newCap
        //如果新的数组大小,小于最大数组大小,切之前数组大小大于等于默认大小,则数组新的扩容阈值左移一位翻倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    
    /**
    *oldCap == 0 且 oldThr有值的情况
    * 1.new HashMap(initCap,loadFactor); 2.new HashMap(initCap);3.new HashMap(map);(map有数据)
    */
    //oldCap == 0时(HashMap散列表未被初始化)
    else if (oldThr > 0) // initial capacity was placed in threshold
        //扩容后散列表大小的值等于阈值
        newCap = oldThr;
    else {   
        //oldCap == 0 且 oldThr == 0时。new HashMap()时有这种情况
        //扩容散列表大小、扩容阈值设置成默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        //扩容阈值 = 负载因子 * 数组长度
        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;
    
    /*------------------------核心扩容逻辑代码---------------------------------*/
    
    @SuppressWarnings({"rawtypes","unchecked"})
    	//创建新数组。根据上面算出来新数组大小创建
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //将引用指向新数组
    table = newTab;
    
    //之前的数组不为null,进行扩容
    if (oldTab != null) {
        
        //循环扩容之前的数组
        for (int j = 0; j < oldCap; ++j) {
            //临时存储元素,指向被操作的Node
            Node<K,V> e;
            //当前数组下标位置有元素是向下操作,e指向当前操作的元素
            if ((e = oldTab[j]) != null) {
				//将数组对元素的引用设为null,循环结束方便jvm垃圾回收
                oldTab[j] = null;
                if (e.next == null)
                    //如果e的下个元素为null,计算出当前元素在新数组中的位置放入
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    //如果是红黑树走这里
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    //当前数组位置已经是链表
                    
                    //低位链表:存放扩容之后的数组的下标位置,与当前数组的下标位置一致
                    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的扩容,只有两个阈值条件都成立,才会触发扩容。扩容的逻辑就是,创建一个比之前大一倍的数组,将旧数组中的元素,放到新数组中对应的位置。

HashMap小结

HashMap中的操作,主要是对数组、链表、红黑树的操作。数组的性质就是可以通过下标直接得到元素,时间复杂度为O(1),所以通过key的hash值算出数组所在位置速度很快,而且散列表插入的时候是插入到已有的位置,当位置存在元素会存成链表的形式(jdk1.7只有链表),这样就解决了哈希冲突的位置,而对链表就行插入和删除的时间复杂度都是O(1),只需要改变前结点指针所(java中的引用)指向的位置。因为链表查询元素的时间复杂度是O(n),所以jdk1.8链表超过阈值会转成红黑树优化查询效率。个人理解。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值