对hashMap的初步理解

当我们创建一个hashmap,往hashmap里面put元素的时候,hashmap内部会创建一个数组,数组如果不指定初始容量,那么hashmap在创建对象时,在第一次put元素的时候会默认指定一个数组的初始长度,为16位

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

将元素节点的key换算成hashcode,hashcode通过一个hash值的计算,计算出来一个具体的索引下标index的位置,然后将put进来的元素放入到对应的索引处

 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

进行hash计算的时候需要进行位扰动,进行位扰动的目的是让结点在数组中分散均匀,每一个索引尽量不空着,在不浪费内存的同时提升查找效率,如果只进行取模运算而不进行位的干扰,会导致有些索引没有存值,有的索引下的链表已经很长了,大大降低查询效率。JDK8以后hashMap底层数据结构为:Hash表 = 数组+链表+红黑树,计算hash值的时候进行位干扰,可以让索引下的链表尽量短一点,发挥每一个数据结构的优点,数组结构,链表结构,以及红黑树的结构,将这些优点集中起来从而使得hashMap拥有很高的效率。

int hash = hash(key);,
为什么不直接用hashcode取模,而要用hash去算hash值?当hashMap容量比较大的时候,会产生比较多的hash碰撞, hash函数在运算(即通过hashcode计算索引下标存放位置的时候)的时候,每个字符串或者对象会产生哈希碰撞,产生哈希碰撞会导致计算出来的大量的索引位置重复,重复之后会产生大量哈希碰撞,会导致链表变长,get值的时候查询性能降低,所以在hash值里面会将拿到的hashcode进行一系列的位干扰/位扰动(异或或者位移运算)尽量减少哈希碰撞,减少查询元素的时间复杂度。
数组存储特点:使用数组存储的时候,使用一段连续存储单元存储数据,对于指定下标的查找时间复杂度为O(1)

HashMap<String,String> hashMap = new HashMap<String,String>(13);
        hashMap.put("波妞","宗介");

假设对Key求Hash值:
h = “波妞”.hashcode的值为:
0000 0000 0000 1101 0001 0101 0101 1111
length的初始长度为16
h >>> 16:
0000 0000 0000 0000 0000 0000 0000 1101
hash = h^(h>>>16):0000 0000 0000 1101 0001 0101 0101 1111
n = 16 - 1 :0000 0000 0000 0000 0000 0000 0000 1111
做与运算即可得出十进制的索引值。
切记lengh的初始长度为16(10000),如果不减1,无论上面的hashcode值怎样变化都会导致与出来的十进制结果为0或者16,此时会导致索引下标为1~14永远为空举个例子:
hash: 1010 1111
length: 0001 0000 此时与出来的结果为0,则会导致put进来的元素永远存入0索引处
hash: 1011 1111
length: 0001 0000 此时与出来的结果为1,则会抛出索引越界异常。

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

put过程:
1.对Key求Hash值,然后在计算下标。
2.如果没有碰撞,直接放入桶中,
3.如果碰撞了,以链表的方式链接在后面
4.如果结点已经存在就替换旧值
5.如果桶满了(容量*加载因子),进行resize

 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;
            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;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

往hashmap里面put元素的时候,当put进来的新元素经过hash运算,得出的index值的位置已经存在节点元素了,JDK7处理方式:先校验该索引处已经存在的结点元素或者已经在该索引处形成链表的所有元素的key值是否有重复的,逐一比对做校验,校验完成后若没有重复的key值,则新put进来的结点元素形成一个指针指向数组元素上计算出来索引位置的结点,相对应地在头部去插入,不会一个一个地遍历插入在链表的尾部,在插入结点之前做校验的时候已经将整个链表遍历了,遍历的目的就是为了避免key值重复,避免新put进来元素的key值是在这个链表里已经存在的key,若已经存在会用新值替换旧值)JDK8以后,新put进来的结点元素会用它的键值对与已经存在的元素的键值对做比对,比对后如果新put进来的key值和已经存在的结点元素的key值相同,则会用新的value值覆盖掉旧的value值,如果key值不同,则会判断已经存在的结点下的数据结构是红黑树还是链表,如果是红黑树则会把新put进来的结点插入到红黑树中,往红黑树里面插入元素会破坏红黑树的平衡性,此时需要经过树旋转以及着色来重新维护红黑树的平衡性,如果数据结构没有达到红黑树的级别则会在原有结点的基础上形成一条链表,当链表的数据结构长度超过8的时候会转换成红黑树,因为链表对于查找操作需要遍历链表中的所有结点逐一进行对比,复杂度为O(n)。
为什么每次size++的时候modCount的值也会加加?
modCount为快速失败机制fail fast,在使用hashMap的时候当取出来某个迭代器进行迭代时,同时又对hashMap进行put或者remove操作,在迭代的同时并发操作可能会导致迭代器当中的内容有问题。假设初始化迭代器的容量为5,因为迭代过程中进行put或者remove操作,初始容量会发生变化,则迭代器会报异常。
由于笔者基础薄弱对于hashMap的底层存在诸多不理解的地方,欢迎大家纠错指正,同时也谢谢杨过老师一丝不苟的授课。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值