源码解析:HashMap 1.8

本文源码基于HashMap 1.8,下载地址:Java 8java

另外本文不分析红黑树相关的源码node

前言

在对HashMap进行源码解析前,咱们颇有必要搞清楚下面这几个名词,这对于下文的阅读有很大的帮助。git

  • 哈希表:这里指的就是HashMap
  • 哈希桶:HashMap的底层数据结构,即数组
  • 链表:哈希桶的下标装的就是链表
  • 节点:链表上的节点就是哈希表上的元素
  • 哈希表的容量:元素的总个数
  • 哈希桶的容量:数组的个数,因为当发生哈希冲突时,采用链地址法解决冲突,故哈希桶的容量<=哈希表的容量

注意:必定要区分哈希表的容量和哈希桶的容量,一开始很容易将这两个定义搞混淆github

1、链表节点

HashMap.Node面试

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//哈希值
        final K 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;
        }

        public final K getKey() { return key; }
        public final V getValue() { return value; }
        public final String toString() { return key + "=" + value; }

		//结点的hash值等于key和value哈希值的异或
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

		//设置新的value,同时返回旧的value
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        
        //key和value都相等才被认为是相同的节点
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
复制代码

从上面能够发现哈希桶的链表就是单链表结构,而且节点的hash值会等于key和value哈希值的异或。数组

2、成员属性

HashMap安全

1. 常量

//哈希桶默认容量为16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

	//哈希桶最大容量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;
	
    //当哈希表的容量达到64时,也会转换为红黑树结构
    static final int MIN_TREEIFY_CAPACITY = 64;
复制代码

2. 变量

//哈希桶,存放链表。transient关键字表示该属性不能被序列化
    transient Node<K,V>[] table;

    //迭代功能
    transient Set<Map.Entry<K,V>> entrySet;

    //哈希表元素数量
    transient int size;

    //统计该map修改的次数
    transient int modCount;

    //阈值,当元素数量,即哈希表的容量达到阈值时,会进行扩容
    int threshold;

    //加载因子,用于计算哈希表的阈值。threshold = 哈希桶的容量*loadFactor
    final float loadFactor;
复制代码

3、构造器

1. 无参构造器

//默认的构造函数,加载因子为默认的0.75f
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
复制代码

咱们在通常状况下都是用这个无参构造器的,这就证实当咱们平时new了一个HashMap时,底层只是设置了一个加载因子的值为默认的0.75fmarkdown

2. 指定哈希桶容量的构造函数

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
复制代码

其实调用的是下面一个构造函数,不过这里在指定哈希表的容量的同时,也指定了加载因子为默认值数据结构

3. 指定哈希桶容量和加载因子的构造函数

//指定加载因子的构造函数仍是用的比较少的
    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);
    }
复制代码

在上面对容量作了一些边界处理,而后设置了加载因子的值和哈希表的阈值,这里在设置阈值时,首先会调用tableSizeFor函数对容量作一些处理函数

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;
    }
复制代码

上面具体实现就再也不深究,最后的结果就是可以返回恰好大于cap的2的n次方,当扩容时也会调用这个函数,这就保证了哈希桶的容量永远都是2的n次方,也正是由于这个前提下,接下来的取下标的操做可以经过 hash&(table.length-1)来替换hash%(table.length)

这时候你也许就会有疑问了,我明明传入的是哈希桶的容量,怎么最后却赋值给了阈值呢?这实际上是由于在构造器中,并无对哈希桶table进行初始化,初始化的工做交给了扩容函数。当第一次put时,会调用扩容函数,将阈值赋值给哈希桶的容量,接着对哈希桶table进行初始化,而后根据公式设置从新设置阈值,大概流程是这样,后续还会提到!

4. 批量添加元素的构造函数

public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
复制代码

这个构造函数比较特殊,其做用就是在构造一个新的哈希表的同时加入指定map全部的元素。这里也是首先设置了加载因子为默认值,而后调用putMapEntries方法来进行批量增长元素,注意这里的第二个参数为false。

