面试必备1:HashMap(JDK1.8)原理以及源码分析

首先附上我的几篇其它文章链接感兴趣的可以看看,如果文章有异议的地方欢迎指出,共同进步,顺便点赞谢谢!!!
Android framework 源码分析之Activity启动流程(android 8.0)
Android studio编写第一个NDK工程的过程详解(附Demo下载地址)
面试必备2:JDK1.8LinkedHashMap实现原理及源码分析
Android事件分发机制原理及源码分析
View事件的滑动冲突以及解决方案
Handler机制一篇文章深入分析Handler、Message、MessageQueue、Looper流程和源码
Android三级缓存原理及用LruCache、DiskLruCache实现一个三级缓存的ImageLoader

HashMap概述:

在分析HashMap的源码前,需要了解一下几个问题。
面试必备2LinkedHashMap的源码分析(基于JDK1.8)

1:HashMap的数据结构

我们需要先知道HashMap是基于什么样的数据结构来进行数据存储的,知道了这些我们再去看源码就容易的多。

散列表(哈希表) 我们常用的数据结构就是数组和链表,数组具有增删慢查找快的特点,而链表具有增删快查找慢 的特点;基于上述特点,HashMap 即想要查询效率快,又想增删效率高,基于这样的特点HashMap的数据结构就是通过数组和链表组成的散列链表。
一直到JDK7为止,HashMap的结构都基于一个数组以及多个链表的实现,hash值冲突的时候,就将对应节点以链表的形式存储;JDK8中,HashMap采用的是数组(位桶)+链表/红黑树组成,当同一个hash值的节点数不小于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树,提高查询效率,这就是JDK7与JDK8中HashMap实现的最大区别。

在这里插入图片描述

2:存储节点 : Node 和TreeNode

HashMap存储数据时如何组织出这样一个散列表呢?HashMap中将我们存入的每一个数据封装成一个Node类(Node节点),Node应该有以下下属性 :key--------键、value-----值 组成我们put的键值对,既然要组织起链表则必须有Node next属性、还有一个根据key算出的int 类型hash值,hash值用来确定该该节点所在数组的索引后面会进行详细描述。

/**
 *  散列表的节点  Node  部分源码
 */
 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//用于确定该节点所在数组的位置,  下标
        final K key;//  我们所操作的键
        V value;//  值
        Node<K,V> next;//存储下一个节点,通过next组织起链表
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;//位置
            this.key = key;
            this.value = value;
            this.next = next;
        }
		/**
         *  返回当前节点的根节点  
         */
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }
    }

当链表长度大于8时将链表转换成红黑树,红黑树节点TreeNode部分源码

 static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {
        TreeNode<K,V> parent;  // 父节点
        TreeNode<K,V> left;//左子树
        TreeNode<K,V> right;//右子树
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;//颜色属性  
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
}

当我们放入或取出元素时大致流程是:

  1. 现根据hash函数去求出key的Hash值,至于求hash值得算法将在后面进行详细分析
  2. 根据上一步的hash值可以确定在数组的哪个位置,定下标(数组的索引)
  3. 根据索引从数组中获取数据,如果为NULL 则将该元素直接放入或取出(直接在数组中操作),如果该位置数据不为NuLL,则需要向该元素所在的链表进行数据的操作。

哈希冲突(哈希碰撞)

如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。

3:HashMap中散列表中数组(或位桶)如何表示?

HashMap源码中与数组相关的几个成员属性:

  1. 数组的表示: transient Node<K,V>[] table;
  2. 数组的大小 : 数组的初始化需要一个指定的大小,在HashMap中 数组的大小永远是 2的幂次方(原因将在源码中分析)
  3. 数组的默认大小:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 , 1 << 4 二进制中1左移四位就是2^4=16, 不直接写16的原因是,效率高,计算机最终读取的二进制;
  4. 数组的最大容量:static final int MAXIMUM_CAPACITY = 1 << 30//最大为2^30,使用位移操作符的原因也是效率高 ;
  5. 数组扩容因子: static final float DEFAULT_LOAD_FACTOR = 0.75f//用于确定数组扩容的临界值;
  6. 以存放数据大小:transient int size//记录 hashMap 当前存储的元素的数量;
  7. 数组扩容的临界值:int threshold;//数组已使用量size>threshold时需要扩容,threshold=数组大小*扩容因子提升效率,没有必要等到数组都用完了在进行扩容,每次扩大2倍
    **注意:数组的扩容是在当已使用量大于数组容量*扩容因子(默认0.75)**时进行扩容

4:HashMap中散列表中链表的长度限制:长度越长存储查询效率低的问题

