Map接口的实现类---HashMap类续

HashMap()采用所有的默认配置值,其中的参数值有initialCapacity:int初始化容积,默认值DEFAULT_INITIAL_CAPACITY=16,第2个参数是loadFactor:float用于设置加载因子,取值范围为0-1之间,默认值为DEFAULT_LOAD_FACTOR=0.75,值越大则存储数据越多,hash冲突的概率越高;值越小则越浪费空间,但是hash冲突的概率越低。为了减少hash冲突,引入了加载因子,加载因子表示容器中最多存储百分子多少的数据。例如默认容器为16,默认加载因子为0.75,则表示最多存储16*0.75个元素,如果大于这个值则需要进行扩容 

tableSizeFor用于计算容积值,不是设置多少就是多少。就是计算一个2**n值>=设定的容积值.例如初始化容积参数值为7则实际创建容积值为8

注意:在构造器中并没有创建任何用于存储数据的集合---延迟加载,第一次存储数据时才进行空间分配

为什么初始化容积值需要转换位2的n次方? 

求余的实现方法:
        2%8=2  2 & (8-1)  0010  & 0111 = 0010
        9%8=1  9 & (8-1)  1001 & 0111 = 0001
        15%8=7  15 & (8-1) 1111 & 0111=0111
         每个Node[]数组中的元素被称一个桶bucket,一个桶对应一个hash映射的值,例如0,1等,可能会出现不同的key,但是hash映射的位置相同,例如16、32等,这采用链表结构存储hash映射值相同的所有数据(JDK8+在单个链表长度大于阈值8时自动转换为红黑树,删除节点使某单个链表节点数小于阈值6时会自动从红黑树退化为链表结构)

相关参数:
        - capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的2倍。
        - loadFactor:负载因子,默认为 0.75。
        - threshold:扩容的阈值,等于 capacity * loadFactor
JDK1.8以前版本的实现HashMap底层采用的是Entry数组和链表实现。JDK1.8和以后的版本是采用Node数组(Entry)+ 单向链表 + 红黑树实现
        Map主要用于存储键key值value对,根据键得到值,因此键不允许重复,但允许值重复。
        HashMap是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。
        HashMap最多只允许一条记录的键为null;允许多条记录的值为null
        HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致,在JDK1.7中会出现环形链,虽然JDK1.8不会出现环形链,但是还会有rehash操作出现死循环、脏读问题、size值不准确等问题。
        如果需要同步,可以用Collections的synchronizedMap方法使HashMap具有同步的能力。

 如何判断环型链?

        创建一个Set,然后遍历整个集合,将每个元素的key值存入set,如果要存的key值已经存储在set中,则出现环型链
        Java7中使用Entry来代表每个HashMap中的数据节点,Java8中使用Node,基本没有区别,都是key,value,hash和next这四个属性,不过,Node只能用于链表的情况,红黑树的情况需要使用TreeNode。
         TREEIFY_THRESHOLD为 8如果新插入的值是链表中的第 9 个会触发下面的 treeifyBin(树化操作,就是将单向链转换为红黑树),也就是将链表转换为红黑树。
         JDK8+插入数据到链表的最后面,Java7是插入到链表的最前面 

HashMap的put方法的具体流程

public V put(K key, V value) { 以key存储value值,返回原始位置上的value值
        //先执行hash(key)根据key获取一个hash值, 参数2是要存储的key值,参数3是要存储的value,参数4表示如果当前位置已存在一个值,是否替换,false是替换,true是不替换。参数5是否在创建模式,如果为false,则表是在创建模式
        return putVal(hash(key), key, value, false, true);
}
    
        hash方法:将高位数据移位到低位进行异或计算,这是因为有些数据计算出的哈希值差异在于高位,而HashMap里的哈希寻址是忽略容量以上的高位的,可以有效地避免类似情况下的哈希碰撞
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
    
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; 
        Node<K,V> p; 
        int n, i;
        // 1.如果table为空或者长度为0,即没有元素,那么使用resize()方法扩容。resize方法兼顾两个职责,创建初始化数组[因为数组采用的是延迟处理]或者在容量不满足需求时进行扩容处理。插入数据if (++size > threshold)resize();进行条件扩容
        if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
         // 2.计算插入存储的数组索引i,此处计算方法同的indexFor()方法,如果数组为空,即不存在Hash冲突,则直接插入数组    
                if ((p = tab[i = (n - 1) & hash]) == null)  //通过使用hash值对当前数组长度n进行求余获取key对应的下标
                tab[i] = newNode(hash, key, value, null);  如果对应下标位置上的元素为null,则新创建一个Node对象存储到当前位置
        else {  如果key对应位置非空
                 Node<K,V> e; K k;
        如果当前节点上的key值和需要插入的key值相等(==或equals),则后添加的节点替换原来的节点
                if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))  
                        e = p;
                else if (p instanceof TreeNode)  //如果当前节点上的key值和需要插入数据的key值不相等,而且当前位置上的Node对象是TreeNode类型,则确定当前桶上存放的是红黑树,则调用红黑树的处理将当前key对应的数据插入红黑树中
                        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else { 如果当前节点上的key值和需要插入数据的key值不相等,而且当前位置上的Node对象是不是红黑树
                        for (int binCount = 0; ; ++binCount) {
                                if ((e = p.next) == null) {  如果到达链表末尾则将数据插入到链尾上
                                        p.next = newNode(hash, key, value, null);
                                if (binCount >= TREEIFY_THRESHOLD - 1) //如果当前链表上的节点个数大于树化阈值8
                                        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;

}

        创建节点对象的方法
        Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
                return new Node<>(hash, key, value, next);
        }    

        容量、负载因子和树化
        因为容量和负载因子决定了可用的桶的数量,空桶太多了会浪费空间,如果使用太满则会严重影响操作的性能。

插入key所对应的value,首先根据key计算对应的桶位置下标(n-1)&hash,然后再判断桶上是否存储了数据。如果没有存储数据则将key和value封装为Node对象【单向链的第一个节点】,插入对应的桶位置;如果已经插入了数据【对应位置上应该有一个单向链表或红黑树,如果第一个节点为Node则是单向链,如果TreeNode则是红黑树】,则根据是否为TreeNode,进行进一步处理,如果是红黑树,则调用TreeNode中的putTreeVal方法将数据插入到红黑树中;如果是单向链表,遍历当前桶位置上的整个链表,如果某个节点上的key值和插入数据的key值相等则后盖前【key不变value变】,如果所有节点上的key值和插入数据的key值不相等,则插入当前链表的末尾,最后再判断是否需要进行树化处理

扩容发生条件为【负载因子*容量 < 元素数量】,所以预先设置的容量需要满足大于【预估元素数量/负载因子】,同时它是大于等于预先设置的容量的2的幂数
        - 如果没有特殊需求不要轻易更改,JDK自带默认负载因子是适用于通用场景需求的
        - 如果确实需要修改建议不要超过0.75,因为会显著增加冲突,降低hashmap性能
        - 如果使用太小的负载因子,否则可能会导致更加频繁的扩容,增加性能开销,本身访问性能也会受到影响

        如果容量小于64只会进行简单扩容,如果容量大于64则会进行树化改造。树化处理可以避免哈希碰撞攻击

扩容处理
        - 在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容
        - 每次扩展的时候,都是扩展2倍
        - 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值