//将map表的全部元素加入到当前表中,当前Map初始化时evict为false,其它状况为true
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
            	//求出须要的容量。由于实际使用的长度=容量*加载因子
            	//+1是由于小数相除,基本都不会是整数,容量大小不能为小数的,
                //后面转换为int,多余的小数就要被丢掉,
            	//因此+1,例如,map实际长度22,22/0.75=29.3,所须要的容量确定为30
            	//若是刚恰好除得整数呢,除得整数的话,容量大小多1也没什么影响
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
			    //将哈希桶的容量存在阈值中
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
			//当前表尚未初始化,因此不会进行扩容
            else if (s > threshold)
                resize();

            //遍历
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                //添加
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

复制代码

假设这里要批量增长的数据不为0,经过上面分析咱们知道,构造器并无构造哈希桶table,因此这里的table为null。接着就是跟上面的构造函数同样,将要建立的哈希桶的容量暂时存在阈值中。

这里有个问题值得一提,刚开始我一直觉得ft是阈值,因此一直搞不明白为何要用s/loadFactor ,由于公式是:threshold = s*loadFactor才对。后来才想通,这里的ft并非阈值,而是哈希桶的容量,由于最后并非设置阈值,而是将容量的值暂存在阈值中。

接着会遍历m依次将元素添加到当前哈希表中,这里涉及到了两个操做:遍历和添加,这里不进行展开讲,在后文会详细进行分析。

5. 小结

这里咱们从新梳理下当new一个HashMap时,内部的实际工做:

  • 无参数构造HashMap,如HashMap<Integer,Integer> map = new HashMap<>(),只设置了加载因子为默认值
  • 指定哈希桶容量构造HashMap,如HashMap<Integer,Integer> map = new HashMap<>(5),内部的工做就是设置加载因子为默认值。暂存容量的值到阈值中,其值为恰好大于容量的2的n次方,这里的阈值为8。
  • 同时指定容量和加载因子,这个构造器通常不多使用
  • 在构造的同时批量加载数据。如HashMap<Integer,Integer> newMap = new HashMap<>(map),根据map的size和公式算出哈希桶的容量,而后将容量暂存到阈值中,最后遍历map,将map中的元素添加到newMap中

能够发如今构造的时候并无构造哈希桶table的实例,因此将哈希桶的容量都先暂存在阈值threshold中

4、添加

put操做其实包括了哈希表的增、改两个操做。当添加的元素key存在时,就会修改value的值,不存在则添加这个元素。

1. 添加(改)一个元素

在实际上咱们的操做很简单,就一行代码搞定

map.put(key,value);

复制代码

因此咱们直接看put的操做

HashMap#put

public V put(K key, V value) {
        //先计算key的hash值
        return putVal(hash(key), key, value, false, true);
    }

复制代码

这里涉及到了hash函数来求当前key的hash值,咱们来看看

HashMap#hash

