HashMap详解

来源:互联网 发布:猫眼网络大电影数据 编辑:程序博客网 时间:2024/06/05 04:04

1、HashMap简介:
1.1、HashMap的存储结构:链表的数组
数据的主要存储结构有:顺序存储,链式存储,索引存储,散列存储。
1. 顺序存储:逻辑上相邻的元素存储在物理位置上也相邻的存储单元里。优点:随机存取;缺点:产生碎片。例如:数组,特点是:寻址容易,插入和删除困难;
2. 链式存储:逻辑上相邻的元素存储在物理位置上不一定相邻的存储单元里。优点:没有碎片;缺点:存储的指针需要占用一定的空间,并且只能顺序存取。例如:链表,寻址困难,插入和删除容易;
3. 索引存储:在存储元素信息时,同时建立一张索引表。优点:检索速度快;缺点:增加了附加的索引表;
4. 散列存储:根据元素的关键字直接计算出该元素的存储地址,又称为Hash存储。优点:检索,增加和删除节点的速度都很快;缺点:散列函数不好可能会引发元素存储冲突,而解决冲突会增加额外的时间和空间开销。

哈希表有多种不同的实现方法,HashMap采用的是一种常用的实现方法——拉链法,我们可以理解为“链表的数组”。如下所示:

HashMap存储结构

1.2、HashMap的继承关系:

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

1.3、HashMap的成员变量:

    // 默认的初始容量(容量为HashMap中槽的数目)是16,且实际容量必须是2的整数次幂。         static final int DEFAULT_INITIAL_CAPACITY = 16;    // 最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)    static final int MAXIMUM_CAPACITY = 1 << 30;    // 默认加载因子为0.75      static final float DEFAULT_LOAD_FACTOR = 0.75f;    // 存储数据的Entry数组,长度是2的幂。HashMap采用链表法解决冲突,每一个Entry本质上是一个单向链表     transient Entry[] table;   // HashMap的底层数组中已用槽的数量    transient int size;   //HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)     int threshold;  // 加载因子实际大小    final float loadFactor;    // HashMap被改变的次数    transient volatile int modCount;

HashMap中的静态内部类:Entry,其重要的属性有 key , value, next。我们上面说到HashMap的基础就是一个链表数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。

 static class Entry<K,V> implements Map.Entry<K,V> {        final K key;        V value;        Entry<K,V> next;        final int hash;        ...    }

2、HashMap常用的构造器:

  • HashMap():构建一个初始容量为 16,负载因子默认为 0.75 的 HashMap;

  • HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap;

  • HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。

而其中的负载因子loadFactor的理解为:HashMap中的数据量/HashMap的总容量(initialCapacity),当loadFactor达到指定值或者0.75时候,HashMap的总容量自动扩展一倍,以此类推。

扩充示意图

对于 HashMap 及其子类而言,它们采用 Hash 算法来决定集合中元素的存储位置。当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity 的 Entry 数组,这个数组里可以存储元素的位置被称为“桶(bucket)”,每个 bucket都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。

无论何时,HashMap 的每个“桶”只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因此可能出现的情况是:HashMap 的 bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链。如图所示:

Entry table示意图

3、HashMap 的常用方法

