浅析JAVA的HashMap(JDK1.8)

关于HashMap的分析文章网上有很多,我现在也以自己的理解来简单分析下HashMap。这里主要针对JDK1.8来讲解,若有不足或者错误之处还请多多指教。

在分析之前先转载一张java集合的关系图https://img-blog.csdn.net/20160124221843905

一、底层数据结构

在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理hash冲突,同一hash值的键值对会被放在同一个位桶里,当桶中元素较多时,通过key值查找的效率较低。

而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8),时,将链表转换为红黑树,这样大大减少了查找时间。

JDK7及之前

JKD8及之后

由上面结构图可知,HashMap由数组和链表共同构成,JDK8对HashMap做了优化,采用了红黑树的数据结构来做优化,使得HashMap存取速度更快。


二、HashMap的实现原理

1、几个重要的参数

//默认初始容量16 
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
//容量最大值 
static final int MAXIMUM_CAPACITY = 1 << 30; 
//默认加载因子0.75 
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;
//存储数据的数组
transient Node<K,V>[] table;
//key-value的数量
transient int size;
//被操作的次数
transient int modCount;
//阈值 size>threshold时扩容resize()
int threshold;
//加载因子 默认= DEFAULT_LOAD_FACTOR
final float loadFactor;

2、构造函数HashMap(JDK1.8)

 public HashMap(int initialCapacity, float loadFactor) {
        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;
        //tableSizeFor返回的是容量,即table.length
        //这里为了不初始化table,赋值给threshold ,resize()的时候做处理
        this.threshold = tableSizeFor(initialCapacity);
    }
 
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
 
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

无参构造函数时,仅仅是初始化loadFactor让其等于默认值。

Map通过构造函数new一个HashMap时,其内部存储数据的数组并没有实例化,而是在PUT方法中去做了一件判断table是否为空的事,若为空就会调用resize()方法,resize()第一次调用就会实例化一个长度为DEFAULT_INITIAL_CAPACITY的Node[]数组。