static final int hash(Object key) {
        int h;
		//当前key为null,则hash指为0,不然返回扰动函数干扰后的hashCode值
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

复制代码

这个函数被称之为扰动函数,能够发现这个函数并无简单的返回了key的hashCode的值,而是进行了干扰,干扰的细节不进行分析,其原理就是将key的hashCode值的高位变相的添加到低位去,而后增长key的hash值的随机性来减小hash冲突。为何要将高位添加到低位呢?这是由于在HashMap中取桶下标的方式是经过 hash&(桶.size-1)来替代模操做,而位操做的时候hashCode只有低位参与位运算。

讲完hash,咱们回到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;
		//若是当前的哈希桶是空的,则表示当前为首次初始化
        if ((tab = table) == null || (n = tab.length) == 0)
			//扩容操做
            n = (tab = resize()).length;

		//没有发生哈希碰撞,直接构建一个新的结点,而后放在指定的位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
		//发生哈希冲突,链地址法解决哈希冲突
        else {
			
            Node<K,V> e; K k;
			//若是key的哈希值相同,key也相同,则进行覆盖value操做
            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) {
				   //链表末端
                    if ((e = p.next) == null) {
						//插入一个新的结点
                        p.next = newNode(hash, key, value, null);
						//若是链表结点数>=8,则转化成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
					//若是找到要覆盖的结点
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

			//value覆盖操做,若是e不为null,则须要覆盖value
            if (e != null) { 
				//覆盖结点值,并返回原来结点的value
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
				//空实现的函数,若是是LinkedHashMap会重写该方法
                afterNodeAccess(e);
                return oldValue;
            }
        }
		//走到这,表示是添加操做

		//修改次数+1
        ++modCount;
		//更新size,判断是否须要扩容
        if (++size > threshold)
            resize();
		//空实现的函数,若是是LinkedHashMap会重写该方法
        afterNodeInsertion(evict);
        return null;
    }

复制代码

这个方法干的事情不少,其职责就是:

  • 首次初始化须要进行扩容操做
  • 判断是否发生哈希冲突
    • 没有,则直接构造一个新的节点,而后放在哈希桶对应下标的位置看成链表的头结点
    • 发生哈希冲突,采用链地址法解决冲突
      • 若是头节点的key等于要添加的key,表示是修改操做,则进行覆盖value操做
      • 不然,若是是头结点是红黑树,则进行红黑树的添加操做
      • 不然,表示当前链表节点少于8,则对链表进行遍历,若是遍历中途出现节点key等于要添加的元素的key时候,表示是修改操做,跳出遍历;不然遍历到链表末端插入一个新的节点,插入后判断当前链表节点数是否达到8,达到8则进行转换成红黑树的操做
      • 进行覆盖value操做,覆盖节点值,返回原来节点的值。
  • 若是是添加操做,则更新当前哈希表的大小,而后判断是否须要扩容,最后返回null

因此若是是添加操做则返回null,若是是修改操做则返回原来的value。另外扩容操做将在下文进行解析。

2. 批量添加元素

实际使用也很简单,也是一行代码搞定,下面的map和oldMap都是HashMap类型,而且两个key和value类型一致

实际使用

map.putAll(oldMap)

复制代码

而后咱们看看HashMap中的putAll方法

HashMap#putAll

//批量增长数据
    public void putAll(Map<? extends K, ? extends V> m) {
        putMapEntries(m, true);
    }

复制代码

调用了putMapEntries,emmmm,怎么感受这个函数似曾相识!没错,在批量添加元素的构造函数中,也调用了这个putMapEntries,不一样的是在构造中传入的第二个参数是false,而在putAll中传入的是true.咱们仍是再次看看这个putMapEntries方法。

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
			//若是当前表是空的
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
			//若是当前表已经初始化,而且m的元素数量大于阈值,则进行扩容
            else if (s > threshold)
                resize();
            //添加元素
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

复制代码

因此这个批量添加元素的函数和批量添加元素的构造函数有什么区别吗?当调用putAll前,没有初始化(构造后直接调用putAll)确实没什么区别。可是当putAll前,已经调用过put或者putAll时,就不同了,这时候由于已经初始化,因此table并不为null,故还要判断添加的元素个数是否须要进行扩容(扩容操做后续解析),而后才遍历oldMap添加元素。

5、扩容

扩容操做可谓是HashMap的精髓,在了解这个操做前,咱们首先须要知道这个神秘的扩容究竟在什么场合下会出现。

1. 触发扩容的状况

从上面的分析中,咱们得知触发扩容有三种状况:

1.首次初始化,有多是第一个put操做或者第一个putAll操做,也有多是使用批量添加元素的构造函数

2.已经初始化,putAll批量添加元素,增长元素的总个数大于阈值

3.已经初始化,putVal添加一个节点后,节点个数大于阈值

注:putVal包括了添加一个元素和批量添加元素的状况,由于批量添加元素也会调用putVal

而后接着看具体扩容方法

2. 扩容方法

因为扩容方法太长,这里将分解成两部分进行讲解

final Node<K,V>[] resize() {
    	//当前哈希桶
        Node<K,V>[] oldTab = table;
		//当前桶的容量,若是首次初始化,则当前桶的容量为0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
		//当前的阈值
        int oldThr = threshold;
		//扩容后新的容量和新的阈值
        int newCap, newThr = 0;
        
        
		//1.构造新的哈希桶
		.....
		
		//2.合并哈希桶
		.....
        return newTab;
    }

复制代码

2.1 构造新的哈希桶

HashMap#resize

.....
		//1.若是当前桶的容量大于0,则是触发的状况2或状况3
        if (oldCap > 0) {
            //边界处理
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
			//不然,则设置新的容量为当前的容量的两倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //若是当前的容量达到16的话,新的阈值也等于旧的阈值的2倍 
                newThr = oldThr << 1; // double threshold
        }

		//2.当前的阈值大于0,只能是1状况而且使用了指定容量的构造函数
        else if (oldThr > 0) 
            newCap = oldThr;
		//3.仍是状况1,而且使用的是无参数的构造函数
        else {
			//新的容量为默认容量16
            newCap = DEFAULT_INITIAL_CAPACITY;
			//新的阈值=默认容量*默认加载因子,即新的阈值为16*0.75=12
            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;
		
		//若是之前的哈希桶有元素,合并哈希桶
		.....


复制代码

构造新的哈希桶,首先要得获得新的容量才能构造,而且在构造的同时还得设置阈值。因此须要根据不一样的状况设置新的容量和阈值,其总体流程以下:

  • 若是当前桶的容量大于0,表示已经初始化,则应该是触发扩容的状况2和状况3,则设置新的容量为当前容量的两倍,若是当前容量大于16,则设置新的阈值为当前阈值的两倍(什么状况下在已经初始化后,当前容量仍是小于16呢?答案是若是是使用指定了容量的构造函数这种状况)
  • 不然,当前阈值大于0,代表是初始化而且使用了指定容量的构造器,将暂存在threshold的哈希桶的容量取出来,即新的容量等于当前阈值。
  • 不然,代表是初始化而且没有指定容量,则设置新的容量为默认值16,新的阈值为12(新的阈值=默认容量*默认加载因子)
  • 若是当前的阈值为0,则代表上述没有对新的阈值进行赋值,则新的阈值等于新的容量*加载因子
  • 更新全局变量阈值threshold,接着根据新的容量构建新的哈希桶,而且赋值给全局变量table

在这里也验证了上面构造函数中提到的设置阈值threshold,其实只是暂存容量的说法。

2.2 合并哈希桶

if (oldTab != null) {
			//遍历当前哈希桶
            for (int j = 0; j < oldCap; ++j) {
			    //当前的结点
                Node<K,V> e;
			    //当前哈希桶中有元素,赋值给e
                if ((e = oldTab[j]) != null) {
					//将当前哈希桶的结点置为null,便于gc
                    oldTab[j] = null;
					//若是当前链表只有一个元素(没有发生哈希碰撞)
                    if (e.next == null)
					  //则将这个元素放进新的哈希桶中
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
						//若是发生哈希碰撞且结点数超过8个,转化成红黑树的状况
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
					
				 //发生哈希碰撞
                    else { 
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
						  //等于0:当前结点的哈希值小于oldCap,故放在low位链表中
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
						 //当前结点的哈希值大于oldCap,故放在high位链表中
                            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;
                        }
                    }
                }
            }
        }

复制代码

从上面的一开始的判断咱们能够得知,若是在已经初始化而且当前哈希表有元素的状况下就会进行合并哈希桶的操做,操做的过程以下:

  • 遍历当前哈希桶
  • 若是当前哈希桶的下标,即链表头节点有元素,则赋值给e
    • 首先为了便于GC须要将当前链表的头节点置为null
    • 若是当前链表只有一个节点,则表示这个链表以前并无发生哈希冲突,因此直接位操做取下标放到新的哈希桶上(这里为何不用判断头节点的hash值与原哈希桶容量的大小关系呢?由于是容量翻倍,若是当前链表只有一个节点,位操做取下标(模运算)后依旧会是一个节点,因此无论大小,最后的链表都只会是一个节点)
    • 不然,若是头结点是红黑树,则进行红黑树的的合并操做
    • 不然,当前链表节点小于8,则须要根据每一个节点的hash值来放入到低位链表或高位链表
      • 遍历当前链表,利用位运算e.hash & oldCap来判断当前节点与当前容量的大小关系,若是e.hash & oldCap=0,则表示当前节点的hash值小于当前容量,故放入低位链表;不然,放入高位链表
      • 遍历结束,将低位链表放回原位置,将高位链表放在新位置,新位置 = 原位置+ 当前容量(oldCap)

咱们发现这里又运用了位操做,这么作的缘由就是为了提高扩容的效率。讲到这扩容就分析完了,让咱们接着下一个操做!

6、查询

在HashMap中提供了几种查询操做,get,containsKey,containsValue,还有Java8新增的getOrDefault,接下来咱们一个个进行分析

1. get

HashMap#get

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

复制代码

get查询几乎和put操做如影随行,从上面咱们也能够发现当查询不到的时候返回null,查询到就返回value。这里实际上调用了getNode方法来进行查询

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
		
        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) {
                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;
    }