链表越长,期查询效率越低,所以链表的长度也有一个限制,我们称为阈值 ,JDK1.8的时候,链表的长度大于阈值,将其结构改成一个红黑二叉树,以提升差值效率,同时也伴随者性能的损耗增加
HashMap源码中与链表相关的几个成员属性:

  1. 转换红黑树的阈值: static final int TREEIFY_THRESHOLD = 8;//阈值 链表越深查找存储效率都低 ,超过8以后将其转换成红黑二叉树 ,提升查找效率
  2. 转换成链表的阈值static final int UNTREEIFY_THRESHOLD = 6; //当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
  3. static final int MIN_TREEIFY_CAPACITY = 64;//当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。

6:新的数据封装成的Node对象如何去存储:
根据key计算hashCode ----》int类型的值,hash(key)然后就知道要存在数组的那个位置

5:HashMap中的属性的概述

这里将对HashMap属性进行描述,以便于理解源码

 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
        //序列号,序列化的时候使用。
        private static final long serialVersionUID = 362498820763181265L;
        /**默认容量,1向左移位4个,00000001变成00010000,也就是2的4次方为16,使用移位是因为移位是计算机基础运算,效率比加减乘除快。**/
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
        //最大容量,2的30次方。
        static final int MAXIMUM_CAPACITY = 1 << 30;
        //加载因子,用于扩容使用。
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
        //当某个桶节点数量大于8时,会转换为红黑树。
        static final int TREEIFY_THRESHOLD = 8;
        //当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
        static final int UNTREEIFY_THRESHOLD = 6;
        //当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。
        static final int MIN_TREEIFY_CAPACITY = 64;
        //存储元素的数组,transient关键字表示该属性不能被序列化
        transient Node<K,V>[] table;
        //将数据转换成set的另一种存储形式,这个变量主要用于迭代功能。
        transient Set<Map.Entry<K,V>> entrySet;
        //元素数量
        transient int size;
        //统计该map修改的次数
        transient int modCount;
        //临界值,也就是元素数量达到临界值时,会进行扩容。
        int threshold;
        //也是加载因子,只不过这个是变量。
        final float loadFactor;
        ....
    }

HashMap源码分析:

了解了概述中的几个概念后,将从对HashMap的put、get、remove三个方法进行源码分析。

1:HashMap的put(key,value)方法源码

put方法的大致流程:

  1. 根据传入的key,计算hash值
  2. 判断键值对数组tab[]是否为空或为null,否则以默认大小resize()进行数组的初始化操作;
  3. 根据键值key计算hash值得到插入的数组索引i,如果tab[i]==null,直接新建节点添加,否则转入3
  4. 判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理
//返回值扔然为Value
public V put(K key, V value) {
		//1:hash(key)先找存在哪里,先算key的hash值
		//2: putVal(hash(key), key, value, false, true)方法进行存储
        return putVal(hash(key), key, value, false, true);
    }

//根据key求出Hash值,通过这去求散列表的下标
static final int hash(Object key) {
        int h;//32位数,不足高位补0 
        // (h = key.hashCode()) ^ (h >>> 16)  
       // 1: key的HashCode     h  
        //2:  h先做了个位移运算,向右边位移16位即h >>> 16  
       /// 3:然后将两个值进行异或预算    充分利用hash的每一位数,将h的高16位异或h的低16位得到一个值, 使得高位也可以参与hash,更大程度上减少了碰撞率。
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

putVal方法源码:

//,如果参数onlyIfAbsent是true,那么不会覆盖相同key的值value。如果evict是false。那么表示是在初始化时调用的
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;//   数组  当前操作的节点     
        if ((tab = table) == null || (n = tab.length) == 0)//数组table为null
             n = (tab = resize()).length;//resize()  作用1:初始化数组   n记录数组的大小
        if ((p = tab[i = (n - 1) & hash]) == null) // tab[i = (n - 1) & hash]==null, 即数组tab[i]==null,没有元素直接新建节点添加
     /**
     *  根据hash和数组长度算出数组下标i  table[i]取出对应元素
     *  i = (n - 1) & hash 算出数组下标的优点?
     *  1:防止数组越界:这么写是为了解决hash有可能越界问题  ,即hash不能大于n 数组长度,
     *     数组的大小n一定是2几次幂,只有这样n-1,n-1二进制表示时每位的值都为1,从而保证在(n-1)&hash值的值永远<=n-1 ,保证不会数组越界
     *     例如:当n=16默认值时,n - 1=15 的二进制 01111 和前面的hash值进行与运算   的值永远小于等于01111(15),不会越界
     *  2:提高效率:(n - 1) & hash等价于  模/n取余 这样写是因为与运算比模运算效率高
     *  3: 数组的大小n一定是2几次幂,n-1 的二进制形式每位都是1,在&hash时导致数组的散列性变大,降低了hash碰撞的概率
     */
            tab[i] = newNode(hash, key, value, null);
        else {//tab[i]!=null需要向链表或红黑树中存放
            Node<K,V> e; K k;
            if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
			// 如果这个元素的key与要插入的一样,那么就替换
                e = p;
            else if (p instanceof TreeNode)
            	//1.如果当前节点是TreeNode类型的数据,执行putTreeVal方法, 向红黑树中存放
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            	 //遍历这条链子上的数据,跟jdk7没什么区别
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {//即table[i].next==null,table[i]就是尾节点
                       //向链表尾部添加数据
                        p.next = newNode(hash, key, value, null);
                        //项链表中添加完元素后判断,链表长度是否超过8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        	//长度超过8,执行treeifyBin方法,将链表转换成红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
             // 表示在桶中找到key值、hash值与插入元素相等的结点
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                // 返回旧值
                return oldValue;
            }
        }
        ++modCount;
         //每次添加完成后,判断阈值,决定是否扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);//这个为子类HashMap服务,进行lru排序,在这里无需研究
        return null;
    }

