zoukankan      html  css  js  c++  java
  • 浅析Java中HashMap的实现

    概述

    HashMap是一个散列表,是基于拉链法实现的。这个类继承了Map接口,Map接口提供了所有的哈希操作,比如set()、put()、remove()等,并且允许操作的键值对为null。HashMap跟Hashtable基本相同,区别是HashMap是非同步的并且允许键值对为null。HashMap不保证映射的顺序,特别是不保证该顺序恒久不变。

    在用到的哈希函数均匀性比较好的前提下,基本操作比如put和get的时间都是O(1)的。处理哈希冲突所需的时间与哈希表的容量(桶的数目)加上表的大小(存储的键值映射对)之和成正比。所以在初始化哈希表时,初始容量不要太大(装载因子不要太小)。

    影响HashMap性能的两个比较重要的参数是初始容量(initial capacity)和装载因子(load factor)。容量(capacity)是哈希表中桶的数量,初始容量就是哈希表创建时桶的数量。装载因子(load factor)是一个参数,它用来衡量哈希表满和空的程度,它在公式上等于size(键值对数量)/capacity(容量)。当装载因子大于某个值时,哈希表会进行重构(rehash),一般会将桶的数目加倍,哈希表的初始容量一般设为2的幂。

    负载因子一般设为0.75。这是时间费用和空间费用的比较好的权衡。负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。

    HashMap是非同步的,如果有多个线程并发的的访问,并且只是有个线程在结构上改变哈希表,必须增加额外的同步策略。这里所说的在结构上该表哈希表是指增加或删除一个或多个键值对,只是改变某个关键字所对应的值并不是指改变哈希表结构。

    HashMap的数据结构

    在Java编程语言中,最基本的结构就是两种,一个是数组,一个是模拟指针(引用),所有的数据结构都可以用这两个结构来构造,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。这是因为HashMap使用拉链法来解决哈希冲突的。

    从构造函数可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

    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 }
    View Code

    Entry就是数组中的元素,每个Map.Entry其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。同时,hash用来计算桶号,每一个桶就是一个链表。

    源码阅读

    HashMap类的源代码主要分如下及部分:

    1)HashMap自身的成员变量及方法;

    2)Entry的成员及方法。Entry是HashMap的子类,哈希表是一个Entry数组,数组的每一项代表一个桶;

    3)

    1.类成员变量

      

    /**
    *哈希表默认的初始容量 ,必须是2的幂.
    */
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    /**
    * 哈希表的最大容量。
    */
    static final int MAXIMUM_CAPACITY = 1 << 30;
    
    /**
    * 转载因子,此值用于无参数的构造函数。
    */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    /**
    *哈希表,需要时会调整大小,长度必须是2的幂.
    */
    transient Entry[] table;
    
    /**
    * 哈希表中的键值对.
    */
    transient int size;
    
    /**
    * 阈值,当哈希表键值对数目达到此值时,哈希表会调整大小,等于capacity * load factor.
    * @serial
    */
    int threshold;
    
    /**
    * 哈希表的转载因子.
    *
    * @serial
    */
    final float loadFactor;
    
    /**
    * 哈希表结构改变的次数
    */
    transient volatile int modCount;
    View Code

    如以上源码所示,定义了哈希表重要的参数。因为HashMap实现了Serilizable接口,对象可以序列化,被关键字transient声明的变量可以不被序列化,详情可以参考http://www.cnblogs.com/lanxuezaipiao/p/3369962.html。

    2构造函数

    HashMap有四个构造函数,

     1 /**
     2      * Constructs an empty <tt>HashMap</tt> with the specified initial
     3      * capacity and load factor.
     4      *
     5      * @param  initialCapacity the initial capacity
     6      * @param  loadFactor      the load factor
     7      * @throws IllegalArgumentException if the initial capacity is negative
     8      *         or the load factor is nonpositive
     9      */
    10     public HashMap(int initialCapacity, float loadFactor) {
    11         if (initialCapacity < 0)
    12             throw new IllegalArgumentException("Illegal initial capacity: " +
    13                                                initialCapacity);
    14         if (initialCapacity > MAXIMUM_CAPACITY)
    15             initialCapacity = MAXIMUM_CAPACITY;
    16         if (loadFactor <= 0 || Float.isNaN(loadFactor))
    17             throw new IllegalArgumentException("Illegal load factor: " +
    18                                                loadFactor);
    19 
    20         // Find a power of 2 >= initialCapacity
    21         int capacity = 1;
    22         while (capacity < initialCapacity)
    23             capacity <<= 1;
    24 
    25         this.loadFactor = loadFactor;
    26         threshold = (int)(capacity * loadFactor);
    27         table = new Entry[capacity];
    28         init();
    29     }
    30 
    31     /**
    32      * Constructs an empty <tt>HashMap</tt> with the specified initial
    33      * capacity and the default load factor (0.75).
    34      *
    35      * @param  initialCapacity the initial capacity.
    36      * @throws IllegalArgumentException if the initial capacity is negative.
    37      */
    38     public HashMap(int initialCapacity) {
    39         this(initialCapacity, DEFAULT_LOAD_FACTOR);
    40     }
    41 
    42     /**
    43      * Constructs an empty <tt>HashMap</tt> with the default initial capacity
    44      * (16) and the default load factor (0.75).
    45      */
    46     public HashMap() {
    47         this.loadFactor = DEFAULT_LOAD_FACTOR;
    48         threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    49         table = new Entry[DEFAULT_INITIAL_CAPACITY];
    50         init();
    51     }
    52 
    53     /**
    54      * Constructs a new <tt>HashMap</tt> with the same mappings as the
    55      * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
    56      * default load factor (0.75) and an initial capacity sufficient to
    57      * hold the mappings in the specified <tt>Map</tt>.
    58      *
    59      * @param   m the map whose mappings are to be placed in this map
    60      * @throws  NullPointerException if the specified map is null
    61      */
    62     public HashMap(Map<? extends K, ? extends V> m) {
    63         this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
    64                       DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    65         putAllForCreate(m);
    66     }
    View Code

    构造函数的重要功能是初始化capacity、loadFactor、threshold和table等。值得注意的是,第四个构造函数是根据已有的某个HashMap创建新的HashMap.

    3基本哈希操作

    3.1求索引

     1 /**
     2      * Applies a supplemental hash function to a given hashCode, which
     3      * defends against poor quality hash functions.  This is critical
     4      * because HashMap uses power-of-two length hash tables, that
     5      * otherwise encounter collisions for hashCodes that do not differ
     6      * in lower bits. Note: Null keys always map to hash 0, thus index 0.
     7      */
     8     static int hash(int h) {
     9         // This function ensures that hashCodes that differ only by
    10         // constant multiples at each bit position have a bounded
    11         // number of collisions (approximately 8 at default load factor).
    12         h ^= (h >>> 20) ^ (h >>> 12);
    13         return h ^ (h >>> 7) ^ (h >>> 4);
    14     }
    15 
    16     /**
    17      * Returns index for hash code h.
    18      */
    19     static int indexFor(int h, int length) {
    20         return h & (length-1);
    21     }
    View Code

    这两个函数是求索引的,也就是键值对应该存放在哪个桶中。hash函数的输入是key的hashcode()。

     3.2存

     1 /**
     2      * Associates the specified value with the specified key in this map.
     3      * If the map previously contained a mapping for the key, the old
     4      * value is replaced.
     5      *
     6      * @param key key with which the specified value is to be associated
     7      * @param value value to be associated with the specified key
     8      * @return the previous value associated with <tt>key</tt>, or
     9      *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
    10      *         (A <tt>null</tt> return can also indicate that the map
    11      *         previously associated <tt>null</tt> with <tt>key</tt>.)
    12      */
    13     public V put(K key, V value) {
    14         if (key == null)
    15             return putForNullKey(value);
    16         int hash = hash(key.hashCode());
    17         int i = indexFor(hash, table.length);
    18         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    19             Object k;
    20             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    21                 V oldValue = e.value;
    22                 e.value = value;
    23                 e.recordAccess(this);
    24                 return oldValue;
    25             }
    26         }
    27 
    28         modCount++;
    29         addEntry(hash, key, value, i);
    30         return null;
    31     }
    View Code

    当我们向HashMap中put元素的时候,先根据key的hashcode作为hash函数的输入计算hash值,根据hash值得到这个元素在数组中的下标,也就是哈希表的桶号。如果数组该位置上已经存放有其他元素,那么将新加入的放在链表头。如果数组该位置上没有元素,就直接将该元素放到数组中的该位置上。

    分为三个情况:1)key已存在HashMap中;2)key不存在;3)key为null。对于第一种情况,定位到这个key,并将旧的value替换为新的value。对于第二种情况,调用addEntry函数,定义如下

    1  void addEntry(int hash, K key, V value, int bucketIndex) {
    2     Entry<K,V> e = table[bucketIndex];
    3         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    4         if (size++ >= threshold)
    5             resize(2 * table.length);
    6     }
    View Code

    addEntry函数调用了Entry的构造函数,如下

    1 Entry(int h, K k, V v, Entry<K,V> n) {
    2             value = v;
    3             next = n;
    4             key = k;
    5             hash = h;
    6         }
    View Code

    从代码中可以看到,向表中插入一个Entry时,用到的是前插法,也就是说,新的Entry放在了链表的头部。当系统决定存储HashMap中的key-value对时,完全没有考虑Entry中的value,仅仅只是根据key来计算并决定每个Entry的存储位置。第三种情况,key为null时,调用函数putForNullKey,函数如下:

     1  private V putForNullKey(V value) {
     2         for (Entry<K,V> e = table[0]; e != null; e = e.next) {
     3             if (e.key == null) {
     4                 V oldValue = e.value;
     5                 e.value = value;
     6                 e.recordAccess(this);
     7                 return oldValue;
     8             }
     9         }
    10         modCount++;
    11         addEntry(0, null, value, 0);
    12         return null;
    13     }
    View Code

    当key为null时,索引为0.

    1  static int hash(int h) {
    2         // This function ensures that hashCodes that differ only by
    3         // constant multiples at each bit position have a bounded
    4         // number of collisions (approximately 8 at default load factor).
    5         h ^= (h >>> 20) ^ (h >>> 12);
    6         return h ^ (h >>> 7) ^ (h >>> 4);
    7     }
    View Code

    hash(int h)方法根据key的hashcode重新计算一次散列,此算法加入了高位计算,防止当低位不变,高位变化时,造成hash冲突

    在HashMap中要找到某个元素时,需要根据key的hash值来求得对应数组中的位置。HashMap的数据结构是数组和链表的结合,我们当然希望这个HashMap里面的元素尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,当我们查询时不用遍历链表,这样就大大优化了查询效率。

    对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用hash(int h)方法所得到的hash值总是相同的,我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,在HashMap中是这样的:调用indexFor(int h,int length)方法来计算该对象应该保存在table数组的哪个索引处。

    1  static int indexFor(int h, int length) {
    2         return h & (length-1);
    3     }
    View Code

    这个方法非常巧妙,它通过h&(table.length-1)来得到该对象的索引,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。在HashMap构造函数中有如下代码:

    1  // Find a power of 2 >= initialCapacity
    2         int capacity = 1;
    3         while (capacity < initialCapacity)
    4             capacity <<= 1;
    View Code

    这段代码保证初始化时HashMap的容量总是2的n次方,即底层的长度总是2的n次方。当length是2的n次方时,h&(length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。

    当数组长度是2的n次方时,2n-1得到的二进制数的每位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。所以说,当数组长度为2的n次幂的时候,不同的key计算得到的index相同的几率较小。那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率就较高了。

    3.3取

    View Code

    根据key取对应的value,key可以是null,其索引是0

    View Code

    根据key取出其对应的Entry(键值对),key不存在时,返回null

    HashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象。HashMap底层采用一个Entry数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据hash算法来决定其在数组中的存储位置,再根据equals方法决定其在该数组上的链表中的存储位置,当需要读取一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

    3.4 Rehash

    当哈希表中的元素数目达到阈值时,哈希表就自动进行Rehash,capacity加倍。值得注意的是,当capacity等于MAXIMUM_CAPACITY的时候,不会调整哈希表大小,但是会把阈值调为Integer.MAX_VALUE。rehash是一个比较耗时的操作。Rahash对应的函数是resize,如下

     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 
     9         Entry[] newTable = new Entry[newCapacity];
    10         transfer(newTable);
    11         table = newTable;
    12         threshold = (int)(newCapacity * loadFactor);
    13     }
    View Code

    resize调用transfer函数,它的作用是将久表中的元素重新映射到新表中。transfer函数的代码如下:

     1  void transfer(Entry[] newTable) {
     2         Entry[] src = table;
     3         int newCapacity = newTable.length;
     4         for (int j = 0; j < src.length; j++) {
     5             Entry<K,V> e = src[j];
     6             if (e != null) {
     7                 src[j] = null;
     8                 do {
     9                     Entry<K,V> next = e.next;
    10                     int i = indexFor(e.hash, newCapacity);
    11                     e.next = newTable[i];
    12                     newTable[i] = e;
    13                     e = next;
    14                 } while (e != null);
    15             }
    16         }
    17     }
    View Code

    3.5删除

    根据key删除其对应的Entry是由函数remove实现的,remove函数调用removeEntryForKey实现,removeEntryForKey的代码如下:

     1  final Entry<K,V> removeEntryForKey(Object key) {
     2         int hash = (key == null) ? 0 : hash(key.hashCode());
     3         int i = indexFor(hash, table.length);
     4         Entry<K,V> prev = table[i];
     5         Entry<K,V> e = prev;
     6 
     7         while (e != null) {
     8             Entry<K,V> next = e.next;
     9             Object k;
    10             if (e.hash == hash &&
    11                 ((k = e.key) == key || (key != null && key.equals(k)))) {
    12                 modCount++;
    13                 size--;
    14                 if (prev == e)
    15                     table[i] = next;
    16                 else
    17                     prev.next = next;
    18                 e.recordRemoval(this);
    19                 return e;
    20             }
    21             prev = e;
    22             e = next;
    23         }
    24 
    25         return e;
    26     }
    View Code

    删除并返回key所对应的Entry,如果哈希表中不存在参数key对于的键值对,则返回null。

    clear函数将哈希表置空,如下

     1  /**
     2      * Removes all of the mappings from this map.
     3      * The map will be empty after this call returns.
     4      */
     5     public void clear() {
     6         modCount++;
     7         Entry[] tab = table;
     8         for (int i = 0; i < tab.length; i++)
     9             tab[i] = null;
    10         size = 0;
    11     }
    View Code

    可以看出,函数遍历Entry数组,并将数组的每一项置为null。

    3.6存在性

    判断某个value是否存储在哈希表中,函数如下

     1 /**
     2      * Returns <tt>true</tt> if this map maps one or more keys to the
     3      * specified value.
     4      *
     5      * @param value value whose presence in this map is to be tested
     6      * @return <tt>true</tt> if this map maps one or more keys to the
     7      *         specified value
     8      */
     9     public boolean containsValue(Object value) {
    10     if (value == null)
    11             return containsNullValue();
    12 
    13     Entry[] tab = table;
    14         for (int i = 0; i < tab.length ; i++)
    15             for (Entry e = tab[i] ; e != null ; e = e.next)
    16                 if (value.equals(e.value))
    17                     return true;
    18     return false;
    19     }
    View Code

    没有巧妙的方法,就是遍历哈希表。value也可以取值为null,用专门的函数containsNullValue,代码如下:

     1  /**
     2      * Special-case code for containsValue with null argument
     3      */
     4     private boolean containsNullValue() {
     5     Entry[] tab = table;
     6         for (int i = 0; i < tab.length ; i++)
     7             for (Entry e = tab[i] ; e != null ; e = e.next)
     8                 if (e.value == null)
     9                     return true;
    10     return false;
    11     }
    View Code

    也是遍历哈希表。

    4.Entry类

    键值对存储在一个Entry中,HashMap的存储结构就是一个Entry数组,索引是数组的下标。数组的每一项对应一个桶,每个桶是一个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 
    17         public final K getKey() {
    18             return key;
    19         }
    20 
    21         public final V getValue() {
    22             return value;
    23         }
    24 
    25         public final V setValue(V newValue) {
    26         V oldValue = value;
    27             value = newValue;
    28             return oldValue;
    29         }
    30 
    31         public final boolean equals(Object o) {
    32             if (!(o instanceof Map.Entry))
    33                 return false;
    34             Map.Entry e = (Map.Entry)o;
    35             Object k1 = getKey();
    36             Object k2 = e.getKey();
    37             if (k1 == k2 || (k1 != null && k1.equals(k2))) {
    38                 Object v1 = getValue();
    39                 Object v2 = e.getValue();
    40                 if (v1 == v2 || (v1 != null && v1.equals(v2)))
    41                     return true;
    42             }
    43             return false;
    44         }
    45 
    46         public final int hashCode() {
    47             return (key==null   ? 0 : key.hashCode()) ^
    48                    (value==null ? 0 : value.hashCode());
    49         }
    50 
    51         public final String toString() {
    52             return getKey() + "=" + getValue();
    53         }
    54 
    55         /**
    56          * This method is invoked whenever the value in an entry is
    57          * overwritten by an invocation of put(k,v) for a key k that's already
    58          * in the HashMap.
    59          */
    60         void recordAccess(HashMap<K,V> m) {
    61         }
    62 
    63         /**
    64          * This method is invoked whenever the entry is
    65          * removed from the table.
    66          */
    67         void recordRemoval(HashMap<K,V> m) {
    68         }
    69     }
    View Code

    参考

    http://www.cnblogs.com/skywang12345/p/3310835.html

    http://www.codeceo.com/article/java-hashmap-learn.html

  • 相关阅读:
    解决关于 在android studio 出现的 DELETE_FAILED_INTERNAL_ERROR Error while Installing APK 问题
    oracle 时间日期常用语句及函数
    微信小程序 网络请求之re.request 和那些坑
    微信小程序 网络请求之设置合法域名
    开发中常用js记录(三)
    oracle 锁表 and 解锁
    微信小程序 JS动态修改样式
    微信小程序 获得用户输入内容
    微信小程序 引用其他js里的方法
    微信JSAPI支付回调
  • 原文地址:https://www.cnblogs.com/qiaoshanzi/p/4813359.html
Copyright © 2011-2022 走看看