复制代码

getNode的方法也很简单,流程以下:

  • 根据key来经过位操做取下标,得到当前key的链表
  • 若是链表头是查询的节点,则直接返回该节点
  • 若是头结点是红黑树,则进行红黑树查询操做
  • 不然,遍历链表,返回要查询的节点
  • 若是查询不到,返回null

2. getOrDefault

这个方法是Java8新增的,笔者使用过一次以后,对它可谓是爱不释手,由于有个这个函数,省去了一开始的判空操做。好比有这么一个需求,咱们须要计算一个int数组各个数字出现的次数。

//常规操做
for(int num:nums){
    if(map.get(num) == null){
        map.put(num,0);
    }
    map.put(num,map.get(num)+1);
}

//getOrDefault
for(int num:nums){
    map.put(num,map.getOrDefault(num,0)+1);
}

复制代码

是否是顿时爱上了这个getOrDefault,其实其内部实现也是很简单的,让咱们看看其操做

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

复制代码

有没有发现其实就是跟get方法是同样的,只不过在这里帮咱们实现了在实际上的判空操做。因此当查询不到,返回默认值defaultValue,查询到了就返回value。

3. containsKey

这个方法在平时也是常常会使用到

public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }

复制代码

能够发现其内部实现其实就是跟get的实现是同样的,同样是调用了getNodet,不一样的是二者的返回类型不同。

