Map集合之HashMap

2022-09-12 09:26:15

HashMap 概述

HashMap 是通过put(key,value) 存储,get(key)来获取。当传入key 时,HashMap 会根据keyhashCode() 方法计算出hash 值,根据hash 值将value 保存在bucket(桶)里。当计算出的hash 值相同时,称之为hash 冲突。HashMap 的做法是用链表和红黑树存储相同hash 值的value

jdk 1.8 之前与之后的HashMap

  • jdk1.8 之前的HashMap 是由数组 + 链表来实现的,数组是HashMap 的主体。链表主要是为了解决hash 冲突的
  • jdk1.8 之后的HashMap 是由数组 + 链表 + 红黑树来实现的,在解决hash 冲突时有了较大的变化。当链表长度大于阈值8 时,并且数组的长度大于64 时,此时此索引位置上的所有数据改为使用红黑树存储

在这里插入图片描述

HashMap 的数组,链表,红黑树之间的转换

  • 当创建HashMap 集合对象的时候,在jdk1.8 之前,是在它的构造方法中创建了一个默认长度是16Entry[] table 的数组来存储键值对数据的。而从jdk1.8开始,是在第一次调用put 方法时创建了一个默认长度是16Node[] table 的数组来存储键值对数据的
  • 数组创建完成后,当添加一个元素(key,value)时,首先计算元素keyhash 值,以此确定插入数组中的位置。但是可能存在同一hash 值的元素已经被放在数组同一位置了,这时就添加到同一hash 值的元素的后面,他们在数组的同一位置,这就形成了单链表,同一各链表上的Hash 值是相同的。当链表长度大于阈值8 时,并且数组的长度大于64 时,此时此索引位置上的所有数据改为使用红黑树存储,这样大大提高了查找的效率
  • 在转换为红黑树存储数据后,如果此时再次删除数据,当红黑树的节点数小于6 时,那么此时的红黑树将转换为单链表结构来存储数据

HashMap 扩容机制

默认情况下,数组大小为16,那么当HashMap 中元素个数超过16 * 0.75 = 12(这个值就是代码中的threshold 值,也叫做临界值)的时候,就把数组的大小扩展为2*16 = 32,即扩大一倍,然后重新计算每个元素在数组中的位置

0.75 这个值称为负载因子,那么为什么负载因子为0.75? 这是通过大量实验统计得出来的,如果过小,比如0.5,那么当存放的元素超过一半时就进行扩容,会造成资源的浪费;如果过大,比如1,那么当元素满的时候才进行扩容,会使get,put 操作的碰撞几率增加

HashMap 源码

HashMap 的基本属性

publicclassHashMap<K,V>extendsAbstractMap<K,V>implementsMap<K,V>,Cloneable,Serializable{// 序列号privatestaticfinallong serialVersionUID=362498820763181265L;// 默认的初始容量是16staticfinalint DEFAULT_INITIAL_CAPACITY=1<<4;// 最大容量staticfinalint MAXIMUM_CAPACITY=1<<30;// 默认的填充因子staticfinalfloat DEFAULT_LOAD_FACTOR=0.75f;// 当桶(bucket)上的结点数大于这个值时会转成红黑树;+对应的table的最小大小为64,即MIN_TREEIFY_CAPACITY ;这两个条件都满足,会链表会转红黑树staticfinalint TREEIFY_THRESHOLD=8;// 当桶(bucket)上的结点数小于这个值时树转链表staticfinalint UNTREEIFY_THRESHOLD=6;// 桶中结构转化为红黑树对应的table的最小大小staticfinalint MIN_TREEIFY_CAPACITY=64;// 存储元素的数组,总是2的幂次倍transientNode<k,v>[] table;// 存放具体元素的集transientSet<map.entry<k,v>> entrySet;// 存放元素的个数,注意这个不等于数组的长度。transientint size;// 每次扩容和更改map结构的计数器transientint modCount;// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容int threshold;// 填充因子finalfloat loadFactor;}

HashMap 中涉及到的数据结构

链表节点(单链表)

NodeHashMap 的一个内部类,实现了Map.Entry 接口,本质上是一个单链表的数据结构。链表中的每个节点就是一个Node 对象

staticclassNode<k,v>implementsMap.Entry<k,v>{finalint hash;finalK key;V 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;}publicfinalKgetKey(){return key;}publicfinalVgetValue(){return value;}publicfinalStringtoString(){return key+=+ value;}publicfinalinthashCode(){returnObjects.hashCode(key)^Objects.hashCode(value);}publicfinalVsetValue(V newValue){V oldValue= value;
        value= newValue;return oldValue;}// 判断两个node是否相等,若key和value都相等,返回true。可以与自身比较为truepublicfinalbooleanequals(Object o){if(o==this)returntrue;if(oinstanceofMap.Entry){Map.Entry<!--?,?--> e=(Map.Entry<!--?,?-->)o;if(Objects.equals(key, e.getKey())&&Objects.equals(value, e.getValue()))returntrue;}returnfalse;}}

红黑树节点

红黑树比链表多了四个变量,parent 父节点、left 左节点、right 右节点、prev上一个同级节点

staticfinalclassTreeNode<k,v>extendsLinkedHashMap.Entry<k,v>{TreeNode<k,v> parent;// 父节点TreeNode<k,v> left;// 左子树TreeNode<k,v> right;// 右子树TreeNode<k,v> prev;// 删除时需要取消下一个链接boolean red;// 颜色属性TreeNode(int hash,K key,V val,Node<k,v> next){super(hash, key, val, next);}// 返回当前节点的根节点finalTreeNode<k,v>root(){for(TreeNode<k,v> r=this, p;;){if((p= r.parent)==null)return r;
            r= p;}}// ......省略}

