• HashMap(jdk1.8)


    0.底层实现加了红黑树

    数组+链表(链表长度超过阈值,默认8-->红黑树)

    HashMap在进行put get remove的时候,都是先计算hash,然后根据hash定位桶的位置(table[]的下标),桶中是hash值冲突的键值对组成的链表,然后遍历该链表进行相应的操作,当冲突很多时,遍历链表效率低,红黑树的效率高

    1.用于描述键值对的内部类由Entry<K,V>变成了Node<K,V>,多了一个描述红黑树的内部类TreeNode<K,V>

    static class Node<K,V> implements Map.Entry<K,V> {

    final int hash;  //1.7中,hash不是final的
    final K key;
    V value;
    Node<K,V> next;

    ……

    }

    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {

    TreeNode<K,V> parent; // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev; // needed to unlink next upon deletion
    boolean red;

    ……

    }

     

    2.成员变量

    transient Node<K,V>[] table;  哈希表

    static final int TREEIFY_THRESHOLD = 8;  一个桶中冲突的个数大于该值时,将链表转化成红黑树

    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
      treeifyBin(tab, hash);

    static final int UNTREEIFY_THRESHOLD = 6;   一个桶中冲突的个数小于该值时,将红黑树转化成链表

    static final int MIN_TREEIFY_CAPACITY = 64;   该值最少为TREEIFY_THRESHOLD 的4倍,将一个链表转化成二叉树的过程中,如果table.length小于该值,会resize(),,也就是说,当桶的个数(Node数组的长度)小于该值的情况下,一个桶中的链表的长度大于TREEIFY_THRESHOLD 阈值,需要进行链表转换成红黑树,此时,进行扩容,而不是树化

    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
      resize();

    3.构造器

    在构造器中判断参数合法之后,初始化loadFactor(加载因子)和threShold(阈值),并不真正初始化哈希表,即存储键值对的数据结构并不是在构造方法里初始化的,而是在第一次调用put方法时,在put方法中调用resize方法进行哈希表的初始化

        public HashMap(int initialCapacity, float loadFactor) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: " +
                                                   initialCapacity);
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: " +
                                                   loadFactor);
            this.loadFactor = loadFactor;
            this.threshold = tableSizeFor(initialCapacity);  //该方法将返回一个大于等于initialCapacity的2的幂次方的值
        }

      

    1.7中,将阈值初始化为初始容量  threshold = initialCapacity;   在之后,第一次put的时候,需要调用inflateTable方法初始化哈希表,在此时,会将容量调整为大于等于初始容量的2的幂次方的值

    1.8中,阈值初始化为大于等于初始容量的2的幂次方的值  this.threshold = tableSizeFor(initialCapacity);  

    何时初始化存储键值对的数据结构?

    1.7 在调用put方法时,如果table是null,则初始化Entry<K,V>[] table,调用initfatetable方法初始化,在该方法中,将容量调整为大于等于initialCapacity的2的幂次方的值。

    1.8 在调用put方法时,如果table是null,则初始化Node<K,V>[] table,调用resize方法初始化

    4.hash计算

      hash值的作用:定位桶的位置(n - 1) & hash  n = table.length,定位桶的位置要用哈希表的长度-1 & hash值

      为何hash没有直接使用对象的HashCode?

      哈希表的长度n相较于对象的HashCode比较小,如果直接使用对象的HashCode和(n-1)作与运算,可能HashCode只有低几位参与了运算,因此,hash值是对对象的HashCode调整之后的值,为的是让HashCode的高几位也参与运算,减少随机性带来的影响。

    static final int hash(Object key) {

    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

    }

    1.7和1.8计算hash值的方法不一样,但是都是对对象的HashCode做了一些位运算,目的都是为了减少哈希冲突

     

    5.查找get

    定位桶的位置,看桶中是TreeNode类型的红黑树,还是Node类型的链表

      调用查找红黑树的方法 or 遍历链表

        public V get(Object key) {
            Node<K,V> e;
            return (e = getNode(hash(key), key)) == null ? null : e.value;
         // 如果有key对应的键值对,返回其value,没有,返回null
        // 所以,返回null时,有可能键对应的值为null,有可能不存在相应的键值对
    }
    final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 如果哈希表是Null,表明再创建HashMap之后,还没有put元素,直接返回 null
          哈希表不是null,定位桶的位置
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        // 先判断第一个结点
          如果第一个结点就是要找的键值对,直接返回
          如果第一个结点不是要找的键值对,那么, 判断桶中是红黑树还是链表
          红黑树:调用TreeNode的getTreeNode方法查找
          链表:遍历链表查找
    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;

    6.插入put,如果键值有重复的,返回旧值;没有重复的,返回null

        扩容、树化(将桶中的链表转化成红黑树)

        调用put方法, 计算hash(key),计算hash值是为了定位桶的位置

      在put方法中调用putVal方法

      如果哈希表是null(调用构造器创建HashMap之后还没有进行table的初始化),调用resize方法初始化哈希表

      如果已经初始化了,定位桶的位置 length-1  &   hash

        如果当前桶是空的,直接创建Node并插入

        如果当前桶不是空的(有冲突)

          判断第一个结点(不论put、get、remove都先判断第一个结点),看当前桶中第一个结点是否和要插入的键值对的键重复,重复,用新值覆盖旧值,结束

          不重复,判断当前桶中是链表还是红黑树

            红黑树:调用内部类TreeNode的putTreeVal方法,如果红黑树中有重复的,将重复的节点返回,之后用新值覆盖旧值;没有重复的,插入在合适位置

            链表:从头到尾遍历链表,没有重复的,插在链表尾部;有重复的,将重复的节点记录下来,之后用新值覆盖旧值

    注意:用新值覆盖旧值是有条件的,putValue的方法入参onlyIfAbsent决定了是否仅在旧的value值==null时,用新值覆盖旧值

        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;  //调用构造器创建HashMap之后,只是初始化了加载因子和阈值,并为初始化Node<K,V>[] table,所以,在第一次put时,先初始化table哈希表
    //初始化哈希表
    1.7 调用inflateTable方法初始化哈希表transient Entry<K,V>[] table,将指定初始容量值调整为大于等于指定初始容量的2的幂次方的值之后,table = new Entry[capacity]
    1.8 调用resize方法初始化哈希表transient Node<K,V>[] table
    if ((p = tab[i = (n - 1) & hash]) == null)  //定位桶的位置,如果当前桶是空的,表明不冲突,直接新建Node并插入 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))))  // 先判断第一个结点(不论当前桶中是链表还是红黑树,第一个结点都单独判断)
                                                  如果第一个结点就和要插入的键值对的键值一样,将该节点记录在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) {
                // 遍历到链表尾部了,证明一直没有一样的键,所以,新建Node插入在链表尾部 p.next
    = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                  //如果当前链表的长度超过了阈值,将链表转化成红黑树
    treeifyBin(tab, hash); break; }
                // 在遍历链表的过程中,如果发现有一样的键,和上面一样,将该节点Node记录在e中,后面用新值覆盖旧值
    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } }
          // 此处,如果e != null,证明上面有和要插入的键值对一样的键,用新值覆盖旧值
    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; }

    7.扩容

    扩大的是Node<K,V>[] table的长度,扩充为原来的2倍,并且要重新计算键值对的位置,将其放在何时的桶中,扩容之后,桶的数量增加,但是冲突减少

    table数组的长度总是2的幂,原因:用位运算代替取余运算来定位桶的位置

    何时扩容?   HashMap中元素的个数 大于  阈值(容量*加载因子)

       final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;   //记录扩容之前的table的状态
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {  //之前的容量大于0,表明已经初始化了,此次调用resize方法是为了扩大哈希表的容量
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;  //已经没有可扩充的余地了,将阈值设为MAX_VALUE,直接返回原哈希表
                }
                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
          //oldCap == 0 表示还没初始化, 但是oldThr已经在构造器中设为 指定容量对应的2的幂次方的值
          //那么初始化之后的容量即为oldThr中保存的值 newCap = oldThr; else { // zero initial threshold signifies using defaults // oldCap == 0 并且 oldThr == 0 表明没有初始化并且调用构造器时没有指定容量,将它们都设为默认值
           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;  // 创建哈希表并赋给成员变量table

          // 将旧table中的键值对一一计算其hash值,放在新的哈希表的合适位置; 如果是初始化,oldTab是null,下面的操作不用进行
    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)  //如果桶中只有一个Node,那就直接计算其新的数组下标 newTab[e.hash & (newCap - 1)] = e;
                // 如果桶中有多个Node,如果是红黑树,要对红黑树进行查分;如果是链表,遍历链表
    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; }

    8.树化

    9.remove

    根据一个key删除对应的键值对,返回键值对对应的value

    先遍历HashMap,找到key对应的节点并用变量node记录下来

    如果node是null,表明没有对应的节点,直接返回null

    如果node不是nul,表明存在要删除的节点

      如果node是红黑树的一个节点,调用removeTreeNode方法删除

      如果node是链表的第一个结点,tab[index] = node.next;

          不是链表的第一个结点,p.next = node.next;

        public V remove(Object key) {
            Node<K,V> e;
            return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
        // 删除key对应的Node,返回对应的value值或者null }
    final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index;
        // 如果table是null,直接返回
    if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
          // 定位桶的位置 Node
    <K,V> node = null, e; K k; V v;
          // 先判断当前桶中的第一个结点是不是要删除的
    if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } }
          // 先遍历,如果要删除的节点存在,记录该结点
          // 然后删除对应的结点
    if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
  • 相关阅读:
    ASIHTTPREQUEST 文档
    本地通知
    ASIHttpRequest 使用过程中,中文编码的问题
    讲讲最近自己的学习,谈谈未来的想法
    关于 ASP.NET MVC 4 如果管理用户
    [转贴]超级懒汉编写的基于.NET的微信SDK
    [转贴]实践:C++平台迁移以及如何用C#做C++包装层
    [转贴]Linq之动态查询
    [转贴]watin的一些例子
    [转贴]xcode帮助文档
  • 原文地址:https://www.cnblogs.com/duanjiapingjy/p/9542921.html
Copyright © 2020-2023  润新知