treeifyBin方法源码:将链表转换成红黑二叉树

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);//将Node转成TreeNode
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

2:HashMap的扩容机制:resize()源码

resize的作用:1:初始化数组table 2: 扩容
构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比*Node.length)重新调整HashMap大小 变为原来2倍大小,扩容很耗时

  1. 每次扩展的时候,都是扩展2倍;
  2. 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
//resize  作用1:初始化数组   2扩容
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//记录数组的长度
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {//数组大于0进行扩容操作
            if (oldCap >= MAXIMUM_CAPACITY) {//如果容量已大于最大值
                threshold = Integer.MAX_VALUE;//修改扩容临界值
                return oldTab;
            }
            //如果oldCap << 1扩大两倍,满足扩容的条件
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 左移1位扩大两倍容量
        }
         // 如果旧表的长度的是0,就是说第一次初始化表
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
        	//数组的初始化
            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;//threshold   容量*负载因子    =临界值
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//初始化数组   一开始默认到校
       table = newTab;//把新表赋值给table  
       if (oldTab != null) {//原表不是空要把原表中数据移动到新表中
        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)//说明这个node没有链表直接放在新表的数组e.hash & (newCap - 1)位置  
                       newTab[e.hash & (newCap - 1)] = e;  
                    else if (e instanceof TreeNode)
                    	//如果是红黑树则,转为链表
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                       //如果e后边有链表,到这里表示e后面带着个单链表,需要遍历单链表,将每个结点重新计算在新表的位置,并进行搬运  
                        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),可以得到哈希值去模后,
          					 //是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,
          					 //否则存放在高位。这里又是一个利用位运算 代替常规运算的高效点 
                            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) {//lo队不为null,放在新表原位置  
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {//hi队不为null,放在新表j+oldCap位置  
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;//返回初始化或者扩容后的数组
    }

3:小结:

  • 运算尽量都用位运算代替,更高效。
  • 对于扩容导致需要新建数组存放更多元素时,除了要将老数组中的元素迁移过来,也记得将老数组中的引用置null,以便GC
  • 取下标 是用 哈希值 与运算 (桶的长度-1) i = (n - 1) & hash。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高
  • 扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
  • 因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量
  • 利用哈希值 与运算 旧的容量 ,if ((e.hash & oldCap) == 0),可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位。这里又是一个利用位运算 代替常规运算的高效点
  • 如果追加节点后,链表数量》=8,则转化为红黑树
  • 插入节点操作时,有一些空实现的函数,用作LinkedHashMap重写使用。

4:HashMap的get(key)方法源码分析:

下面简单说下 get(key) 的过程:
get(key)方法时获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=tab[hash&(n-1)],先判断first的key是否与参数key相等,不等就遍历后面的链表找到相同的key值返回对应的Value值即可,即先根据hash值确定数组(哈希桶)位置,然后根据key相等取值 , hash相等&&key相等的节点

//传入键  key
public V get(Object key) {
        Node<K,V> e;
        //根据hash函数得到的hash值和key去 getNode(hash(key), key)  返回null或者Node.value
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

getNode(hash, key)方法源码分析:

//根据 hash  和key获取node节点
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;//在tab数组中经过散列的第一个位置  
           // table已经初始化,长度大于0,根据hash寻找table中的项也不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash &&// 判断是不是第一个节点,是的话从数组中返回
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
                //否则
            if ((e = first.next) != null) {
           	 // 取出first的下一个节点,如果为为红黑树结点
                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;
    }

5:remove方法源码分析

public V remove(Object key) {
        Node<K,V> e;
         //这里传入了value 同时matchValue为true
        return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;
    }

removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable)源码

 /**
     * Implements Map.remove and related methods
     * @param hash 哈希值
     * @param key the key  
     * @param value the value to match if matchValue, else ignored
     * @param matchValue 为true的话,则表示删除它key对应的value,不删除key
     * @param movable 如果为false,则表示删除后,不移动节点,为true则移动节点
     * @return the node, or null if none
     */
   final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        //tab 哈希数组,p 数组下标的节点,n 长度,index 当前数组下标
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //哈希数组不为null,且长度大于0,然后获得到要删除key的节点所在是数组下标位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {
            //node 存储要删除的节点,e 临时变量,k 当前节点的key,v 当前节点的value
            Node<K,V> node = null, e; K k; V v;
            //如果数组下标的节点正好是要删除的节点,把值赋给临时变量node
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
                //也就是要删除的节点,在链表或者红黑树上,先判断是否为红黑树的节点
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    //遍历红黑树,找到该节点并返回
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else { //表示为链表节点,一样的遍历找到该节点
                    do {
                        if (e.hash == hash &&
                                ((k = e.key) == key ||
                                        (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        /**注意,如果进入了链表中的遍历,那么此处的p不再是数组下标的节点,而是要删除结点的上一个结点**/
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //找到要删除的节点后,判断!matchValue,我们正常的remove删除,!matchValue都为true
            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;
                //修改计数器
                ++modCount;
                //长度减一
                --size;
                /**此方法在hashMap中是为了让子类去实现,主要是对删除结点后的链表关系进行处理**/
                afterNodeRemoval(node);
                //返回删除的节点
                return node;
            }
        }
        //返回null则表示没有该节点,删除失败
        return null;
    }

HashMap的遍历entrySet()

entrySet()源码:

 public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        //直接返回成员变量entrySet==null时 new  EntrySet()并返回,否则直接返回
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }

集合的遍历主要是通过内部类EntrySetiterator()方法实现,EntrySet的部分源码如下:


final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<Map.Entry<K,V>> iterator() {
        	//返回EntryIterator类型的迭代器
            return new EntryIterator();
        }
   		 ....省略部分源码...
    }

iterator方法返回EntryIterator类型的迭代器其源码如下

final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        //Iterator的`next`方法就是调用父类`HashIterator`的`nextNode`方法获取下一个节点
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

nextNode方法是父类HashIterator提供的方法,即HaapMap的迭代的核心就是HashIterator源码的源码如下,这里是重点!!!

abstract class HashIterator {
        Node<K,V> next;        // 下一个节点
        Node<K,V> current;     // 当前节点
        int expectedModCount;  // HashMap是线程不安全的,在迭代时通过expectedModCount去记录成员变量modCount,判断迭代过程中的并发修改异常
        int index;             // 当前数组的索引
		/**
		  * 构造器中对属性进行了初始化,
		  * 并通过while循环得到数组中的第一个不为空的元素下标以及值,并将此元素值赋给next
		  */
        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            //while循环得到数组中的第一个不为空的元素下标以及值,并将此元素值赋给next
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }
        //判断是否有下一个节点
        public final boolean hasNext() {
            return next != null;
        }
		/**
          * nextNode()的方法,就是Iterator中next方法中调用的,即Iterator中next方法就是nextNode方法
         * 其遍历HashMap过程就是,就是依次遍历数组中的链表,取出下一个节点
        */
        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;//e,为当前要返回的节点
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
                //HashMap遍历的核心思想
             //e为当前要返回的节点,
             //获取e的下一节点,并赋值给next,如果此时的next为空,则说明当前链表已经遍历完毕,
             //那么进入判断体,开始通过while遍历下一个数组桶中的链表,并将此链表的头结点赋值给next;
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

		 /**
           * Iterator的remove删除方法 本质上还是调用了HashMap的removeNode方法
          * 只是在调用之前,通过modCount != expectedModCount时抛出并发修改异常,处理线程不安全问题,
          * 如果相等则调用HashMap的removeNode方法移除节点
          * 
        */
        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)//处理多线程并发问题
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

HashMap的遍历过程就是通过nextNode取出下一个节点的过程,它的核心是想就是依次遍历哈希桶中的链表实现。

总结

到此为止,HashMap原理分析完毕,阅读时需要从第一部分先看,然后再看第二部分的源码分析,具体的总结在概述和小结中都已经描述清楚,这里不做过多赘述。这是周末看的HashMap源码的理解,里面有不对的对方欢迎大家留言指处,共同进步,后续会陆续将LinkedHashMap、CurrentHashMap的源码分享出来以供参考。

趁热打铁接下来可以看一下我的另一片文章:面试必备2LinkedHashMap的源码分析(基于JDK1.8)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章