位桶

存储(位桶)的数组

transientNode<K,V>[] table;

HashMap 类中有一个非常重要的字段,就是Node[] table,即哈希桶数组,明显它是一个Node 的数组

HashMapput() 方法

数组判断

  • 判断tab[] 数组是否为null 或长度为0,如果是null 或长度为0;则通过resize() 方法初始化数组,并获取长度
  • 如果单链表Node<K,V> p == tab[i = (n - 1) & hash]) == null,就直接put 进单链表中,说明此时并没有发生hash 冲突
  • 如果该数组索引位置之前放入过数据,在通过keyhash 值,(k = p.key) == key || (key != null && key.equals(k)) 判断该put 的数据是否与数组索引位置数据相同;如果相同,使用e = p 来则初始化数组Node<K,V> e

红黑树判断

  • 如果不相同,则判断是否是红黑树,如果是红黑树就直接插入树中

链表判断

  • 如果不是红黑树,就遍历链表每个节点,并判断链表末尾节点是否为null;如果为null,则在该单链表末尾节点插入数据,并判断是否binCount > 8 -1,为true 的话会调用treeifyBin(tab, hash) 方法,该方法在后面详解
  • 然后,判断该put 的数据是否与单链表的某个节点数据相同,如果相同则跳出循环,执行下一步
