转载

【源码分析】HashMap(一)

花了几个小时的时间,给自己总结一下所学的

一、HashMap

HashMap 是基于哈希表 Map 接口的一个实现,通过 Key-Value ( key、value都支持 null )存放数据, 在 JDK1.7 时,底层是 数组 + 链表JDK1.8 改为了 数组 + 链表 + 红黑树
HashMap 实现了 Map 接口, HashMap 中的 Node 静态内部类则是实现了 Map 接口中的内部接口 Entry

【源码分析】HashMap(一)

静态内部类 Node 是一个单向链表, 对应HashMap的拉链式存储. 它实现了 getKey、getValue、setValue、hashCode、equals 这些函数

二、数组 + 链表 + 红黑树

HashMap 为什么采用这种实现方式

1. 容量、加载因子

首先介绍 HashMap 中两个概念: 容量加载因子

HashMap 的默认初始容量为 16 ,加载因子为 0.75f

实际容量 = 16 * 0.75 = 12

加载因子指的是元素对容器的充满程度,当元素达到这个充满程度就会进行自动扩容

为什么默认规定负载因子是 0.75,而不是0.8,0.76

因为负载因子越大,表示可填充的程度越大,那么空间利用率越大,但链表的的长度就会越来越大,查询的效率就会降低,同时 hash冲突 的机会也会增加

负载因子越小,表示可填充的程度越小,那么空间的利用绿越小,造成空间资源浪费,但是链表的长度短,hash冲突的机会小,查询效率高

所以 0.75f 是官方给出的一种时间和空间权衡的 折衷选择

transient Node<K,V>[] table; // 存放内容的实体数组
 transient int size;    // 存放的大小
 transient int modCount;    // 被修改的次数
 int threshold;   // 临界值 = 容量 * 加载因子
 final float loadFactor;    // 加载因子

transient 关键字 表示不参与序列化过程

2. hash冲突

hashmap存放键值对时,通过对象的hashCode 算出 hash 值来确定存储位置的,当hashCode一样时,hash值就是一样的, 当存储的对象多时,可能会出现不同的对象 hash 值相同,这就是hash 冲突, hashmap 底层是通过链表来解决hash冲突的 .

3. 为什么转换成红黑树结构

hashmap默认初始化是链表存储的,当链表的长度过长时,查询效率慢,在相同条件下 链表的查询复杂度为O(n),树型的查询复杂的为O(log(n)). 将查询效率从 O(n) 提升到 O(log(n))

什么时候才转换

首先,看代码

/*
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million
     */

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

定义在 hashmap中的 TREEIFY_THRESHOLD = 8 , 8 链表转成树的阈值

当hashmap 中一个链表的节点足够多时( 因为TreeNodes占用空间是普通Nodes的两倍 ),长度达到了 8 ,就转换成红黑树

当红黑树的节点长度降到为 6 时,又转成链表

为什么阈值设置为 8 而不是其他值

在 hashCode 离散性均匀的情况下,hashmap 中数据的位置均匀 ,很小的概率会用到红黑树结构,几乎链表的长度不会达到 8

然而随机 hashCode 离散性不是很好的情况下,JDK 又不能阻止用户实现离散性低的hashCode,因此就可能导致不均匀的数据分布

根据什么大数据概率统计,泊松分布,得出

hashCode理想情况下链表的长度能达到 8 时的概率为 0.00000006 ,几乎是不发生事件,所以阈值为 8

三、进入putVal方法

执行语句 map.put("lankeren","1069941886") 时,调用的时是

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

见招拆招,进入 putVal()
先通过 对象hashCode 算出 hash 值

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

用到了 ^ 运算

/**
     *  map.put()  
     **/
    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) // 如果table数组还没初始化的进行初始化操作
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)  // 获取该 bin 的头节点
            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))))
                // 若插入的key已经存在哈希映射了(mapping)
               // 将当前节点赋值给 e  然后进行新旧值更新
                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))))
                        // 如果发生已存在该key的哈希映射(mapping), break,然后对该key进行新旧值更新
                        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;
    }

思路:

将数据加入到哈希表中时,

  1. 先对实体数组进行初始化(默认长度16)
  2. 判断该 hash 值对应的位置(table数组下标)是否已经有哈希映射了

    1. 如果有,进入 else
    2. 如果没有,存进该头部
  3. 当前链表不为空,判断待插入的key是否已存在哈希映射

    1. 与头节点判断
    2. 与头节点之后节点的判断
    3. 是否属于树型结构的节点
  4. 更新新旧值
  5. 增加被修改次数
  6. 是否大于临界值

    1. 是 --> 扩容
    2. 不是 --> 无操作

【源码分析】HashMap(一)

笔者太菜了,可能写出了很多错误的理解,望评论斧正,感谢

原文  https://segmentfault.com/a/1190000021123143
正文到此结束
Loading...