Java源码阅读之HashMap

来源:互联网 发布:和孩子一起学编程 编辑:程序博客网 时间:2024/05/22 09:43

HashMap是基于哈希表的Map实现,它提供了所有可选的映射操作,并允许空value和空key。(HashMap类似于Hashtable, 除了HashMap是线程不安全的,并允许null)。HashMap不保证映射顺序,而且也不保证顺序在一段时间内保持不变。
HashMap的迭代需要与其实例的容量(Hash桶数)与其size(键值映射数)成比例。因此,如果迭代性能很重要,就不要将初始容量设置得太高(或负载因子太低)。HashMap的一个实例有两个影响其性能的参数:初始容量和负载因子。容量是Hash表中的Hash桶数,初始容量只是创建哈希表时的容量;负载因子是在容量自动增加之前允许哈希表得到满足的度量。当哈希表中的条目数超过负载因子和当前容量的乘积时,就重新排列哈希表(这时HashMap内部数据结构被重新构建,以使散列表具有大约两倍的桶数,即容量增加为原来的2倍。默认负载因子(0.75)提供了时间和空间成本之间的良好折中,初始容量为16,如果初始容量大于最大条目数除以负载因子, 则不会发生rehash操作。
需要注意,HashMap是线程不安全的。如果多个线程同时访问HashMap, 并且至少有一个线程在结构上修改了映射, 那么它必须在外部进行同步。可以使用 Collections.synchronizedMap方法“包装”Map以获取线程安全的HashMap。
HashMap的迭代器都是fast-fail的,如果Map在迭代器创建之后的任何时间被结构化地修改, 除了通过迭代器自己的remove方法之外, 都会抛出一个ConcurrentModificationException异常。

总结一下:
1. HashMap容许空Key和空value
2. HashMap默认有两个初始参数:initial capacity和load factor
3. HashMap不是线程安全的

HashMap的数据存储

HashMap的数据存储在Node<K, V> [] table的数组中。先看看Node的定义。

static class Node<K,V> implements Map.Entry<K,V> {    final int hash;    final K key;    V value;    Node<K,V> next;    Node(int hash, K key, V value, Node<K,V> next) {        this.hash = hash;        this.key = key;        this.value = value;        this.next = next;    }    //......}

存储数组的每一个节点都是一个单链表,链表的每一个节点存储该节点数据的key,value和hashCode。

再看看插入操作:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                   boolean evict) {        Node<K,V>[] tab; Node<K,V> p; int n, i;        if ((tab = table) == null || (n = tab.length) == 0)            n = (tab = resize()).length;        if ((p = tab[i = (n - 1) & hash]) == null)            tab[i] = newNode(hash, key, value, null);        else {            Node<K,V> e; K k;            if (p.hash == hash &&                ((k = p.key) == key || (key != null && key.equals(k))))                e = p;            else if (p instanceof TreeNode)                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);            else {                for (int binCount = 0; ; ++binCount) {                    if ((e = p.next) == null) {                        p.next = newNode(hash, key, value, null);                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                            treeifyBin(tab, hash);                        break;                    }                    if (e.hash == hash &&                        ((k = e.key) == key || (key != null && key.equals(k))))                        break;                    p = e;                }            }            if (e != null) { // existing mapping for key                V oldValue = e.value;                if (!onlyIfAbsent || oldValue == null)                    e.value = value;                afterNodeAccess(e);                return oldValue;            }        }        ++modCount;        if (++size > threshold)            resize();        afterNodeInsertion(evict);        return null;    }
  1. 如果table数组为空,调用resize()为table分配空间,得到table大小为n
  2. 如果key的hashCoden计算key的索引i=(n - 1) & hash。如果table数组中索引为i的元素为空,则直接new一个Node对象存储到该位置。
  3. 如果索引为i的位置不为null,则从该节点的链表的头部依次迭代,如果发现链表的这个节点p和要插入数据p的key满足:p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))),则替换原来的节点p。
  4. 如果遍历单链表也没有找到满足p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))的节点,则将插入的值放到该链表的尾部。
  5. 如果该节点的单链表大于8,则需要将单链表转化为红黑树,即其中的treeifyBin操作。
  6. 修改内部属性modCountsize的值,如果修改后的size大于threshold,则需要执行resize()操作。

对于get操作,代码如下:

final Node<K,V> getNode(int hash, Object key) {        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;        if ((tab = table) != null && (n = tab.length) > 0 &&            (first = tab[(n - 1) & hash]) != null) {            if (first.hash == hash && // always check first node                ((k = first.key) == key || (key != null && key.equals(k))))                return first;            if ((e = first.next) != null) {                if (first instanceof TreeNode)                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);                do {                    if (e.hash == hash &&                        ((k = e.key) == key || (key != null && key.equals(k))))                        return e;                } while ((e = e.next) != null);            }        }        return null;    }

大概思路如下:

  1. 通过给定的key计算出hashCode,根据(n - 1) & hash计算出桶的位置。
  2. 找到桶,如果桶不为空,判断桶中链表的第一个节点first是否满足first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))),如果满足,返回该节点的value值。
  3. 遍历这个单链表,如果有节点满足上述条件,返回该节点的value。
  4. 如果index桶中的元素为TreeNode对象,则这个桶中的KV对数量大于8,该桶为一颗红黑树,调用红黑树的getTreeNode(key, hash)方法查找key对于的节点,并返回value。
  5. 其他情况,返回null。

resize过程

 final Node<K,V>[] resize()

resize用来初始化或者将table的容量翻倍。如果table为null, 则为其分配初始容量(16)。步骤如下:

  1. 根据当前的容量(oldTab.length)和门限threshold,计算出resize之后的容量并更新会内部属性,在不超出最大容量(1>>30)前提下,容量翻倍。
  2. 遍历table,如果某个位置j的桶不为空,并且桶中的链表只有一个节点e,直接将其放到新的newTab中,newTab[e.hash & (newCap - 1)] = e
  3. 如果这个桶中的节点e是一个TreeNode的节点,需要将这个红黑树拆两棵树,拆分的依据是:(e.hash & oldCap) == 0,为0时放到loHead中,不为0时放到hiHead中。然后将loHead放到新表newTab的j索引的位置。HiHead放到newTab的的j+oldCao位置。
  4. 如果桶中的元素为链表的头结点,按照同样的方式拆分到新的两个新的链表loHeadhiHead位置。然后将loHead放到新表newTab的j索引的位置。HiHead放到newTab的的j+oldCao位置。
  5. 遍历所有桶,重复2、3、4步直到结束。

对比

(1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。