【Java分析】HashMap源码分析

目录:

一:java集合框架

二:ArrayList

三:LinkedList

四:HashMap

 

总结:

1、HashMap 底层数据结构:数组+单链表,存储的是键值对

2、key 可以为 null,但是只能有一个 key 为 null ,允许多条记录的值为null,key为null的存贮位置在数组0号下标(table[0])找 value 的值

3、HashMap是非线程安全

4、数组是HashMap的主体,链表则主要是为了解决哈希冲突而存在的。

5、如果有链表的话,添加,时间复杂度仍然为O(1)。但是查找操作,就需要遍历链表,然后通过key对象的equals 一一对比。

6、二倍扩容

7、hashmap初始化大小是16

8、Hashmap 处理冲突的方法 :

            A.链地址法:

                        最佳情况下 HashMap 查找时间复杂度 O(1)

                        最坏情况下 HashMap 查找时间复杂度 O(n)

 

                       拉链法有如下几个优点:

                               ①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

                               ②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

                               ③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

                               ④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

 

                      拉链法的缺点

                               指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。
有几种常用的探查序列的方法:

            B.开放定址法:

                        a 线性探测法

                               定义一个哈希算法,算出 index,如果 index 存放了,就看 index+1 有没 有存放。如果 index+1 存放了,就看 index+2 有没有存放,依次类推。

                        b 随机探测法

                               将线性探测的步长从常数改为随机数,即令: j = (j + RN) % m ,其中 RN 是一个随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次序,从而可以避 免或减少堆聚。基于与线性探测法相同的理由,在线性补偿探测法和随机探测法中,删除一个记录后也要打上删除标记。

                        c 二次探测法

                               index+1^2 index-1^2  index+2^2,index-2^2,…,index+k^2,index-k^2    ( k<=m/2 );这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。

            C.公共溢出区:将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。

            D.再哈希: 定义的多个计算公式(哈希算法):a 哈希算法 b 哈希算法 c 哈希 算法 d 哈希算法,这种方法不易产生聚集,但是增加了计算时间。

 

源码分析:

一:先介绍JDK1.7

(1)、属性介绍:

 

 

 

默认数组大小 1<<4 = 16

最大大小限制 1<<30 = 2^30

默认加载因子 0.75f

空Entry数组

 

size大小

threshold阈值 = 当前数组大小*加载因子

 

modeCount线程安全:其中保存了一个modCount属性 是为了线程安全。遍历的时候++ 使用的时候比较一下这俩一样不。

    Entry(key,value)

 

(2)、put方法:

 

                1、判断是否为空,如果为空就去inflateTable()初始化

 

                toSize是创建hashmap时候传入的大小参数,roundUpToPowerOf2()方法把传入的参数搞成2的次方的数(15->16 , 28->32)然后存入capacity再new一个此大小的Entry当做table(底层数组),判空初始化结束。

                为什么是2的次方??

                答:计算下标的时候 hash&length-1 做与运算时保证低位数字的值。

              2、key可以为null,key可以为null 但是 只有一个key等于null的时候就是把key放到了数组的第0个位置。

              3、hash()方法,对key取hash

 

             4、i= indexfor(hash,table.length) 计算i 插入的下标。

                           让hash值与length-1做与运算,这样可以保证:

                           1、数组不会越界,

                           2、低位数字不会改变(与0&的话 全部成0了,hash值就没有意义了,而2的次方-1 的话 低位全部是1,这样就可以保证hash算出的 低位值不变)例:

                                                    16:0001 0000

                                                     15:0000 1111

 

                            %保证index一定是在数组范围内

                            & cpu计算的时候会更快

                            table.length 必须是2的幂

               5、for(En...)遍历table[ i ]下标的链表,查看是否有相同的key 存在的情况如果有相同key的情况,那么就更新这个key的value的值,并且把老的value用oldValue返回回来

               6、如果没有相同的key那么就调用addEntry(hash,key,value,i)然后return null

                       6.1、if()扩容2倍扩容

                                 threshold阈值

                                 当size大于阈值和当前下标不为null,(jdk1.8中没有)时resize()扩容

                             扩容类似数组的扩容,就是先创建一个新的数组,然后把数据复制上去

                              调用transfer()方法进行数据的转移。

                               rehash一般都为false,然后对新的容量计算一个下标

 

 

单线程情况下:

 

                          如此循环,但是 这样的话移动完,数据会逆置过来。

                          多线程之中,,多次循环之后可能会出现构成了循环链表,而导致“死锁”或者叫死循环。原因是线程一已经改变了之前链表的顺序 然后别的线程再来改动的话,就会出现这个问题。

 

                 6.2、createEntry(hash,key,value,bucketIndex)增加头结点在 数组上。

      (3)、get方法

                   先算hash,null返回0号下标,然后再在算出的下标里遍历链表。

 

                 数组加链表,每次把新的数据插入数组节点,然后链表相当于后移了。

                 put(key,value)

                 int hashcode = hash(key)

                 int index = hashcode % table.length;

                 table[index] = new Entry(key, value, table[index]);

 

二:JDK1.8:加入红黑树

 

(1)、put方法:

 

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<K,V>[] tab; Node<K,V> p; int n, i;
    //判断是否为空,如果为空就去初始化。
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //判断  计算出的下标的数组位置上是否为空,是空就 new一个节点存入数组,
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
        //不是空的话 判断是链表,还是树,或者有重复的key
    else {
        Node<K,V> e; K k;
        //是否有相同的key
        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 {
            //bincount计数,遍历一遍链表,看是不是大于8了
            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;
            }
        }
        //如果已经存在了key的节点,就把old节点返回
        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;
}

 

               put之前先对key进行hash

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

 

              为了让元素更加的散列,右移是让高位也参与算法,因为加入的红黑树,所以hash算法可以比jdk1.7中的简便一点。

static final int TREEIFY_THRESHOLD = 8;

 

               如果一个链表的元素大于8,,就转红黑树

               新节点插入链表尾部

                //****原因:反正都要遍历一遍,所以还不如直接加在最后

                // 防止死锁的发生

 

              因为:32:0010 0000

                        16:0001 0000

                        31:0001 1111

              所以: 当31与key与的时候 如果key第五位值为1的话,就相当于是 在之前与16计算的基础上加了16,所以: 扩容后的index = index + oldTable.length;

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值