3.1 public V put(K key , V value) 方法

 public V put(K key, V value) {        if (key == null)            return putForNullKey(value);        int hash = hash(key.hashCode());        int i = indexFor(hash, table.length);        // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素        for (Entry<K,V> e = table[i]; e != null; e = e.next) {            Object k;            //找到指定 key 与需要放入的 key 相等(hash 值相同,通过 equals 比较放回 true),那么替换。            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue;            }        }        modCount++;        addEntry(hash, key, value, i);        return null;    }

3.1.1 key==null时,将执行下面的方法:

    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允许存放null键和null值。key值只允许有一个null,value则可以允许有多个null。

当key为null时,调用putForNullKey()方法,将value放置在数组第一个位置。也就是存在key=null时,则table[0]中的Entry存放 key=null value=”新值”。记住,key为null的键值对永远都放在以table[0]为头结点的链表中,当然不一定是存放在头结点table[0]中。

3.1.1 key !=null 时,可以归纳为以下几个步骤:

根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置;若两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的value 将覆盖集合中原有 Entry 的 value,但 key 不会覆盖;反之,如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部 。

当向 HashMap 中添加 key-value 对,由其key 的 hashCode() 返回值决定该 key-value 对(就是 Entry 对象)的存储位置。当两个 Entry 对象的 key 的 hashCode() 返回值相同时,将由 key 通过 eqauls() 比较值决定是采用覆盖行为(返回 true),还是产生 Entry 链(返回 false)。

上面程序中还调用了 addEntry(hash, key, value, i)方法:

    void addEntry(int hash, K key, V value, int bucketIndex)       {           // 获取指定 bucketIndex 索引处的 Entry           Entry<K,V> e = table[bucketIndex];     // ①          // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry           table[bucketIndex] = new Entry<K,V>(hash, key, value, e);           // 如果 Map 中的 key-value 对的数量超过了极限          if (size++ >= threshold)               // 把 table 对象的长度扩充到 2 倍。              resize(2 * table.length);    // ②      }  

上面方法的代码很简单,但其中包含了一个非常优雅的设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序①号代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。

根据上面代码可以看出,在同一个 bucket 存储 Entry 链的情况下,新放入的 Entry 总是位于 bucket 中,而最早放入该 bucket 中的 Entry 则位于这个 Entry 链的最末端。
上面程序中还有这样两个变量:
size:该变量保存了该 HashMap 中所包含的 key-value 对的数量。
threshold:该变量包含了 HashMap 能容纳的 key-value 对的极限,它的值等于 HashMap 的容量乘以负载因子(load factor)。

从上面程序中②号代码可以看出,当 size++ >= threshold 时,HashMap 会自动调用resize 方法扩充 HashMap 的容量。每扩充一次,HashMap 的容量就增大一倍

3.2 public V get(Object key) 方法:
当 HashMap 的每个 bucket 里存储的 Entry 只是单个 Entry ——也就是没有通过指针产生 Entry 链时,此时的 HashMap 具有最好的性能:

当程序通过 key 取出对应 value 时,系统只要先计算出该 key 的 hashCode() 返回值,再根据该 hashCode 返回值找出该key 在 table 数组中的索引,然后取出该索引处的 Entry,最后返回该 key 对应的 value 即可。看 HashMap 类的 get(K key) 方法代码:

public V get(Object key) {        if (key == null)            return getForNullKey();        int hash = hash(key.hashCode());        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;    }

从上面代码中可以看出,如果 HashMap 的每个 bucket 里只有一个 Entry 时,HashMap 可以根据索引、快速地取出该 bucket 里的 Entry;在发生“Hash 冲突”的情况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。

4、Fail-Fast机制:
我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。

这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中HashIterator(){…}会将这个值赋给迭代器的expectedModCount

private abstract class HashIterator<E> implements Iterator<E> {        Entry<K,V> next;    // next entry to return        int expectedModCount;   // For fast-fail        int index;      // current slot        Entry<K,V> current; // current entry        HashIterator() {            expectedModCount = modCount;            if (size > 0) { // advance to first entry                Entry[] t = table;                while (index < t.length && (next = t[index++]) == null)                    ;            }        }        public final boolean hasNext() {            return next != null;        }        final Entry<K,V> nextEntry() {            if (modCount != expectedModCount)                throw new ConcurrentModificationException();            Entry<K,V> e = next;            if (e == null)                throw new NoSuchElementException();            if ((next = e.next) == null) {                Entry[] t = table;                while (index < t.length && (next = t[index++]) == null)                    ;            }        current = e;            return e;        }        public void remove() {            if (current == null)                throw new IllegalStateException();            if (modCount != expectedModCount)                throw new ConcurrentModificationException();            Object k = current.key;            current = null;            HashMap.this.removeEntryForKey(k);            expectedModCount = modCount;        }    }

在迭代过程中nextEntry(),判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map:注意到modCount声明为volatile,保证线程之间修改的可见性。

0 0