JDK源码学习系列08----HashMap

来源:互联网 发布:淘宝自然流量提升 编辑:程序博客网 时间:2024/06/05 20:02

                                                          JDK源码学习系列08----HashMap

1.HashMap简介

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的

<span style="font-size:10px;">public class HashMap<K,V>    extends AbstractMap<K,V>    implements Map<K,V>, Cloneable, Serializable</span>

Map接口定义了所有Map子类必须实现的方法。Map接口中还定义了一个内部接口Entry(为什么要弄成内部接口?改天还要学习学习)。Entry将在后面有详细的介绍。

    AbstractMap也实现了Map接口,并且提供了两个实现Entry的内部类:SimpleEntry和SimpleImmutableEntry。

2.HashMap的数据结构

Java最基本的数据结构有数组和链表。数组的特点是空间连续(大小固定)、寻址迅速,但是插入和删除时需要移动元素,所以查询快,增加删除慢。链表恰好相反,可动态增加或减少空间以适应新增和删除元素,但查找时只能顺着一个个节点查找,所以增加删除快,查找慢。有没有一种结构综合了数组和链表的优点呢?当然有,那就是哈希表(虽说是综合优点,但实际上查找肯定没有数组快,插入删除没有链表快,一种折中的方式吧)。一般采用拉链法实现哈希表。


ps:图片来源于网络

3.HashMap成员变量

HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。

    /**     * 默认的初始容量,必须是2的幂。     */    static final int DEFAULT_INITIAL_CAPACITY = 16;    /**     * 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)     */    static final int MAXIMUM_CAPACITY = 1 << 30;    /**     * 默认装载因子      */    static final float DEFAULT_LOAD_FACTOR = 0.75f;    /**     * 存储数据的Entry数组,长度是2的幂。      */    transient Entry[] table;    /**     * map中保存的键值对的数量     */    transient int size;    /**     * 需要调整大小的极限值(容量*装载因子)     */    int threshold;    /**     *装载因子     */    final float loadFactor;    /**     * map结构被改变的次数     */    transient volatile int modCount;
HashMap是通过"拉链法"实现的哈希表。

它包括几个重要的成员变量:tablesizethresholdloadFactormodCount
  table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。 
  size是HashMap的大小,它是HashMap保存的键值对的数量。 
  threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值="容量*加载因子",当HashMap中                    存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
  loadFactor就是加载因子。 
  modCount是用来实现fail-fast机制的。

4.HashMap构造函数