//该算法让最高位的1后面的位全变为1
//cap-1的目的是为了找到的目标值大于或等于原值(当原值是2的整数次幂的时候)
//例:cap = 5 return为8,cap = 7 return为8,cap=8 return为8,cap=9 return为16
static final int tableSizeFor(int cap) {
    //比如 cap = 1010 (十进制为10)
    int n = cap - 1;//1001 (9)
    n |= n >>> 1;//1101 (13)
    n |= n >>> 2;//1111 (15)
    n |= n >>> 4;//1111 (15)
    n |= n >>> 8;//1111 (15)
    n |= n >>> 16;//1111 (15)
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

3、HashMap取get方法

get(K key,V value)

判断表是否为空或者待查找的桶不为空
首先检查待查找的桶的第一个元素是否是要找的元素,如果是直接返回
桶内红黑树,则调用getTreeNode()查找红黑树
桶内是链表,遍历链表寻找节点

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


final Node<K,V> getNode(int hash, Object key) {
	Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
     //表不为空&&表长大于0&&待查找的桶不为空
	if ((tab = table) != null && (n = tab.length) > 0 &&
		(first = tab[(n - 1) & hash]) != null) {
        //首先检查桶中的第一个节点,如果相等,则直接返回
		if (first.hash == hash && // always check first node
			((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;
}

4、HashMap存put方法

put(K key,V value)

先来看一张流程图:

该流程图阐述了putVal()方法的整个执行过程。现在我们来看putVal()的源码

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
//业务逻辑处理
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    
    //定义一个数组,一个Node,n存放数组长度,i用于存放key在数组中的索引        
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    //如果表为空或者表的容量为0,resize初始化表        
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    
    //根据hash得到在tab数组中索引位置的桶,如果桶为空,则将节点直接插入桶中
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    
     //桶不为空  
    else {
        Node<K,V> e; K k; //重新定义一个Node,和一个k
        
	    //首先判断桶中第一个节点的hash与待插入元素的key的hash值是否相同且key是否"相等",如果相等,赋给变量e     
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
            e = p;	
        //是树节点,则调用putTreeVal添加到红黑树中
	    else if (p instanceof TreeNode)  
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 
         //否则是链表,遍历链表,如果不存在相同的key,则插入链表尾部,
         //并且判断节点数量是否大于树化阈值,如果大于则转换为红黑树;如果存在相同的key,break,遍历链表结束	
        else {	
            for (int binCount = 0; ; ++binCount) {
                //表尾的next节点必然为null
                if ((e = p.next) == null) {
                     p.next = newNode(hash, key, value, null);
		            if (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;
            }
        }
        //e不为空表示存在相同的key,替换value并返回旧值
	    if (e != null) { // existing mapping for key
            V oldValue = e.value;    
	    if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //e != null表示有相同的key,直接return oldValue。
    //剩下的情况链表元素增加,并判断是否大于阈值,如果大于,则扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

链表转化为红黑树:treeifyBin()

final void treeifyBin(Node<K,V>[] tab, int hash) { 
    int n, index; Node<K,V> e; 
    //如果表为空或者表的长度小于树化的容量,resize()扩容而不是树化 
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 
    resize(); 
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //hd是转换为树节点后桶中的头节点   tl记录上一个遍历的节点 
        TreeNode<K,V> hd = null, tl = null; 
        do { 
            //将hash位置处的桶中的每个节点包装成树节点,p记录当前遍历的节点 
            TreeNode<K,V> p = replacementTreeNode(e, null); 
            if (tl == null) 
                hd = p; 
            else { 
                p.prev = tl; 
                tl.next = p;
             } 
            tl = p; 
            //循环将桶中每个节点替换为树节点,最终结果就是链表转换为双向链表,prev指向前一个节点,next指向后一个节点 
        } while ((e = e.next) != null); 

        if ((tab[index] = hd) != null) 
            //将双向链表转化为红黑树 
            hd.treeify(tab); 
    } 
}

5、扩容机制resize()

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //旧表的容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //旧的阈值
        int oldThr = threshold;
        //记录新表的容量大小和阈值
        int newCap, newThr = 0;
        //旧表容量大于0,表示被初始化过,需要执行的是扩容操作
        if (oldCap > 0) {
            //如果旧表容量大于容量最大值,那么阈值为Interger的最大值,即提升阈值,不再进行扩容,返回旧表
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //否则,扩容为原先容量的1倍,阈值也扩容为原来的一倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //oldCap不大于0,表示该表未被初始化,需要进行初始化,需要确认表的大小及阈值
        //旧表容量为0,阈值大于0,则用阈值大小作为容量
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //否则,表的容量为默认初始容量16,阈值为默认初始容量16*加载因子0.75
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //如果新表阈值为0,则利用新容量*加载因子计算
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //将新的阈值赋给HashMap的阈值成员变量
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        //新建数组,大小为newCap
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //将新建的表赋给HashMap的表成员变量
        table = newTab;
        //如果旧表不为空,则需要进行扩容
        if (oldTab != null) {
            //遍历旧表中的每一个桶
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //将不为空的桶重hash到新表中
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //桶中只有一个元素,将该元素放到新表的桶中
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //桶中存放的是红黑树
                    //和下面的链表处理方式相似,生成loHead和hiHead链表/红黑树,放在newTab的j位置和j+oldCap位置
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //桶中存放的是链表
                    else { // preserve order
                        //lo就是扩容后仍然在原下标的元素链表
					    //hi就是扩容后下标为  原位置+原容量  的元素链表,从而不需要重新计算hash。
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //e.hash&oldCap == 0 元素位置还在原位置
                            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);
                        //loTail不为空则将loHead放到扩容后的talbe的j位置
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                         //hiTail 不为空则将hiHead放到扩容后的talbe的j + oldCap位置
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        //返回扩容后的新数组或者初始化后的数组
        return newTab;
    }

resize的实例图

 

最后

参考:https://blog.csdn.net/goosson/article/details/81029729

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值