HashMap深度解析------奶妈型剖析

HashMap底层学习

数据结构

JDK1.8之前使用的是数组加链表的数据结构,

JDK1.8之后使用的是数组加链表加红黑树,在链表长度超过8时候加入红黑树,红黑树的加入使得时间复杂度从O(n)变成了O(logn)

基本属性

//默认初始化容量
static final 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;
//默认最小的树的长度
static final int MIN_TREEIFY_CAPACITY = 64;

//用于保存Node节点的数组
transient Node<K,V>[] table;
//用于保存存储在HashMap中的键值对数量  
transient int size;
//用于保存对HashMap进行结构性更改操作(例如resize,rehash,put)
transient int modCount;
//用于保存阈值
int threshold;
//用于保存负载因子
final float loadFactor;

构造方法

  	//有参的构造方法
	/*
	自定义初始化容量和负载因子
	这里设置自定义容量的时候需要注意几个问题:
	1.因为我们计算得Index是按位与运算得到的,我们2的幂数-1得到的二进制一定是11...11这样的数,
	如果我们不是2的幂数,就例如15,15-1=14,14的二进制位1110,那么不管怎么样最后一位进行与运算一定是0,那么这样计算的话,
	我们的0001,1001,0101,0011,1011,1101,0111这几个index值都无法进行存储,这样首先是浪费了资源,其次是加大了哈希碰撞的几率
	*/
	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;
        this.threshold = tableSizeFor(initialCapacity);
    }
	
	//自定义容量,使用的是默认的负载因子
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

	//默认构造方法,使用的是默认的负载因子(在resize()中会对其进行一个内存空间分配,容量大小为16)
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }


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

Node<K,V>结构

//该Node节点是HashMap编写的一个静态内部类
static class Node<K,V> implements Map.Entry<K,V> {
    	//用于保存key的hash值
        final int hash;
    	//保存传入的key
        final K key;
    	//保存传入的value
        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; }

    	//重写了hashcode和equals方法用处在取值时候的判断和存值时候判断是不是需要加在节点的next属性上(即链表尾部)
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        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;
        }
    }

put方法

	//这是put方法,实际上时调用内置的final方法putVal进行一个存储
	public V put(K key, V value) {
    	//这里放入五个值
    	/*
    	1.hash(key): 计算了key的hash值
    	2.key: 传入的key
    	3.value: 传入的value
    	4.onlyIfAbsent: 这里传入为false,代表我们可以替换掉原来的key的vlaue,如果为true代表如果原来key已经存储过了,那么						 其value就不会被更改。
    	5.evict: 对应方法没有实际操作
    	*/
        return putVal(hash(key), key, value, false, true);
	}

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //这是用于保存Node的数组,HashMap数据结构是数组+链表的形式
        Node<K,V>[] tab; 
        Node<K,V> p; 
        int n, i;
        //第一步先将属性table赋值给tab,判断tab是否为创建或者tab是否存储了数据(如果符合这个if说明该HashMap未使用或者清空)
        if ((tab = table) == null || (n = tab.length) == 0)
            //这里调用到了resize()方法(讲解在下面),返回了一个Node数组,并把数组的长度赋值给n(因为这里为空,所以这里是创建			了一个容量为16, threshold为12)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            //这里是判断进行Hash运算后对应下标的是否已经存储了数据
            /*
            如果对应下标不存在Node: 那么就直接创建一个Node对象直接存储
            如果对应下标存在Node: 那么首先进行判断hash是否一致,如果一致则进行替换,不一致则放入链尾(或者红黑树中)
            */
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            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);
                        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;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //存储过后,当我们发现size+1后就会超过阈值,那么就会进行扩容操作
        if (++size > threshold)
            resize();
        //该方法什么都没做。。。。
        afterNodeInsertion(evict);
        return null;
    }

resize()方法(扩容)