/**     *使用默认的容量及装载因子构造一个空的HashMap     */    public HashMap() {        this.loadFactor = DEFAULT_LOAD_FACTOR;        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//计算下次需要调整大小的极限值        table = new Entry[DEFAULT_INITIAL_CAPACITY];//根据默认容量(16)初始化table        init();    }/**     * 根据给定的初始容量的装载因子创建一个空的HashMap     * 初始容量小于0或装载因子小于等于0将报异常      */    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);        int capacity = 1;        //设置capacity为大于initialCapacity且是2的幂的最小值        while (capacity < initialCapacity)            capacity <<= 1;        this.loadFactor = loadFactor;        threshold = (int)(capacity * loadFactor);        table = new Entry[capacity];        init();    }/**     *根据指定容量创建一个空的HashMap     */    public HashMap(int initialCapacity) {        this(initialCapacity, DEFAULT_LOAD_FACTOR);//调用上面的构造方法,容量为指定的容量,装载因子是默认值    }/**     *通过传入的map创建一个HashMap,容量为默认容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的较大者,装载因子为默认值     */    public HashMap(Map<? extends K, ? extends V> m) {        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);        putAllForCreate(m);    }

5.HashMap的内部类Entry<K,V>

HashMap底层是用一个Entry<k,v>数组实现的,每个Entry对象的内部又含有指向下一个Entry类型对象的引用。

static class Entry<K,V> implements Map.Entry<K,V> {        final K key;        V value;        Entry<K,V> next;//对下一个节点的引用(看到链表的内容,结合定义的Entry数组,是不是想到了哈希表的拉链法实现?!)        final int hash;//哈希值        Entry(int h, K k, V v, Entry<K,V> n) {            value = v;            next = n;            key = k;            hash = h;        }        public final K getKey() {            return key;        }        public final V getValue() {            return value;        }        public final V setValue(V newValue) {        V oldValue = value;            value = newValue;            return oldValue;//返回的是之前的Value        }        public final boolean equals(Object o) {            if (!(o instanceof Map.Entry))//先判断类型是否一致                return false;            Map.Entry e = (Map.Entry)o;            Object k1 = getKey();            Object k2 = e.getKey();// Key相等且Value相等则两个Entry相等            if (k1 == k2 || (k1 != null && k1.equals(k2))) {                Object v1 = getValue();                Object v2 = e.getValue();                if (v1 == v2 || (v1 != null && v1.equals(v2)))                    return true;            }            return false;        }        // hashCode是Key的hashCode和Value的hashCode的异或的结果        public final int hashCode() {            return (key==null   ? 0 : key.hashCode()) ^                   (value==null ? 0 : value.hashCode());        }        // 重写toString方法,是输出更清晰        public final String toString() {            return getKey() + "=" + getValue();        }        /**         *当调用put(k,v)方法存入键值对时,如果k已经存在,则该方法被调用(为什么没有内容?)         */        void recordAccess(HashMap<K,V> m) {        }        /**         * 当Entry被从HashMap中移除时被调用(为什么没有内容?)         */        void recordRemoval(HashMap<K,V> m) {        }    }
其中,Map接口:

K getKey();//获取Key V getValue();//获取ValueV setValue();//设置Value,至于具体返回什么要看具体实现 boolean equals(Object o);//定义equals方法用于判断两个Entry是否相同 int hashCode();//定义获取hashCode的方法

6.HashMap的常用方法解析

6.1  V put(K key, V value)

public V put(K key, V value) {    // 若“key为null”,则将该键值对添加到table[0]中。    if (key == null)        return putForNullKey(value);    // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。    int hash = hash(key.hashCode());    int i = indexFor(hash, table.length);    for (Entry<K,V> e = table[i]; e != null; e = e.next) {        Object k;        // 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {            V oldValue = e.value;            e.value = value;            e.recordAccess(this);            return oldValue;        }    }    // 若“该key”对应的键值对不存在,则将“key-value”添加到table中    modCount++;    addEntry(hash, key, value, i);    return null;}
put时的步骤为:①.若key为null,调用putForNullKey(value);

private V putForNullKey(V value) {        for (Entry<K,V> e = table[0]; e != null; e = e.next) {            if (e.key == null) {                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue;            }        }        modCount++;        addEntry(0, null, value, 0);        return null;    }
HashMap将“key为null”的元素都放在table的位置0处。

②.key不为null

先用hash()得到key的Hash码,然后通过indexFor得到在数组中的索引。再通过key.equals()在链表中找到插入 的位置

void addEntry(int hash, K key, V value, int bucketIndex) {    // 保存“bucketIndex”位置的值到“e”中    Entry<K,V> e = table[bucketIndex];    // 设置“bucketIndex”位置的元素为“新Entry”,    // 设置“e”为“新Entry的下一个节点”    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);    // 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小    if (size++ >= threshold)        resize(2 * table.length);}
6.2 V get(Object key)
public V get(Object key) {      if (key == null)          return getForNullKey();      // 获取key的hash值     int hash = hash(key.hashCode());     // 在“该hash值对应的链表”上查找“键值等于key”的元素      for (Entry<K,V> e = table[indexFor(hash, table.length)];           e != null;          e = e.next) {         Object k;         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))             return e.value;     }     return null; }
6.3 void putAll(Map<? extends K, ? extends V> m)

public void putAll(Map<? extends K, ? extends V> m) {    // 有效性判断    int numKeysToBeAdded = m.size();    if (numKeysToBeAdded == 0)        return;    // 计算容量是否足够,    // 若“当前实际容量 < 需要的容量”,则将容量x2。    if (numKeysToBeAdded > threshold) {        int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);        if (targetCapacity > MAXIMUM_CAPACITY)            targetCapacity = MAXIMUM_CAPACITY;        int newCapacity = table.length;        while (newCapacity < targetCapacity)            newCapacity <<= 1;        if (newCapacity > table.length)            resize(newCapacity);    }    // 通过迭代器,将“m”中的元素逐个添加到HashMap中。    for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {        Map.Entry<? extends K, ? extends V> e = i.next();        put(e.getKey(), e.getValue());    }}
6.4 containsKey() 

containsKey() 首先通过getEntry(key)获取key对应的Entry,然后判断该Entry是否为null

public boolean containsKey(Object key) {    return getEntry(key) != null;}
final Entry<K,V> getEntry(Object key) {    // 获取哈希值    // HashMap将“key为null”的元素存储在table[0]位置,“key不为null”的则调用hash()计算哈希值    int hash = (key == null) ? 0 : hash(key.hashCode());    // 在“该hash值对应的链表”上查找“键值等于key”的元素    for (Entry<K,V> e = table[indexFor(hash, table.length)];         e != null;         e = e.next) {        Object k;        if (e.hash == hash &&            ((k = e.key) == key || (key != null && key.equals(k))))            return e;    }    return null;}

7.关于Hash冲突

8.HashMap的优化

容量调整
对于容量的调整,这个是HashMap较为重点的部分,仔细想想看,对于hashMap我们应该做的是尽量的避免hash冲突 ,此时对于数组的扩容就应该考虑了。不过一个蛋疼的问题也就 出现了,由于新数组的容量变了,原数组的数据就必须重新计算其再数组中的位置,并放入这就是resize。同时这也是最消耗性能的地方。那么在什么情况下对HashMap进行扩容呢?一般当HashMap的元素个事超过数组大小**loadFactory的时候,就会进行扩容,而loadFactor就是上文所说的负加载因子。默认值为0.75 例如数组空间为16,当元素超过16*0.75=12的时候就把数组大小扩为2*16=32,然后resize这是一个非常消耗性能的是,因此如果我们预料到HashMap中元素的个数,这就能够有效的提高hashMap的性能。
负载因子
为确定何时调整大小,而不是对每个存储桶中的链接列表的深度进行计数,基于hash的  Map使用一个额外的参数并粗略计算存储桶的密度。Map在调整大小之前,使用名为LoadFactory的参数指示Map将承担的“负载”量,即它的负载程度。loadFactory、map大小、容量之间关系: 如果(负载因子)x(容量)>(Map 大小),则调整 Map 大小

数组长度为2的n次方      

当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。

 假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:

       h & (table.length-1)                       hash                             table.length-1

       8 & (15-1):                                 0100                   &              1110                   =                0100

       9 & (15-1):                                 0101                   &              1110                   =                0100

       -----------------------------------------------------------------------------------------------------------------------

       8 & (16-1):                                 0100                   &              1111                   =                0100

       9 & (16-1):                                 0101                   &              1111                   =                0101

  

  从上面的例子中可以看出:当它们和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就

产生了碰撞,8和9会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。

  同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么 最后一位永远是0,而0001,0011,0101,

1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,

这意味着进一步增加了碰撞的几率,减慢了查询的效率!

  而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,

加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上

形成链表。

   所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,

相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

9.总结

a.HashMap 非线程安全
b.初始长度为16,
c.允许键和值为null


ps:参考以下网友,感谢感谢~~

http://www.cnblogs.com/yuyutianxia/p/3800768.html

http://blog.csdn.net/lcore/article/details/8885961

http://www.cnblogs.com/hzmark/archive/2012/12/24/HashMap.html

 

2 0
原创粉丝点击