zoukankan      html  css  js  c++  java
  • HashMap 底层实现原理

    概述

    以下基于 JDK 1.8

    数据结构

    HashMap 实际是一种“数组+链表”数据结构。

    在put操作中,通过内部定义算法寻止找到数组下标,将数据直接放入此数组元素中,若通过算法得到的该数组元素已经有了元素(俗称hash冲突,链表结构出现的实际意义也就是为了解决hash冲突的问题)。将会把这个数组元素上的链表进行遍历,将新的数据放到链表末尾。

    存储数据的对象

    在数组或链表中存储的节点的 Node 对象,以下是源码

        static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            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; }
    
            public final int hashCode() {
                return Objects.hashCode(key) ^ Objects.hashCode(value);
            }
    
            public final V setValue(V newValue) {
                V oldValue = value;
                value = newValue;
                return oldValue;
            }
    

    存储对象Node实际是实现Map.Entry对象接口。

    有 4 个属性

    • final int hash;
      此属性存放的是对象的 key 的hash值,以下是实现:
      static final int hash(Object key) {
          int h;
          return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }
      
      这里还是要根据传入的不同 key 的数据类型有不同的实现 hashCode() 的方式,在此不表。
    • final K key;
      数据的 key
    • V value;
      数据的 value
    • Node<K,V> next;
      指向下一个对象,当出现哈希冲突时,该数组元素会出现链表结构使用 next 指向下一个元素对象
    使用链表结构导致的问题

    通过哈希算法从寻止上能够高效的找到对应的下标,但是随着数据的增长,哈希冲突碰撞过多。在寻找数据时,找到该链表,会通过遍历再寻找对应数据,如此将会使得 get 数据效率越来越低。在 jdk1.8 中,链表元素数量大于等于 8 将会重组该链表结构形成为“红黑树结构”,这种结构使得在 hash 冲突碰撞过多情况下,get 效率比链表的效率高很多。

    数据插入,put()

    几个重要的变量

    • transient Node<K,V>[] table;
      • 存数据的数组,随着数据增多,不断变大,厨师长度是 16:
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
        
    • final float loadFactor;
      • 负载因子,每当扩容时会和数组原长度相乘,默认是 0.75:
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
        
    • int threshold;
      • 允许存储的最大数据量,超过此阈值,便会触发扩容 resize()
    • transient int modCount;
      • 记录内部结构发生变化的次数,put操作(覆盖值不计算)以及其他…
    • transient int size;
      • 实际存储的元素数量

    插入操作 put()

        public V put(K key, V value) {
            // 传入 key 的哈希值,key,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;
            // 检查数组是否为空或数组长度是否为 0,是则进行扩容
            // 数组长度为空会进行初始化,因为在实例化 HashMap 时,并不会进行初始化
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            
            // 根据计算得到的 key 的哈希值,得到应该插入的数组索引点,    
            // 为空则新建元素再插入
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            // 应该插入的位置不为空,已存在数据了
            else {
                Node<K,V> e; K k;
                // 找到已存在的数组元素,若 hash 相等且 key 相等,则直接覆盖
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                // 该节点是否为 TreeNode(红黑树结构),是则红黑树根据键值对直插
                else if (p instanceof TreeNode)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                // 这个已找到的元素既不和待插元素相同,也不是红黑树结构,说明在链表里,接下来遍历链表插入
                else {
                    // 遍历链表
                    for (int binCount = 0; ; ++binCount) {
                        // 链表插入,循环直到链表最后一个节点还没发现 key,那么插入到最后一个节点后面
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            // 链表长度大于 8,则转换成红黑树结构
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
                        // 链表插入,若 key 存在直接覆盖 value
                        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;
        }
    

    在这里插入图片描述

    主要操作是:

    1. 初始化
      • 首选判断table是否为空,数组长度为空,将会进行第一次初始化。(在实例化HashMap是,并不会进行初始化数组)
    2. 插入
      • 不存在该值,新建节点直接插入到数组中
      • 数组中已存在该值:
        • 新值和数组中已存在的值,key 和 hash 都相等,直接覆盖
        • 该节点是红黑树结构,调用红黑树函数插入
        • 这个待插值和原有元素不同,且不是红黑树结构,那么就在链表中找
          • 遍历链表,发现存在该元素则覆盖
          • 遍历链表,最终发现不存在该元素,插入到链表尾。并检查是否长度大于 8,是则把链表转为红黑树
    3. 判断是否扩容:
      • 如果条件满足元素大小size>允许的最大元素数量threshold,则再一次进行扩容操作。每次扩容操作,新的数组大小将是原始的数组长度的两倍。

    扩容操作 resize()

    静态语言在初始化时必须给个数组长度,但 put 操作会让数组装不下,所以要扩容。
    触发扩容的条件是,当前实际元素数 > 数组长度 * 负载因子,即 size > threshold,threshold 是阈值

    resize() 源码
        final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            // 初始容量,是所能允许的最大数量
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {
                // 旧的 capacity 大于了所能允许的最大限制,不扩容
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                // 对原始长度进行 2 倍扩容
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
            // 第一次初始化,把旧的阈值 threshold 
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            
            // 第一次初始化
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            
            // 新的最大允许元素数量值
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
            // 新的数组
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            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;
                                }
                                // 原索引 + oldCap
                                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;
        }
    

    取出数据 get()

    源码

        public V get(Object key) {
            Node<K,V> e;
            return (e = getNode(hash(key), key)) == null ? null : e.value;
        }
    
        final Node<K,V> getNode(int hash, Object key) {
            Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
            // 通过key的hashCode和寻址算法得到数组下标
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
                // 若数组元素中的key和hash相等,则直接返回
                if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                    return first;
                // 若不相等
                if ((e = first.next) != null) {
                    // 是红黑树结构,就在红黑树中找
                    if (first instanceof TreeNode)
                        return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                    // 遍历链表元素
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            return e;
                    } while ((e = e.next) != null);
                }
            }
            return null;
        }
    

    没有修不好的电脑
  • 相关阅读:
    解决ListView异步加载数据之后不能点击的问题
    android点击实现图片放大缩小 java技术博客
    关于 数据文件自增长 的一点理解
    RAC 实例不能启动 ORA1589 signalled during ALTER DATABASE OPEN
    Linux 超级用户的权利
    RAC 实例 迁移到 单实例 使用导出导入
    Shell 基本语法
    Linux 开机引导与关机过程
    RAC 实例不能启动 ORA1589 signalled during ALTER DATABASE OPEN
    Oracle RAC + Data Guard 环境搭建
  • 原文地址:https://www.cnblogs.com/duniqb/p/12702438.html
Copyright © 2011-2022 走看看