• HashMap源码分析


    HashMap源码分析

    0、绪论

    hashMap是一个很重要的数据结构首次出现在JDK1.2中 。简单的来说就是将数据的hash码按照相近的程度分别装入一些桶。每一个桶中的对象的hash值相似。这样可以按照需要查找的对象的hash值,来快速逼近所需要查询的对象的本体。

    1、初始容量

    初始容量要求是2的n次幂:即为16,32,64,128 etc

    源码中:1 >> 4 = 2^4 = 16 ,所以缺省的初始容量是16

    2、负载因子

    负载因子表示扩展的比例,已经存入的KV对的数量占capacity的最大比例。

    0.75 这个缺省的负载因子,在时间成本和空间成本上做到了最好的折衷。过大省空间但是浪费了时间,过小省时间但是浪费了空间。

    3、阈值

    阈值:threshold  = initcapacity * loadFactory

    阈值表示这个hashMap所能容纳的最大的KV对数。(一个K和一个对应的V叫做一个键)超过了这个数量,容量就需要翻倍,即 capaciry >> 1。

    为2的n次幂的原因是:对每一个桶进行重新排列的时候,hash码是二进制的形式,每一位上只有0 和 1 ,当容量增大的时候,对应的位数上为0 的保持在原来的位置 n 上,为1 的变到 n + oldLength 上

    1、原理

    构建一个数组,这个数组成为桶数组(bucket-array)。这个数组中存放的是一个链表的头节点或者是一个红黑树的根结点。

    属于桶数组的数据结构是链表还是红黑树,要取决于属于这个桶KV对的数量。

    如果在8个以下,那么这个数据结构一定会是一个链表。

    如果链表长度到达了8个,那么有两种情况

    1、桶数组的长度在64以下,那么就会进行扩容操作,将桶的数量翻倍。

    2、桶数组的长度到达了64,就会将这个链表变成一个红黑树。

    如果某个桶中装的红黑树,经过删除操作之后又回到了7个,就会降级,变成一个链表。

    以上的操作,解决了hash冲突。

    还有一种导致扩容的条件:

    那就是table中的 容量 * 负载因子 都填充了对象,那么就会导致扩容。这就是构造方法里面的初始设置的变量的意义。

    2、HashMap的构造方法

    HashMap 有四个构造方法:

    0、无参构造方法:

    1 public HashMap() {
    2 
    3     this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    4 
    5 }

    1、初始桶数量构造方法:

    1 public HashMap(int initialCapacity) {
    2 
    3     this(initialCapacity, DEFAULT_LOAD_FACTOR);
    4 
    5 }

    2、初始桶数量和负载因子构造方法:

     1 public HashMap(int initialCapacity, float loadFactor) {
     2 
     3     if (initialCapacity < 0)
     4 
     5         throw new IllegalArgumentException("Illegal initial capacity: " +
     6 
     7                                            initialCapacity);
     8 
     9     if (initialCapacity > MAXIMUM_CAPACITY)
    10 
    11         initialCapacity = MAXIMUM_CAPACITY;
    12 
    13     if (loadFactor <= 0 || Float.isNaN(loadFactor))
    14 
    15         throw new IllegalArgumentException("Illegal load factor: " +
    16 
    17                                            loadFactor);
    18 
    19     this.loadFactor = loadFactor;
    20 
    21     this.threshold = tableSizeFor(initialCapacity);
    22 
    23 }

    3、从map导入生成构造方法

    1 public HashMap(Map<? extends K, ? extends V> m) {
    2 
    3     this.loadFactor = DEFAULT_LOAD_FACTOR;
    4 
    5     putMapEntries(m, false);
    6 
    7 }

    方法0 :

    以无参的方式构造一个hashmap,使用缺省的桶的数量16,使用缺省的负载因子0.75。

    方法1:

    自行设置初始的桶的数量。并且这个值只能为2的n次方。

    方法2:

    自行设置初始的桶的数量,自行设置初始的桶负载因子(0~1.0f)。

    方法3:

    通过一个map对象,将这个map对象中的所有KV对全部倒入这个hashmap中,这个hashMap使用缺省的初始桶数量和缺省的负载因子。

     

    3、HashMap的查找

    使用get方法和getNode方法实现查找。

    1、get方法。

    1 public V get(Object key) {
    2 
    3     Node<K,V> e;
    4 
    5     return (e = getNode(hash(key), key)) == null ? null : e.value;
    6 
    7 }

    在get方法里面包含着一个getNode方法,get方法返回的是getNode方法返回值的一个V。

    2、getNode方法。

     1 final Node<K,V> getNode(int hash, Object key) {
     2 
     3     Node<K,V>[] tab; 
     4 
     5    Node<K,V> first, e; 
     6 
     7    int n; 
     8 
     9    K k;      
    10 
    11     if ((tab = table) != null && (n = tab.length) > 0 &&
    12 
    13         (first = tab[(n - 1) & hash]) != null) {
    14 
    15  
    16 
    17         if (first.hash == hash && // always check first node
    18 
    19             ((k = first.key) == key || (key != null && key.equals(k))))
    20 
    21             return first;
    22 
    23  
    24 
    25         if ((e = first.next) != null) {
    26 
    27             if (first instanceof TreeNode)
    28 
    29                 return ((TreeNode<K,V>)first).getTreeNode(hash, key);
    30 
    31             do {
    32 
    33                 if (e.hash == hash &&
    34 
    35                     ((k = e.key) == key || (key != null && key.equals(k))))
    36 
    37                     return e;
    38 
    39             } while ((e = e.next) != null);
    40 
    41         }
    42 
    43  
    44 
    45  
    46 
    47     }
    48 
    49     return null;
    50 
    51 }

    getNode方法是hashmap主要的查找方法。

    1、桶数组table(tab)不能为空,桶的长度(n)不能为0,key对应的桶(first)不能为空。

    2、如果桶的hash和传入的hash相同,并且key对应的桶的key和传入的key时第一个key是同一个key,那么这个时候,桶中的root或者head节点就是我们需哦需要的结果,返回它即可。

    3、在第二个不成立的基础上,那么就进入循环。

    4、在循环的第一次,判断是不是一个二叉红黑树,如果是,那么就直接调用树的查询方法,并返回这个结果。

    5、如果不是一个树,那么就要通过一个循环来完成这个,查询的结果。

    6、在链表中判断的结果和第二条的要求一致。

    3、HashMap的遍历

    有两种便利方法:

    1、K (key) 遍历

    可以使用foreach方法:

    1 for (Object key : map.keySet()) {
    2 
    3     System.out.println((String) key);
    4 
    5 }

    map.keySet的作用是返回一个map的Set对象,这样的情况,对hashMap的遍历,就转化成了对这个Set对象的遍历

    中keySet的源码:

     1 public Set<K> keySet() {
     2 
     3     Set<K> ks = keySet;
     4 
     5     if (ks == null) {
     6 
     7         ks = new KeySet();
     8 
     9         keySet = ks;
    10 
    11     }
    12 
    13     return ks;
    14 
    15 }

    1、如果不为空,直接的返回内部用来存放key的容器对象 :keySet(来自于抽象类AbstractMap)。

    2、如果为空,new 一个空keySet,将其返回。

    2、Entry 遍历

    1 for (HashMap.Entry entry : map.entrySet()) {
    2 
    3     System.out.print(entry.getKey()+" -> "+entry.getValue() + " 
    ");
    4 
    5 }

    1、使用了EntrySet方法,返回一个用来存放Entry的容器EntrySet(来自于HashMap)。

    2、Entry是和K、V操作相关的一个接口。

    #、K、V 存放在Entry中

    #、Entry的集合是EntrySet

    3、获取entrySet,对其遍历,可以获得所有的Entry。

    4、提取每一个Entry的K、V,可以获得所有的键对。

    其中entrySet的源码:

    1 public Set<Map.Entry<K,V>> entrySet() {
    2 
    3     Set<Map.Entry<K,V>> es;
    4 
    5     return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    6 
    7 }

    1、如果不为空,直接的返回内部key和value的容器 :EntrySet。

    2、为空,new 一个空EntrySet,将其返回。

    4、HashMap的添加

    1、添加方法

    1 public V put(K key, V value) {
    2 
    3 //这里的hash方法在这段代码的最后,不同于一般的hashcode。
    4 
    5     return putVal(hash(key), key, value, false, true);
    6 
    7 }
      1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
      2 
      3     Node<K,V>[] tab; 
      4 
      5   Node<K,V> p; //p是桶中的第一个KV对
      6 
      7   int n, i;  //n是table的长度,i是桶数组的index。
      8 
      9  
     10 
     11   //如果在hashmap中table是空的,就创建一个。
     12 
     13     if ((tab = table) == null || (n = tab.length) == 0)
     14 
     15         n = (tab = resize()).length;
     16 
     17  
     18 
     19   //如果这个桶是空的,那么在这个桶中创建第一个节点。
     20 
     21     if ((p = tab[i = (n - 1) & hash]) == null)
     22 
     23         tab[i] = newNode(hash, key, value, null);
     24 
     25  
     26 
     27     else {
     28 
     29   //要插入的KV对
     30 
     31         Node<K,V> e; 
     32 
     33       K k;
     34 
     35  
     36 
     37   //如果桶中的第一个KV对的key的hash和要插入的hash相同,那么就把它替代掉
     38 
     39         if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
     40 
     41             e = p;
     42 
     43  
     44 
     45         //如果桶中的第一个节点下面挂的是一棵树
     46 
     47         else if (p instanceof TreeNode)
     48 
     49             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
     50 
     51  
     52 
     53       //如果桶中的第一个节点装的是一个链表
     54 
     55         else {
     56 
     57         //查出尾节点,然后挂上节点就ok了
     58 
     59             for (int binCount = 0; ; ++binCount) {
     60 
     61                 if ((e = p.next) == null) {
     62 
     63                     p.next = newNode(hash, key, value, null);
     64 
     65                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
     66 
     67                         treeifyBin(tab, hash);
     68 
     69                     break;
     70 
     71                 }
     72 
     73                 if (e.hash == hash &&
     74 
     75                     ((k = e.key) == key || (key != null && key.equals(k))))
     76 
     77                     break;
     78 
     79                 p = e;
     80 
     81             }
     82 
     83         }
     84 
     85  
     86 
     87       //复符合了上面的一个,然后返回V
     88 
     89         if (e != null) { // existing mapping for key
     90 
     91             V oldValue = e.value;
     92 
     93             if (!onlyIfAbsent || oldValue == null)
     94 
     95                 e.value = value;
     96 
     97             afterNodeAccess(e);
     98 
     99             return oldValue;
    100 
    101         }
    102 
    103     }
    104 
    105     ++modCount;
    106 
    107     if (++size > threshold)
    108 
    109         resize();
    110 
    111     afterNodeInsertion(evict);
    112 
    113     return null;
    114 
    115 }

    1、添加方法实际上是在内部调用了一个更加详细的添加方法,真正的添加的操作都是在这了详细的操作里面完成的。

    2、并且这里所使用的hash方法不是直接的hashCode。

    2、hash()方法

    1 static final int hash(Object key) {
    2 
    3     int h;
    4 
    5     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    6 
    7 }

    1、>>> :忽略符号位右移

    2、<<< :忽略符号位左移

    3、A^B :以二进制的形式按位取反

    4、这样重新计算一次hash的值,增加了它的复杂度。让它们(KV键对)可以更好的在桶数组上均匀分布。

    5、HashMap的resize(扩容)

    resize方法在权衡之后,判断是否需要对node的数组table的长度翻倍处理。如果需要,将原来的元素分配到新的数组里。table的长度必须为2的幂。(16、32、64、128、256 etc.)

      1 final Node<K,V>[] resize() {
      2 
      3     Node<K,V>[] oldTab = table;
      4 
      5     int oldCap = (oldTab == null) ? 0 : oldTab.length;
      6 
      7     int oldThr = threshold;
      8 
      9     int newCap, newThr = 0;
     10 
     11  
     12 
     13   //说明初始化过了,将新的容量赋给newCap
     14 
     15     if (oldCap > 0) {
     16 
     17         if (oldCap >= MAXIMUM_CAPACITY) {
     18 
     19             threshold = Integer.MAX_VALUE;
     20 
     21             return oldTab;
     22 
     23         }
     24 
     25     //容量翻倍
     26 
     27         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
     28 
     29                  oldCap >= DEFAULT_INITIAL_CAPACITY)
     30 
     31             newThr = oldThr << 1; // 阈值翻倍
     32 
     33     }
     34 
     35   //下面的2个条件分支都是说明未被初始化
     36 
     37     else if (oldThr > 0) // initial capacity was placed in threshold
     38 
     39         newCap = oldThr;
     40 
     41     else {               // zero initial threshold signifies using defaults
     42 
     43         newCap = DEFAULT_INITIAL_CAPACITY;
     44 
     45         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
     46 
     47     }
     48 
     49  
     50 
     51   //在上面的选项里面执行的是未初始化过的那第二个条件分支,那么这个判断就会为真,然后将newThr赋值
     52 
     53     if (newThr == 0) {
     54 
     55         float ft = (float)newCap * loadFactor;
     56 
     57         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
     58 
     59                   (int)ft : Integer.MAX_VALUE);
     60 
     61     }
     62 
     63  
     64 
     65     threshold = newThr;
     66 
     67  
     68 
     69     @SuppressWarnings({"rawtypes","unchecked"})
     70 
     71     Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
     72 
     73     table = newTab;
     74 
     75  
     76 
     77   //利用新的容量和阈值,构建新的table,并将oldTable转移到新的Table中
     78 
     79     if (oldTab != null) {
     80 
     81   //遍历oldTable
     82 
     83         for (int j = 0; j < oldCap; ++j) {
     84 
     85       //oldTable中的每一个桶的第一个节点
     86 
     87             Node<K,V> e;
     88 
     89             if ((e = oldTab[j]) != null) {
     90 
     91                 oldTab[j] = null;
     92 
     93           //单节点
     94 
     95                 if (e.next == null)
     96 
     97                     newTab[e.hash & (newCap - 1)] = e;
     98 
     99           //下挂树
    100 
    101                 else if (e instanceof TreeNode)
    102 
    103                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
    104 
    105           //下挂链表
    106 
    107                 else { // preserve order
    108 
    109                     Node<K,V> loHead = null, loTail = null;
    110 
    111                     Node<K,V> hiHead = null, hiTail = null ;
    112 
    113                     Node<K,V> next;
    114 
    115                     do {
    116 
    117                         next = e.next;
    118 
    119                         if ((e.hash & oldCap) == 0) {
    120 
    121                             if (loTail == null)
    122 
    123                                 loHead = e;
    124 
    125                             else
    126 
    127                                 loTail.next = e;
    128 
    129                             loTail = e;
    130 
    131                         }
    132 
    133                         else {
    134 
    135                             if (hiTail == null)
    136 
    137                                 hiHead = e;
    138 
    139                             else
    140 
    141                                 hiTail.next = e;
    142 
    143                             hiTail = e;
    144 
    145                         }
    146 
    147                     } while ((e = next) != null);
    148 
    149                     if (loTail != null) {
    150 
    151                         loTail.next = null;
    152 
    153                         newTab[j] = loHead;
    154 
    155                     }
    156 
    157                     if (hiTail != null) {
    158 
    159                         hiTail.next = null;
    160 
    161                         newTab[j + oldCap] = hiHead;
    162 
    163                     }
    164 
    165                 }
    166 
    167             }
    168 
    169         }
    170 
    171     }
    172 
    173     return newTab;
    174 
    175 }

    6、链表的树化和树的链化

    1、树化的几个阈值

     1 /**
     2 
     3  * 树化必须要达到的节点数
     4 
     5  */
     6 
     7 static final int TREEIFY_THRESHOLD = 8;
     8 
     9  
    10 
    11 /**
    12 
    13  * 取消树化的节点数
    14 
    15  */
    16 
    17 static final int UNTREEIFY_THRESHOLD = 6;
    18 
    19  
    20 
    21 /**
    22 
    23  * 树化的最小table长度。(桶数量)
    24 
    25    低于这个数量说先进行table扩容
    26 
    27  */
    28 
    29 static final int MIN_TREEIFY_CAPACITY = 64;

    这几个常量规定了hashMap中用来存储节点的table数组中存储的节点形态变化的阈值。

    具体可以为以下几点:

    1、树化的条件有两点:

    (1)这个桶中存储的节点数量已经达到了8个

    (2)桶的数量(table的长度)已经到达了64

    满足这个两个条件,桶中的链表就会变成一个红黑树。

    2、树化之后,当树的的节点已经不足6个了,那么就会取消树化返回链表的形态。

    树是一个红黑树。

    7、HashMap的删除

     1 /**
     2 
     3  * 删除这个KV对,如果K在hashMap中不存在,或输入的KV不对应,就返回false。
     4 
     5  * 如果KV对应,那么删除他们,返回true;如果不对应,返回false,其他的什么都不做。
     6 
     7  * 如果存在,删除这个KV对,并且返回true。
     8 
     9  */
    10 
    11  
    12 
    13 public boolean remove(Object key, Object value) {
    14 
    15     return removeNode(hash(key), key, value, true, true) != null;
    16 
    17 }
    18 
    19  
    20 
    21  
    22 
    23 /**
    24 
    25  * 删除这个K对应的V的值,并且返回这个V值,如果这个K不存在,那么返回null。
    26 
    27  * 这个方法和pop有些类似,删除了之后,返回一个这个对应的V值。
    28 
    29  */
    30 
    31 public V remove(Object key) {
    32 
    33     Node<K,V> e;
    34 
    35     return ( e = removeNode( hash(key), key, null, false, true) ) == null ? null : e.value;
    36 
    37 }
    38 
    39  
    40 
    41 /**
    42 
    43 * 方法为final,不可被重写。
    44 
    45 * 上面的两个删除的方法都是调用这个方法实现删除的。
    46 
    47 *
    48 
    49 * @param hash key的hash值,该值是通过hash(key)获取到的
    50 
    51 * @param key 要删除的键值对的key
    52 
    53 * @param value 输入的V是否有效取决于 matchValue 是否为真
    54 
    55 * @param matchValue 是否要求输入的KV对应,如果该值为真,但是KV不对应,就不删除。如果为假,则不关心输入的V的值
    56 
    57 * @param movable 删除后是否移动节点,如果为false,则不移动
    58 
    59 * @return 返回被删除的节点对象,如果没有删除任何节点则返回null
    60 
    61 */
      1 final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
      2 
      3     Node<K,V>[] tab; //tab : table
      4 
      5 Node<K,V> p; //p: 对应的桶中的第一个节点
      6 
      7 int n, index; //n:桶数组长度;index:对应的桶的index
      8 
      9  
     10 
     11 //桶数组不为空、对应的桶也不为空。
     12 
     13     if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
     14 
     15         Node<K,V> node = null, e;
     16 
     17   K k; 
     18 
     19   V v;
     20 
     21  
     22 
     23       //刚好是桶中的第一个节点,将 p 赋给 node
     24 
     25         if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
     26 
     27             node = p;
     28 
     29     //如果没有有那么幸运,对下挂的点检测。
     30 
     31         else if ((e = p.next) != null) {
     32 
     33        //如果是树,调用树的搜索方式,找到这个KV对。
     34 
     35             if (p instanceof TreeNode)
     36 
     37                 node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
     38 
     39        //如果是一个链表,那么使用链表的查找方式,找到这个节点KV对,然后赋给node。
     40 
     41             else {
     42 
     43                 do {
     44 
     45                     if (e.hash == hash &&
     46 
     47                         ((k = e.key) == key ||
     48 
     49                          (key != null && key.equals(k)))) {
     50 
     51                         node = e;
     52 
     53                         break;
     54 
     55                     }
     56 
     57                     p = e;
     58 
     59                 } while ((e = e.next) != null);
     60 
     61             }
     62 
     63  
     64 
     65         }
     66 
     67  
     68 
     69     //如果在上面的步骤里面找到了node,并在逻辑上使用上 V 和 matchValue 判断找到的V是否有效。
     70 
     71         if (node != null && (!matchValue || (v = node.value) == value ||
     72 
     73                                          (value != null && value.equals(v)))) {
     74 
     75             if (node instanceof TreeNode)
     76 
     77                 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
     78 
     79             else if (node == p)
     80 
     81                 tab[index] = node.next;
     82 
     83             else
     84 
     85                 p.next = node.next;
     86 
     87             ++modCount;
     88 
     89             --size;
     90 
     91             afterNodeRemoval(node);
     92 
     93             return node;
     94 
     95         }
     96 
     97     }
     98 
     99     //没有找到node,下挂点为空或者是table为空,都会返回 null
    100 
    101     return null;
    102 
    103 }
  • 相关阅读:
    net下开发COM+组件(一)
    C#中自定义属性的例子
    textBox的readonly=true
    关于ADO.Net的数据库连接池
    CYQ.Data 轻量数据层之路 使用篇三曲 MAction 取值赋值(十四)
    CYQ.Data 轻量数据层之路 SQLHelper 回头太难(八)
    CYQ.Data 轻量数据层之路 MDataTable 绑定性能优化之章(十一)
    C# 浅拷贝与深拷贝区别 解惑篇
    C#中的 ref 传进出的到底是什么 解惑篇
    CYQ.Data 轻量数据层之路 使用篇五曲 MProc 存储过程与SQL(十六)
  • 原文地址:https://www.cnblogs.com/zjxu97/p/10049493.html
Copyright © 2020-2023  润新知