4. containsValue

public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

复制代码

containsValue的实现,须要遍历哈希桶的每个链表,而后与节点上的value匹配,若是找到value,则返回true,不然返回false。

7、删除

在HashMap提供了两个删除操做,都是remove,不过一个只需提供key,一个是须要提供key和value,咱们在实际使用上前者应该用的比较多

1. 根据key删除

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

复制代码

从上面发现当删除成功会返回删除的value,删除失败,则会返回null。经过调用removeNode来进行删除操做,这里传入的matchValue是为false,若是是true的话,则key和value都相等才能删除

final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;

		//当前哈希表不为空,而且该key对应的index的链表有结点
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            //node为待删除的结点
            Node<K,V> node = null, e; K k; V v;
			//若是链表头就是要删除的结点
            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 = e;
                    } while ((e = e.next) != null);
                }
            }
			//有待删除的节点,而且matchValue为false或者删除的值相等
            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;
				//操做次数加1
                ++modCount;
				//更新哈希表的size
                --size;
				//LinkedHashMape回调函数
                afterNodeRemoval(node);
				//返回删除的节点
                return node;
            }
        }
        return null;
    }

复制代码

这里的删除操做与添加增长有点相似,大概流程以下:

  • 首先根据key找到哈希桶上对应下标的链表,而后找待删除的节点
  • 若是待删除的节点为链表头,则直接更新链表头
  • 不然,若是链表头为红黑树,则进行红黑树的删除操做
  • 不然,遍历链表,找到待删除的节点,直接删除该节点
  • 若是删除成功,更新操做次数和哈希表的容量,返回删除的节点
  • 若是删除失败,则返回null

2. 根据key和value删除

public boolean remove(Object key, Object value) {
        return removeNode(hash(key), key, value, true, true) != null;
    }

复制代码

这个删除操做与根据key删除的操做没多大区别,不一样的是这里调用的removeNode中传入matchValue为true,表示只有key和value都匹配才能删除节点,而且返回类型不一致。

8、遍历

HashMap的遍历有不少种,这里列举了三种常见的遍历方法

1. for-each遍历entrySet

上面咱们在批量添加元素时,在HashMap的内部,也是经过这种方式来遍历的

for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }

复制代码

因此咱们以前看entrySet方法

HashMap#entrySet

public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }

复制代码

因为是第一次调用该方法,会直接构造这个EntrySet,这个EntrySet是HashMap的内部类

HashMap.EntrySet

final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size() { return size; }
        public final void clear() { HashMap.this.clear(); }

		//获取iterator
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }

		//最终调用了getNode
        public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Node<K,V> candidate = getNode(hash(key), key);
            return candidate != null && candidate.equals(e);
        }

		//最终调用了removeNode方法
        public final boolean remove(Object o) {
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                Object key = e.getKey();
                Object value = e.getValue();
                return removeNode(hash(key), key, value, true, true) != null;
            }
            return false;
        }
        public final Spliterator<Map.Entry<K,V>> spliterator() {
            return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }

		//for-each遍历entrySet
        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

