zoukankan      html  css  js  c++  java
  • 学习笔记深入理解Java中的HashMap数据结构

    一:定义

      HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作,其实AbstractMap类已经实现了Map。

     public class HashMap<K,V> extends AbstractMap<K,V> 2 implements Map<K,V>, Cloneable, Serializable  

    二:构造函数和数据结构

      HashMap提供了三个构造函数:

    1 HashMap() 
    2 
    3 HashMap(int initialCapacity)
    4  
    5 HashMap(int initialCapacity, float loadFactor)

    第一个:构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。

    第二个:构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。

    第三个:构造一个带指定初始容量和加载因子的空 HashMap。

    那么这两个参数有什么含义呢?在HashMap中有什么作用?我们先来看一下HashMap的数据结构深入理解之后在回过头来看。我们看每次调用map.put方法是,其实我们是往Entry<K,V>[] tab 这个数组里面添加元素的。那么Entry这个又是一个什么呢?

     1  static class Entry<K,V> implements Map.Entry<K,V> {
     2         final K key;
     3         V value;
     4         Entry<K,V> next;
     5         final int hash;
     6 
     7         /**
     8          * Creates new entry.
     9          */
    10         Entry(int h, K k, V v, Entry<K,V> n) {
    11             value = v;
    12             next = n;
    13             key = k;
    14             hash = h;
    15         }
    16 }

    很显然,这其实是一个链表的数据结构。所以我们分析一下,那么HashMap的数据结构是不是就是张的这个样子的呀?  

    其实呢,我们在开始的构造函数里面的initialCapacity这个参数,就是指的这个数组的长度了,我们在看一下具有两个参数的那个构造方法。

     1 public HashMap(int initialCapacity, float loadFactor) {
     2         //初始容量不能<0
     3         if (initialCapacity < 0)
     4             throw new IllegalArgumentException("Illegal initial capacity: "
     5                     + initialCapacity);
     6         //初始容量不能 > 最大容量值,HashMap的最大容量值为2^30
     7         if (initialCapacity > MAXIMUM_CAPACITY)
     8             initialCapacity = MAXIMUM_CAPACITY;
     9         //负载因子不能 < 0
    10         if (loadFactor <= 0 || Float.isNaN(loadFactor))
    11             throw new IllegalArgumentException("Illegal load factor: "
    12                     + loadFactor);
    13 
    14         // 计算出大于 initialCapacity 的最小的 2 的 n 次方值。
    15         int capacity = 1;
    16         while (capacity < initialCapacity)
    17             capacity <<= 1;
    18         
    19         this.loadFactor = loadFactor;
    20         //设置HashMap的容量极限,当HashMap的容量达到该极限时就会进行扩容操作
    21         threshold = (int) (capacity * loadFactor);
    22         //初始化table数组
    23         table = new Entry[capacity];
    24         init();
    25     }

    每次新建一个HashMap时,都会初始化一个table数组。table数组的元素为Entry节点。

    三,添加数据

      

     1 public V put(K key, V value) {
     2         //当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因
     3         if (key == null)
     4             return putForNullKey(value);
     5         //计算key的hash值
     6         int hash = hash(key.hashCode());                  ------(1)
     7         //计算key hash 值在 table 数组中的位置
     8         int i = indexFor(hash, table.length);             ------(2)
     9         //从i出开始迭代 e,找到 key 保存的位置
    10         for (Entry<K, V> e = table[i]; e != null; e = e.next) {
    11             Object k;
    12             //判断该条链上是否有hash值相同的(key相同)
    13             //若存在相同,则直接覆盖value,返回旧value
    14             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    15                 V oldValue = e.value;    //旧值 = 新值
    16                 e.value = value;
    17                 e.recordAccess(this);
    18                 return oldValue;     //返回旧值
    19             }
    20         }
    21         //修改次数增加1
    22         modCount++;
    23         //将key、value添加至i位置处
    24         addEntry(hash, key, value, i);
    25         return null;
    26     }

    重点来了,我们看一下上面代码第6,8行,一个是通过key计算hash值,一个是通过hash值定位到数组中的位置。

    1 static int hash(int h) {
    2         h ^= (h >>> 20) ^ (h >>> 12);
    3         return h ^ (h >>> 7) ^ (h >>> 4);
    4     }
    5     
    6 static int indexFor(int h, int length) {
    7         return h & (length-1);
    8     }

    对于HashMap的数组而言,我们需要他里面的数据分布的尽量均匀,最好是每一项都有元素。因为分布的太紧张查询蛮,分布的太分散浪费空间。因为我们在构造函数中capacity <<= 1;这样做总是能够保证HashMap的底层数组长度为2的n次方。所以index的方法就相当于是对lenght取模处理。那么我们为什么要取模处理呢?因为length是2的N次方,当length-1的时候,你会发现得到的结果值 进行地位&运算时候,值与原来的hash值相同,而进行高位运算时,其值等于其低位值。所以说当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。

    那么所以整体上来讲,HashMap的put方法就是:首先会计算key的hash值,然后根据hash值确认在table中存储的位置。若该位置没有元素,则直接插入。否则迭代该处元素链表并依此比较其key的hash值。如果两个hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的Entry的value覆盖原来节点的value。如果两个hash值相等但key值不等 ,则将该节点插入该链表的链头。

    这种解决hash冲突的方法叫做【链地址法】,还有其他的比如:再哈希法,开放定址法,建立一个公共溢出区。

    具体的实现过程见addEntry方法。

    1 void addEntry(int hash, K key, V value, int bucketIndex) {
    2         //获取bucketIndex处的Entry
    3         Entry<K, V> e = table[bucketIndex];
    4         //将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry 
    5         table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
    6         //若HashMap中元素的个数超过极限了,则容量扩大两倍
    7         if (size++ >= threshold)
    8             resize(2 * table.length);
    9     }

    首先,系统总是将新的Entry对象添加到bucketIndex处。如果bucketIndex处已经有了对象,那么新添加的Entry对象将指向原有的Entry对象,形成一条Entry链,但是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。

    然后是扩容,  随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度*加载因子。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。具体扩容的代码很简单,

     1 void resize(int newCapacity) {
     2         Entry[] oldTable = table;
     3         int oldCapacity = oldTable.length;
     4         if (oldCapacity == MAXIMUM_CAPACITY) {
     5             threshold = Integer.MAX_VALUE;
     6             return;
     7         }
     8         Entry[] newTable = new Entry[newCapacity];
     9         boolean oldAltHashing = useAltHashing;
    10         useAltHashing |= sun.misc.VM.isBooted() &&
    11                 (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    12         boolean rehash = oldAltHashing ^ useAltHashing;
    13         //transfer函数的调用
    14         transfer(newTable, rehash);  
    15         table = newTable;
    16         threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    17     } 
    18 
    19 void transfer(Entry[] newTable, boolean rehash) {
    20         int newCapacity = newTable.length;
    21         //这里才是问题出现的关键
    22         for (Entry<K,V> e : table) {
    23             //遍历旧的Entry数组的每个元素,
    24             while(null != e) {
    25                 //寻找到下一个节点..
    26                 Entry<K,V> next = e.next;  
    27                 if (rehash) {
    28                     e.hash = null == e.key ? 0 : hash(e.key);
    29                 }
    30                 //重新计算每个元素在数组中的位置
    31                 int i = indexFor(e.hash, newCapacity);  
    32                 e.next = newTable[i];  
    33                 newTable[i] = e;
    34                 e = next;
    35             }
    36         }

    四,读取数据

      通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。

     1 public V get(Object key) {
     2         // 若为null,调用getForNullKey方法返回相对应的value
     3         if (key == null)
     4             return getForNullKey();
     5         // 根据该 key 的 hashCode 值计算它的 hash 码  
     6         int hash = hash(key.hashCode());
     7         // 取出 table 数组中指定索引处的值
     8         for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
     9             Object k;
    10             //若搜索的key与查找的key相同,则返回相对应的value
    11             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
    12                 return e.value;
    13         }
    14         return null;
    15     }

    2018年5月2日更新:

    发现当时jdk1.7的版本和现在的版本中HashMap的实现变化还挺大的。所以这里在重新更新一下,把认识到的变化在这里再做一些补充下,避免后面大家看到了这个对大家产生误导。

    第一点:

    JDK1.8 之后的 HashMap 底层在解决哈希冲突的时候,就不单单是使用数组加上单链表的组合了,因为当处理如果 hash 值冲突较多的情况下,链表的长度就会越来越长,此时通过单链表来寻找对应 Key 对应的 Value 的时候就会使得时间复杂度达到 O(n),因此在 JDK1.8 之后,在链表新增节点导致链表长度超过 TREEIFY_THRESHOLD = 8 的时候,就会在添加元素的同时将原来的单链表转化为红黑树。我们知道红黑树是一种易于增删改查的二叉树,他对与数据的查询的时间复杂度是 O(logn) 级别,所以利用红黑树的特点就可以更高效的对 HashMap 中的元素进行操作。

    第二点:在hash方面,首先,在高位扰动方面,只是简单的h = h ^ (h >>> 16),没有再做那么多的扰动,就得到了hash值。其次,去掉了indexFor这个专门定位的函数,而是在put,get等操作中直接定位,可以看到这些函数中都有这两行。

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

    第三点:就是扩容机制,jdk1.7扩容是直接采用头插将老的数据遍历插入到新的table中。在jdk1.8新版本中,我们来看一下扩容机制。

     1  final Node<K,V>[] resize() {
     2         Node<K,V>[] oldTab = table;//首次初始化后table为Null
     3         int oldCap = (oldTab == null) ? 0 : oldTab.length;
     4         int oldThr = threshold;//默认构造器的情况下为0
     5         int newCap, newThr = 0;
     6         if (oldCap > 0) {//table扩容过
     7              //当前table容量大于最大值得时候返回当前table
     8              if (oldCap >= MAXIMUM_CAPACITY) {
     9                 threshold = Integer.MAX_VALUE;
    10                 return oldTab;
    11             }
    12             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    13                      oldCap >= DEFAULT_INITIAL_CAPACITY)
    14             //table的容量乘以2,threshold的值也乘以2           
    15             newThr = oldThr << 1; // double threshold
    16         }
    17         else if (oldThr > 0) // initial capacity was placed in threshold
    18         //使用带有初始容量的构造器时,table容量为初始化得到的threshold
    19         newCap = oldThr;
    20         else {  //默认构造器下进行扩容  
    21              // zero initial threshold signifies using defaults
    22             newCap = DEFAULT_INITIAL_CAPACITY;
    23             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    24         }
    25         if (newThr == 0) {
    26         //使用带有初始容量的构造器在此处进行扩容
    27             float ft = (float)newCap * loadFactor;
    28             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
    29                       (int)ft : Integer.MAX_VALUE);
    30         }
    31         threshold = newThr;
    32         @SuppressWarnings({"rawtypes","unchecked"})
    33         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    34         table = newTab;
    35         if (oldTab != null) {
    36             for (int j = 0; j < oldCap; ++j) {
    37                 HashMap.Node<K,V> e;
    38                 if ((e = oldTab[j]) != null) {
    39                     // help gc
    40                     oldTab[j] = null;
    41                     if (e.next == null)
    42                         // 当前index没有发生hash冲突,直接对2取模,即移位运算hash &(2^n -1)
    43                         // 扩容都是按照2的幂次方扩容,因此newCap = 2^n
    44                         newTab[e.hash & (newCap - 1)] = e;
    45                     else if (e instanceof HashMap.TreeNode)
    46                         // 当前index对应的节点为红黑树,这里篇幅比较长且需要了解其数据结构跟算法,因此不进行详解,当树的个数小于等于UNTREEIFY_THRESHOLD则转成链表
    47                         ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
    48                     else { // preserve order
    49                         // 把当前index对应的链表分成两个链表,减少扩容的迁移量
    50                         HashMap.Node<K,V> loHead = null, loTail = null;
    51                         HashMap.Node<K,V> hiHead = null, hiTail = null;
    52                         HashMap.Node<K,V> next;
    53                         do {
    54                             next = e.next;
    55                             if ((e.hash & oldCap) == 0) {
    56                                 // 扩容后不需要移动的链表
    57                                 if (loTail == null)
    58                                     loHead = e;
    59                                 else
    60                                     loTail.next = e;
    61                                 loTail = e;
    62                             }
    63                             else {
    64                                 // 扩容后需要移动的链表
    65                                 if (hiTail == null)
    66                                     hiHead = e;
    67                                 else
    68                                     hiTail.next = e;
    69                                 hiTail = e;
    70                             }
    71                         } while ((e = next) != null);
    72                         if (loTail != null) {
    73                             // help gc
    74                             loTail.next = null;
    75                             newTab[j] = loHead;
    76                         }
    77                         if (hiTail != null) {
    78                             // help gc
    79                             hiTail.next = null;
    80                             // 扩容长度为当前index位置+旧的容量
    81                             newTab[j + oldCap] = hiHead;
    82                         }
    83                     }
    84                 }
    85             }
    86         }
    87         return newTab;
    88     }

     

  • 相关阅读:
    This counter can increment, decrement or skip ahead by an arbitrary amount
    LUT4/MUXF5/MUXF6 logic : Multiplexer 8:1
    synthesisable VHDL for a fixed ratio frequency divider
    Bucket Brigade FIFO SRL16E ( VHDL )
    srl16e fifo verilog
    DualPort Block RAM with Two Write Ports and Bytewide Write Enable in ReadFirst Mode
    Parametrilayze based on SRL16 shift register FIFO
    stm32 spi sdcard fatfs
    SPI bus master for System09 (2)
    SQLSERVER中的自旋锁
  • 原文地址:https://www.cnblogs.com/yangkangIT/p/4632431.html
Copyright © 2011-2022 走看看