首页 > 代码库 > JDK源码学习系列08----HashMap

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

                                                          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 = http://www.mamicode.com/v;>其中,Map接口:

K getKey();//获取Key
 V getValue();//获取Value
V 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 = http://www.mamicode.com/e.value;>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 = http://www.mamicode.com/e.value;>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