美文网首页Java架构技术栈
离程序猿又近了一步:HashMap全解析

离程序猿又近了一步:HashMap全解析

作者: 若丨寒 | 来源:发表于2020-07-13 16:34 被阅读0次
    离程序猿又近了一步:HashMap全解析

    HashMap是键值对的集合。为什么要写它呢? 首先是因为HashMap日常使用比较多,并且面试中是大概率被问到的面试题。 所以我们对它的设计和源码来做一个分析。

    准备的技术点

    单链表、双链表、红黑树、二叉搜索树,hash

    单链表

    单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。 单链表的实际使用场景并不多,比如只是频繁对头/尾结点进行操作,单链表最佳

    双链表

    双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。 双链表的主要优点是对于任意给的结点,都可以很轻易的获取其前驱结点或者后继结点,而主要缺点是每个结点需要添加额外的next域,因此需要更多的空间开销,同时结点的插入与删除操作也将更加耗时,因为需要更多的指针指向操作。

    二叉搜索树

    又称二叉查找树,亦称二叉搜索树,满足下面性质 若左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若右子树不空,则右子树上所有结点的值均大于它的根结点的值; 左、右子树也分别为二叉排序树; 没有键值相等的结点。

    红黑树

    红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。 在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

    性质1. 节点是红色或黑色。 性质2. 根节点是黑色。 性质3.所有叶子都是黑色。(叶子是NUIL节点) 性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点) 性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

    这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。 性质4导致路径上不能有两个连续的红色节点。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长

    hash算法

    希算法并不是一个特定的算法而是一类算法的统称。哈希算法也叫散列算法,一般来说满足这样的关系:f(data)=key,输入任意长度的data数据,经过哈希算法处理后输出一个定长的数据key。同时这个过程是不可逆的,无法由key逆推出data。 如果是一个data数据集,经过哈希算法处理后得到key的数据集,然后将keys与原始数据进行一一映射就得到了一个哈希表。一般来说哈希表M符合M[key]=data这种形式。 哈希表的好处是当原始数据较大时,我们可以用哈希算法处理得到定长的哈希值key,那么这个key相对原始数据要小得多。我们就可以用这个较小的数据集来做索引,达到快速查找的目的。 稍微想一下就可以发现,既然输入数据不定长,而输出的哈希值却是固定长度的,这意味着哈希值是一个有限集合,而输入数据则可以是无穷多个。那么建立一对一关系明显是不现实的。所以"碰撞"(不同的输入数据对应了相同的哈希值)是必然会发生的,所以一个成熟的哈希算法会有较好的抗冲突性。同时在实现哈希表的结构时也要考虑到哈希冲突的问题。

    源码

    本质上就是数组+链表

    数组

    transient Node<K,V>[] table;
    

    单链表

    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;        }        public final boolean equals(Object o) {            if (o == this)                return true;            if (o instanceof Map.Entry) {                Map.Entry<?,?> e = (Map.Entry<?,?>)o;                if (Objects.equals(key, e.getKey()) &&                    Objects.equals(value, e.getValue()))                    return true;            }            return false;        }    }
    

    双链表-红黑树

    static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {        LinkedHashMapEntry<K,V> before, after;  // 在hashMap中并没有使用这两个节点信息        LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {            super(hash, key, value, next);        }    }
    
    static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {        TreeNode<K,V> parent;        TreeNode<K,V> left;        TreeNode<K,V> right;        TreeNode<K,V> prev;        boolean red;                ............ // 省略方法代码    }
    

    扩容原理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) {            if (oldCap >= MAXIMUM_CAPACITY) {                threshold = Integer.MAX_VALUE;                return oldTab;            }            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&                     oldCap >= DEFAULT_INITIAL_CAPACITY)                newThr = oldThr << 1; // double 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;                            }                            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;    }
    

    容器大小为2的n次方,扩容因子为m;默认n = 4也就是默认大小16, m = 0.75。 存在3种情况下会扩容

    容器内数组未初始化或者大小为0 此时已存储元素个数大于2^n * m 会扩容 容器个数小于64,某个位置的单链表长度大于等于7时,这时不直接转换为树结构,而是进行扩容

    插入操作put

    是否为第一次加入新元素,初始化容器(也是调用扩容方法) 如果加入key的hash值对应索引位置无数据,直接插入 如果key的hash值对应索引位置有数据,且节点为红黑树结构,则查看2节中关于红黑树插入的内容 如果key的hash值对应索引位置有数据,且节点为单链表结构,则进行for循环处理: a)链表存在数据,其key与当前物理地址相同或者key的equal比较相同,则重置数据返回, b) 若是已经到链表尾部,则新增节点,加入到尾部;如果加入后数据大小>= 7 则进行树化,查看2节中树化方法 尺寸+1,判断是否达到阈值,达到则进行扩容

    获取元素

    获取元素比较简单,以key的hash值找到索引位置,然后根据位置的节点特点来查找元素节点为空,则无此元素 节点为红黑树节点,查看2章节中getTreeNode方法分析 节点为单链表,从头到尾进行比对,如果比对成功,则退出,否则返回null;比对依据:hash值相同且物理地址相同或者equal方法相同

    删除方法

    如果未加入过元素,则不处理 如果删除key的hash对应索引位置元素为空,则不处理 如果索引节点的key和要删除的key比对相同,则删除节点即为索引节点 索引节点后驱为空,则不需要处理 索引节点后驱不为空,则在链表或者红黑树中查找此节点 红黑树结构使用removeTreeNode删除元素,单链表,删除元素后,其前驱的后继为其后继;并大小减1

    清空

    所有索引位置置空,也即断开单链表或者双链表-红黑树的根节点,进而删除所有元素

    来源:https://www.tuicool.com/articles/q6v6VjE

    相关文章

      网友评论

        本文标题:离程序猿又近了一步:HashMap全解析

        本文链接:https://www.haomeiwen.com/subject/cxxzcktx.html