final Node<K,V>[] resize() {
    	//首先将属性table赋值给oldTable
        Node<K,V>[] oldTab = table;
    	//这里判断的是该oldTab是否为空,用三目表达式返回table的length
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
    	//获取到阈值赋值给oldThr
        int oldThr = threshold;
        int newCap, newThr = 0;
    	//if为真说明oldTab = table = null,
        if (oldCap > 0) {
            //进入该判断分支说明oldTab = table != null,如果容量超过MAXIMUM_CAPACITY(最大容量)
            //就将阈值设置为最大int值,并返回原来的table(说明已经无法继续存储了)
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                /*
                (newCap = oldCap << 1) < MAXIMUM_CAPACITY
                 oldCap >= DEFAULT_INITIAL_CAPACITY
                 第一行:将oldCap值向左移动一位,即*2(扩容操作),但是并且要保证扩容后的容量要小于最大容量
                 第二行:oldCap要大于DEFAULT_INITIAL_CAPACITY(16)
                */
                
                //进入该分支以后会将newThr = oldThr向左移动一位
                newThr = oldThr << 1;
        }
        else if (oldThr > 0) 
            //进入该分支说明满足上一步的第一个条件,但是oldCap < 16(应该是自定义长度的问题)
            newCap = oldThr;
        else {               
            //进入该分支说明oldCap = oldThr = 0
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            //进入该分支说明oldTab = table = null或者table.length = 0
            //前面最后一个else判断和这个是配合的
            //newCap = 16  loadFactor = 0.75
            float ft = (float)newCap * loadFactor;
            //新的阈值赋值的三木表达式,判断依据是:newCap和新的阈值均小于最大容量,真为ft,假为最大int
            //此时newThr = 16 * 0.75 = 12 
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
 		//我们得到的这个newThr赋值给属性中的threshold(阈值)
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
    	//创建一个新的Node数组,容量为之前赋值的newCap
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    	//将属性中的Node数组table = newTab(扩容后的Node数组)
        table = newTab;
    	//这是对旧Node数组->新Node数组的拷贝过程,期间需要进行rehash
        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)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        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) {
                                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) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

在hashmap中是先对key计算它的hash方法返回的值,hash方法是将key的hashcode值与(capacity-1)进行&运算,得出一个Index值作为table的下表值,然后将key,value,hashcode封装在Node对象中,并且存放在table[index]中

这时候可能会发生哈希碰撞,即两个Key值的hash方法返回值一样,那样就会导致在table数组中的覆盖,在这里我们是在Node中使用链表的形式,Node中有一个next属性,我们是在如果他们的hash一样的话,我们就将这个node存放在第一个node的next上,这样就形成了一个链表结构。

Hashmap的链表插入方法及问题

在JDK1.8之前我们使用的是头插法,JDK1.8之后使用的是尾插法,使用尾插法原因是可以避免扩容时头插法导致的成环问题

举个例子,比如我们一开始ABC存放,然后链表指向是C->B->A,这时候我们进行resize,C没有发生哈希冲突,AB仍哈希冲突,则此时的A->B,但是B中存放的下一个node指向A,这样就形成了环,此时当我们调用get方法获取时候就会出现死循环。然而我们使用尾插法时候,仍然是A—>B,这样就不会形成环,根本原因就是头插法会改变链表原来的顺序,而尾插法不会改变顺序

Hashmap的高并发问题

1.多个线程同时put一个index的值时候,会使得只有最后一个线程的值给存入,其他线程则会被覆盖。

2.多个线程在进行resize操作时候,如果一个线程已经扩容结束,而另一个刚开始,则会导致扩容成原来的4倍,从而导致数据丢失。

为什么要进行扩容?

因为我们数组的特性就是查询快,增删慢;而链表的特性为查询慢,增删快。如果我们不对数组进行扩容,当我们put足够大的数据时候,如果利用的是我们默认的数组大小16的话,那必然会导致链表的长度会变得非常的长,从而导致我们的查询速度会变得很慢

为什么容量必须为2的幂数

因为我们计算得Index是按位与运算得到的,我们2的幂数-1得到的二进制一定是11…11这样的数,如果我们不是2的幂数,就例如15,15-1=14,14的二进制位1110,那么不管怎么样最后一位进行与运算一定是0,那么这样计算的话,我们的0001,1001,0101,0011,1011,1101,0111这几个index值都无法进行存储,这样首先是浪费了资源,其次是加大了哈希碰撞的几率

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值