品HashMap(java8)

逆变珊瑚
• 阅读 113

前言

作为java开发人员,HashMap可谓是业务中的一把利器,9龙再次捡起这老生常谈的知识点,深入源码,细细品味。

首先,我们抛出几个关于HashMap的问题,带着问题去学习,就像捉迷藏一样有意思。

1、为什么要使用HashMap?HashMap有什么特性?

2、HashMap的主要参数有哪些?都有什么作用?

3、HashMap是基于什么数据结构实现的?

4、构造HashMap时传入的初始容量是如何处理的?为什么要这样做?

5、HashMap在什么时候扩容?扩容的时候都做了什么事?hash碰撞8次一定会转换为红黑树吗?

6、在foreach时对hashMap进行增删操作会发生什么?

1、为什么要使用HashMap?

我们在使用一种工具的时候,肯定是因为其的某种特性很符合我们的需求,能够快速准确的解决我们的问题。那我们为什么要使用HashMap呢?

This implementation provides constant-time performance for the basic operations (get and put), assuming the hash function disperses the elements properly among the buckets.

源码注释里有这样一句话,这就是我们使用HashMap的原因。

意为:HashMap为基本操作(get和put)提供了常数时间性能(即O(1)),假设散列函数将元素适当地分散到各个bucket中。

我们可以这样理解,如果当你需要快速存储并查询值,可以使用HashMap,它可以保证在O(1)的时间复杂度完成。前提是你键的hashCode要足够不同。

Map还有一个特性就是key不允许重复。下面我们就来看看HashMap如何保证O(1)进行get和put。

2、细嚼HashMap主要参数2.1、静态常量    //默认的初始化桶容量,必须是2的幂次方(后面会说为什么)    staticfinal int DEFAULT_INITIAL_CAPACITY = 1 << 4;    //最大桶容量    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;    //判断是否可以执行将链表转化为树,如果当前桶的容量小于此值,则进行resize()。避免表容量过小,较容易产生hash碰撞。    static final int MIN_TREEIFY_CAPACITY = 64;2.2、字段    //hash表    transient Node<K,V>[] table;    //缓存的EntrySet,便与迭代使用    transient Set<Map.Entry<K,V>> entrySet;    //记录HashMap中键值对的数量    transient int size;    //当对hashMap进行一次结构上的变更,会进行加1。结构变更指的是对Hash表的增删操作。    transient int modCount;    //判断是否扩容的阈值。threshold = capacity * load factor    int threshold;    //负载因子,用于计算threshold,可以在构造函数时指定。    final float loadFactor;3、嗅探HashMap数据结构

上面我们看到一个Node<K,V>[] table的Node数组。

为什么要使用数组呢?

答:为了能快速访问元素。哦,说的什么鬼,那我得追问,为什么数组能快速访问元素了?

数组只需对 [首地址+元素大小*k] 就能找到第k个元素的地址,对其取地址就能获得该元素。

CPU缓存会把一片连续的内存空间读入,因为数组结构是连续的内存地址,所以数组全部或者部分元素被连续存在CPU缓存里面。

让我们看看Node的结构。

static class Node<K,V> implements Map.Entry<K,V> {        final int hash; //key 的hash        final K key;    //key对象        V value;        //value对象        Node<K,V> next; //链接的下一个节点        Node(int hash, K key, V value, Node<K,V> next) {            this.hash = hash;            this.key = key;            this.value = value;            this.next = next;        } }

我们看到,Node节点内部保留了一个next节点的引用,太熟悉了,这不就是链表嘛。

到这,我们知道了HashMap的底层数据结构是基于数组+链表。但是,这就完了吗?在jdk1.7确实只是这样,jdk1.8为了提高hash碰撞时链表查询效率低的问题,在hash碰撞达到8次之后会将链表转化为红黑树,以至于将链表查询的时间复杂度从O(N)提高到O(logN)。

到这我们就可以明白,HashMap如果能够均匀的将Node节点放置到table数组中,我们只要能够通过某种方式知道指定key的Node所在数组中的索引,基于数组,我们就可以很快查找到所需的值。

接着我们就要看看如何定位到table数组中。

4、走进HashMap构造函数

有了上面的基础知识,知道字段含义及数据结构,我们就有一点信心可以正式进入源码阅读。我觉得了解一个类,得从构造函数入手,知道构造对象的时候做了哪些初始化工作,其次再深入常用的方法,抽丝剥茧。