复制代码

在这个类中咱们能够发现不少操做,不过这些操做不少都是直接调用HashMap的方法来实现的,而后forEach方法就是for-each遍历的实现,经过这个方法,咱们大概能够得出for-each在这个类中就是会遍历哈希桶上的每一个链表,而后返回链表上的节点。而且其原理实际上是调用了Iterator.next,因此咱们能够继续看下一个遍历方法来了解下EntrySet的Iterator

2. 使用Iterator迭代

实际使用

Map<Integer, Integer> map = new HashMap<Integer, Integer>();
Iterator<Map.Entry<Integer, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
	Map.Entry<Integer, Integer> entry = iterator.next();
	System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}

复制代码

经过Iterator迭代,实际上是调用了EntrySet的iterator方法

HashMap.EntrySet#iterator

public final Iterator<Map.Entry<K,V>> iterator() {
       return new EntryIterator();
}

复制代码

能够发现这里其实只是返回了一个EntryIterator的对象,因此咱们须要看看这个EntryIterator.

EntryIterator

final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}

复制代码

在实际使用中咱们经过iterator.next的方式获取到当前节点,而在底层其实调用了父类HashIterator的nextNode方法,因此咱们看看父类HashIterator

HashIterator

abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
			//线程不安全,保存modCount
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
			//next初始化时,指向哈希桶上第一个不为null的链表头
            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;
        }

 		
        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();

			//依次取链表的下一个节点
            if ((next = (current = e).next) == null && (t = table) != null) {
				//若是当前链表节点遍历完了,则取哈希桶的下一个不为null的链表头
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        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删除节点
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

复制代码

从上面咱们会发现,在调用map.entrySet().iterator()的时候,实际上会构造一个HashIterator对象,并赋值哈希桶上第一个不为null的链表头给next,iterator.hasNext的操做其实就是判断当前的next是否为null,在nextNode中next会被赋值下一个节点,返回的是当前的节点。

3. for-each遍历keySet和values

在实际中,有时候咱们会遍历key和values的集合,通常状况下都是调用map的keySet和values

实际使用

for (Integer key : map.keySet()) {
	System.out.println("Key = " + key);
}
for (Integer value : map.values()) {
	System.out.println("Value = " + value);
}

复制代码

接着咱们看看其内部实现

HashMap

public Set<K> keySet() {
        Set<K> ks;
        return (ks = keySet) == null ? (keySet = new KeySet()) : ks;
    }

复制代码
final class KeySet extends AbstractSet<K> {
        public final int size() { return size; }
        public final void clear() { HashMap.this.clear(); }
        public final Iterator<K> iterator() { return new KeyIterator(); }
        public final boolean contains(Object o) { return containsKey(o); }
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        public final Spliterator<K> spliterator() {
            return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super K> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

复制代码

看来上面的代码是否是跟entrySet的实现很相似,forEach方法也是一致的,不同的是iterator方法返回的是KeyIterator

KeyIterator

final class KeyIterator extends HashIterator implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }

复制代码

结果这个迭代器跟entrySet的迭代器基本是同样的,只不过这里返回的是nextNode的key。而values的遍历其实跟entrySet和keySet基本是同样的,最终都是HashIterator中的实现。有兴趣的同窗能够自行阅读相关源码,这里再也不进行分析。

4. 小结

从遍历的方法咱们也能够发现,HashMap的遍历是无序的,其顺序是哈希桶从左往右,链表从上往下依次进行遍历的

总结

在Java8中HashMap的底层数据结构是数组,称之为哈希桶。每一个桶里放的是链表,链表中的节点就是哈希表的元素。当添加元素时,采用链地址法解决哈希冲突,若是链表的节点数超过8个就会将当前链表转换成红黑树,来提升插入和查询效率。因为哈希桶是数组,因此存在扩容问题。当哈希表的容量达到阈值时或者初始化的时候,就会发生扩容。另外哈希表在实现过程当中用了不少位运算替代常规操做来提升效率。

参考博客:

相关文章
相关标签/搜索