publicVput(K key,V value){returnputVal(hash(key), key, value,false,true);}finalVputVal(int hash,K key,V value,boolean onlyIfAbsent,boolean evict){Node<K,V>[] tab;Node<K,V> p;int n, i;// 判断 table[] 是否为空,如果是空的就创建一个 table[],并获取他的长度nif((tab= table)==null||(n= tab.length)==0)
    	n=(tab=resize()).length;// 如果单链表 Node<K,V> p == tab[i = (n - 1) & hash]) == null,// 就直接 put 进单链表中,说明此时并没有发生 Hash 冲突if((p= tab[i=(n-1)& hash])==null)
    	tab[i]=newNode(hash, key, value,null);else{// 说明索引位置已经放入过数据了,已经在单链表处产生了Hash冲突Node<K,V> e;K k;// 判断 put 的数据和之前的数据是否重复if(p.hash== hash&&// 进行 key 的 hash 值和 key 的 equals() 和 == 比较,如果都相等,则初始化数组 Node<K,V> e((k= p.key)== key||(key!=null&& key.equals(k))))   			
            e= p;// 判断是否是红黑树,如果是红黑树就直接插入树中elseif(pinstanceofTreeNode)
        	e=((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else{// 如果不是红黑树,就遍历每个节点,判断单链表长度是否大于等于 7,// 如果单链表长度大于等于 7,数组的长度小于 64 时,会优先选择扩容// 如果单链表长度大于等于 7,数组的长度大于 64 时,才会选择单链表--->红黑树for(int binCount=0;;++binCount){if((e= p.next)==null){// 采用尾插法,在单链表中插入数据
                	p.next=newNode(hash, key, value,null);// 如果 binCount >= 8 - 1if(binCount>= TREEIFY_THRESHOLD-1)treeifyBin(tab, hash);break;}// 判断索引每个元素的key是否可要插入的key相同,如果相同就直接覆盖if(e.hash== hash&&((k= e.key)== key||(key!=null&& key.equals(k))))break;
                 p= e;}}// 说明数组或者单链表中有相同的key,因此只需要将value覆盖,并将oldValue返回即可if(e!=null){V oldValue= e.value;if(!onlyIfAbsent|| oldValue==null)
            	e.value= value;afterNodeAccess(e);return oldValue;}}// 说明没有key相同,因此要插入一个key-value,并记录内部结构变化次数++modCount;// 判断是否扩容if(++size> threshold)resize();afterNodeInsertion(evict);returnnull;}

HashMapget() 方法

首先定位键值对所在桶的位置,之后再对链表或红黑树进行查找

  1. 判断表或key 是否是null,如果是直接返回null key 对应的value
  2. 判断索引处第一个key 与传入key 是否相等,如果相等直接返回
  3. 如果不相等,判断链表是否是红黑二叉树,如果是,直接从树中取值
  4. 如果不是树,就遍历链表查找
publicVget(Object key){Node<K,V> e;return(e=getNode(hash(key), key))==null?null: e.value;}finalNode<K,V>getNode(int hash,Object key){Node<K,V>[] tab;Node<K,V> first, e;int n;K k;// 1.如果表不是空的,并且要查找索引处有值,就判断位于第一个的key是否是要查找的keyif((tab= table)!=null&&(n= tab.length)>0&&(first= tab[(n-1)& hash])!=null){// 1.1.检查要查找的是否是第一个节点if(first.hash== hash&&// always check first node((k= first.key)== key||(key!=null&& key.equals(k))))return first;// 1.2.沿着第一个节点继续查找if((e= first.next)!=null){// 1.2.1.如果是红黑树,那么调用红黑树的方法查找if(firstinstanceofTreeNode)return((TreeNode<K,V>)first).getTreeNode(hash, key);// 1.2.2.如果是链表,那么采用链表的方式查找do{if(e.hash== hash&&((k= e.key)== key||(key!=null&& key.equals(k))))return e;}while((e= e.next)!=null);}}returnnull;}

HashMapremove 方法

HashMap 的删除操作并不复杂,仅需三个步骤即可完成

  • 定位桶位置
  • 遍历链表或红黑树并找到键值相等的节点
  • 删除节点
publicVremove(Object key){Node<K,V> e;return(e=removeNode(hash(key), key,null,false,true))==null?null: e.value;}finalNode<K,V>removeNode(int hash,Object key,Object value,boolean matchValue,boolean movable){// ------------------1. 查找到要删除的节点------------------// tab当前数组,n当前数组容量,p根据hash从数组上找到的节点,index p节点在数组上的位置Node<K,V>[] tab;Node<K,V> p;int n, index;// 当数组存在且数组容量大于0,且p节点存在if((tab= table)!=null&&(n= tab.length)>0&&(p= tab[index=(n-1)& hash])!=null){Node<K,V> node=null, e;K k;V v;// 当 p 的 hash 等于参数 hash,且 p 的 key 等于参数 key// p节点就是当前要删除的节点,将node指向pif(p.hash== hash&&((k= p.key)== key||(key!=null&& key.equals(k))))
            node= p;// 当p节点不是要删除的节点时,说明p节点上有红黑树或者链表elseif((e= p.next)!=null){// p如果是红黑树,通过getTreeNode()查找节点if(pinstanceofTreeNode)
                node=((TreeNode<K,V>)p).getTreeNode(hash, key);// p是链表,循环链表查询节点else{do{if(e.hash== hash&&((k= e.key)== key||(key!=null&& key.equals(k)))){
                        node= e;break;}
                    p= e;}while((e= e.next)!=null);}}// ------------------2. 删除查找到的节点------------------// 如果查找到的node存在且machValue为false或v等于valueif(node!=null
  • 作者:桐花思雨
  • 原文链接:https://blog.csdn.net/weixin_38192427/article/details/108478615
    更新时间:2022-09-12 09:26:15