    public HashMap(int initialCapacity) {        //如果只传入初始值,则负载因子使用默认的0.75        this(initialCapacity, DEFAULT_LOAD_FACTOR);    }   public HashMap(int initialCapacity, float loadFactor) {        if (initialCapacity < 0)            throw new IllegalArgumentException("Illegal initial capacity: " +                                               initialCapacity);       //保证初始容量最大为2^30        if (initialCapacity > MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;        if (loadFactor <= 0 || Float.isNaN(loadFactor))            throw new IllegalArgumentException("Illegal load factor: " +                                               loadFactor);       //使用指定的值初始化负载因子及判断是否扩容的阈值。        this.loadFactor = loadFactor;        this.threshold = tableSizeFor(initialCapacity);    }

我们可以看到,构造函数主要是为了初始化负载因子及hash表的容量。可能大家会疑问,这不是初始化的是threshold吗?不要被表面所欺骗,这只是临时将hash表的容量存储在threshold上,我想是因为HashMap不想增加多余的字段来保存hash表的容量,因为数组的length就可以表示,只是暂时数组还未初始化,所以容量暂先保存在threshold。

我们看到将用户指定的initialCapacity传入tableSizeFor方法返回了一个值,返回的值才是真正初始化的容量。???搞毛子这是?然我们揭开它神秘的面纱。

/** * Returns a power of two size for the given target capacity. */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;    }

好吧, 我们还是把它盖上吧,9龙也没去推算过。我们从jdk给的方法注释看出,该方法返回一个目标值的2的幂次方,进一步9龙翻译为:返回大于或等于目标值的第一个数,该数必须是2的幂次方。

举例说一下:

如果输入10,大于等于10的第一个数,又是2的幂次方的数是16;

如果输入7,大于等于7的第一个数,又是2的幂次方的数是8;

如果输入20;大于等于20的第一个数,又是2的幂次方的是32;

到这我们又得问自己,为什么hash表的容量必须是2的幂次方呢?

5、解剖HashMap主要方法5.1、put

当我们new出HashMa的对象,都会调用put方法进行添加键值对。我跟那些直接贴代码的能一样吗?有啥不一样,哈哈哈。9龙会先读源码,再贴流程图,这样大家会更理解一点。

public V put(K key, V value) {    return putVal(hash(key), key, value, false, true);}static final int hash(Object key) {        int h;    //将key的高16位与低16位异或,减小hash碰撞的机率        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);    }

让我们看看putVal干了什么。

    /**     * 此方法用于将(k,v)键值对存储到HashMap中     *     * @param hash key的hash     * @param key key对象     * @param value key对应的value对象* @param onlyIfAbsent 如果是true,则不覆盖原值。* @param evict if false, the table is in creation mode.     * @return 返回旧值,如果没有,则返回null。     */    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                   boolean evict) {        Node<K,V>[] tab; Node<K,V> p; int n, i;        //在第一次put的时候,此时Node表还未初始化,上面我们已经知道,构造HashMap对象时只是初始化了负载因子及初始容量,但并没有初始化hash表。在这里会进行第一次的初始化操作。        if ((tab = table) == null || (n = tab.length) == 0)            n = (tab = resize()).length;        //如果得到了一个hash值,并且hash值在很少相同的情况下,如何均匀的分布到table数组里呢?最容易想到的就是用hash%n,n为table数组的长度。但是%运算是很慢的,我们知道位运算才是最快的,计算机识别的都是二进制。所以如果保证n为2的幂次方,hash%n 与 hash&(n-1)的结果就是相同的。这就是为什么初始容量要是2的幂次方的原因。        //当找到的hash桶位没有值时,直接构建一个Node进行插入        if ((p = tab[i = (n - 1) & hash]) == null)            tab = newNode(hash, key, value, null);        else {            //否则,表明hash碰撞产生。            Node<K,V> e; K k;            //判断hash是否与桶槽的节点hash是否相同并且key的equals方法也为true,表明是重复的key,则记录下当前节点            if (p.hash == hash &&                ((k = p.key) == key || (key != null && key.equals(k))))                e = p;            //如果桶槽节点是树节点,则放置到树中,并返回旧值            else if (p instanceof TreeNode)                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);            else {                //表明是链表,还未转换为红黑树。                for (int binCount = 0; ; ++binCount) {                    //如果节点的next索引是null,表明后面没有节点,则使用尾插法进行插入                    if ((e = p.next) == null) {                        p.next = newNode(hash, key, value, null);                        //此时链表长度为9,即hash碰撞8次,会将链表转化为红黑树                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                            treeifyBin(tab, hash);                        break;                    }                    //如果key是同一个key,则跳出循环链表                    if (e.hash == hash &&                        ((k = e.key) == key || (key != null && key.equals(k))))                        break;                    p = e;                }            }            //判断是否是重复的key            if (e != null) { // existing mapping for key                //拿到旧值                V oldValue = e.value;                //因为put操作默认的onlyIfAbsent为false,所以,默认都是使用新值覆盖旧值                if (!onlyIfAbsent || oldValue == null)                    e.value = value;                afterNodeAccess(e);                //返回旧值                return oldValue;            }        }        //到这里,表明有新数据插入到Hash表中,则将modCount进行自增        ++modCount;        //判断当前键值对容量是否满足扩容条件,满足则进行扩容        if (++size > threshold)            resize();        afterNodeInsertion(evict);        return null;    }

总结一下

put方法先通过计算key的hash值;

如果hash表没有初始化,则进行初始化;

然后计算该hash应该处于hash桶的哪个位置;

如果该位置没有值,则直接插入;

如果有值,判断是否为树节点,是的话插入到红黑树中;

否则则是链表,使用尾插法进行插入,插入后判断hash碰撞是否满足8次,如果满足,则将链表转化为红黑树;

插入后判断key是否相同,相同则使用新值覆盖旧值;

进行++modCount,表明插入了新键值对;再判断是否进行扩容。、

18�5

点赞
收藏
评论区
推荐文章
九路 九路
4年前
3 手写Java HashMap核心源码
手写JavaHashMap核心源码上一章手写LinkedList核心源码,本章我们来手写JavaHashMap的核心源码。我们来先了解一下HashMap的原理。HashMap字面意思hashmap,map是映射的意思,HashMap就是用hash进行映射的意思。不明白?没关系。我们来具体讲解一下HashMap的原理。HashMap
待兔 待兔
1年前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
java集合系列之HashMap源码
java集合系列之HashMap源码HashMap的源码可真不好消化!!!首先简单介绍一下HashMap集合的特点。HashMap存放键值对,键值对封装在Node(代码如下,比较简单,不再介绍)节点中,Node节点实现了Map.Entry。存放的键值对的键不可重复。jdk1.8后,HashMap底层采用的是数组加链表、红黑树的数据结构,因此实现起
御弟哥哥 御弟哥哥
4年前
HashMap深度解析:一文让你彻底了解HashMap
前言HashMap是Map族中最为常用的一种,也是JavaCollectionFramework的重要成员。本文首先给出了HashMap的实质并概述了其与Map、HashSet的关系,紧接着给出了HashMap在JDK中的定义,并结合源码分析了其四种构造方式。最后,通过对HashMap的数据结构、实现原理、源码实现三
浪人 浪人
4年前
拿下面试!HashMap源码解析!!
拿下面试!HashMap源码解析!!HashMap的设计思想HashMap的底层结构本文主要是讲解jdk1.8中的HashMap源码,会对jdk1.7中的HashMap做一些简单的讲解用来和jdk1.8中的HashMap进行对比。我们先通过下图来理解HashMap的底层结构:(https
Stella981 Stella981
3年前
GridView实现九宫格
GridViewgv(GridView)findViewById(R.id.g1);    ArrayList<HashMap<String,ObjectdatanewArrayList<HashMap<String,Object();    for(inti0;i<images.length;i)
Stella981 Stella981
3年前
HashMap原理学习
概述HashMap对于做Java的小伙伴来说太熟悉了。估计你们每天都在使用它。它为什么叫做HashMap?它的内部是怎么实现的呢?为什么我们使用的时候很多情况都是用String作为它的key呢?带着这些疑问让我们来了解HashMap!HashMap介绍1、介绍HashMap是一个用”KEY”“VALUE”
Stella981 Stella981
3年前
HashMap源码分析 JDK1.8
本文按以下顺序叙述:HashMap的感性认识.官方文档中对HashMap介绍的解读.到源码中看看HashMap这些特性到底是如何实现的.把源码啃下来有一种很爽的感觉,相信你读完后也能体会到~如发现有误,欢迎指出.<h3id1在开始之前,先通过图例对HashMap建立感性认识</h3
Wesley13 Wesley13
3年前
10 HashSet HashMap源码 Properties
2HashSet底层是使用HashMap实现的。当使用add方法将对象添加到Set当中时,实际上是将该对象作为底层所维护的Map对象的key,而value则都是同一个Object对象(该对象我们用不上);3\.HashMap底层维护一个Node数组,我们向HashMap中所放置的对象实际上是存储在该数组当中;HashMap中的Pu
Stella981 Stella981
3年前
HashMap 为什么会导致 CPU 100%?文章看不懂?来看这个视频吧!——面试突击 006 期
无论是在实际工作中还是在面试中,HashMap无疑是使用频率最高的知识点之一,所以我们需要搞懂每一个关于HashMap的知识点才行。哈喽,大家好,我是老王,欢迎来到Java面试突击,我们今天来开始第6期的内容。本期的问题是:HashMap为什会导致CPU运行100%?这是一个比较常见的经典问题了,但是有很多人读者朋友给我反馈
小万哥 小万哥
1年前
Java HashMap 和 HashSet 的高效使用技巧
JavaHashMapHashMap是一种哈希表,它存储键值对。键用于查找值,就像数组中的索引一样。HashMap的优势在于它可以使用任何类型作为键,并且查找速度很快。创建HashMapjava//导入HashMap类importjava.util.Has