从源码的角度看 HashMap

先看 HashMap 类注释是怎么介绍 HashMap 的。

/**
 * Hash table based implementation of the Map interface.  This
 * implementation provides all of the optional map operations, and permits
 * null values and the null key.  (The HashMap
 * class is roughly equivalent to Hashtable, except that it is
 * unsynchronized and permits nulls.)  This class makes no guarantees as to
 * the order of the map; in particular, it does not guarantee that the order
 * will remain constant over time.
 *
 * 

This implementation provides constant-time performance for the basic * operations (get and put), assuming the hash function * disperses the elements properly among the buckets. Iteration over * collection views requires time proportional to the "capacity" of the * HashMap instance (the number of buckets) plus its size (the number * of key-value mappings). Thus, it's very important not to set the initial * capacity too high (or the load factor too low) if iteration performance is * important. * *

An instance of HashMap has two parameters that affect its * performance: initial capacity and load factor. The * capacity is the number of buckets in the hash table, and the initial * capacity is simply the capacity at the time the hash table is created. The * load factor is a measure of how full the hash table is allowed to * get before its capacity is automatically increased. When the number of * entries in the hash table exceeds the product of the load factor and the * current capacity, the hash table is rehashed (that is, internal data * structures are rebuilt) so that the hash table has approximately twice the * number of buckets. * *

As a general rule, the default load factor (.75) offers a good * tradeoff between time and space costs. Higher values decrease the * space overhead but increase the lookup cost (reflected in most of * the operations of the HashMap class, including * get and put). The expected number of entries in * the map and its load factor should be taken into account when * setting its initial capacity, so as to minimize the number of * rehash operations. If the initial capacity is greater than the * maximum number of entries divided by the load factor, no rehash * operations will ever occur. * *

If many mappings are to be stored in a HashMap * instance, creating it with a sufficiently large capacity will allow * the mappings to be stored more efficiently than letting it perform * automatic rehashing as needed to grow the table. Note that using * many keys with the same {@code hashCode()} is a sure way to slow * down performance of any hash table. To ameliorate impact, when keys * are {@link Comparable}, this class may use comparison order among * keys to help break ties. * *

Note that this implementation is not synchronized. * If multiple threads access a hash map concurrently, and at least one of * the threads modifies the map structurally, it must be * synchronized externally. (A structural modification is any operation * that adds or deletes one or more mappings; merely changing the value * associated with a key that an instance already contains is not a * structural modification.) This is typically accomplished by * synchronizing on some object that naturally encapsulates the map. * * If no such object exists, the map should be "wrapped" using the * {@link Collections#synchronizedMap Collections.synchronizedMap} * method. This is best done at creation time, to prevent accidental * unsynchronized access to the map:

 *   Map m = Collections.synchronizedMap(new HashMap(...));
* *

The iterators returned by all of this class's "collection view methods" * are fail-fast: if the map is structurally modified at any time after * the iterator is created, in any way except through the iterator's own * remove method, the iterator will throw a * {@link ConcurrentModificationException}. Thus, in the face of concurrent * modification, the iterator fails quickly and cleanly, rather than risking * arbitrary, non-deterministic behavior at an undetermined time in the * future. * *

Note that the fail-fast behavior of an iterator cannot be guaranteed * as it is, generally speaking, impossible to make any hard guarantees in the * presence of unsynchronized concurrent modification. Fail-fast iterators * throw ConcurrentModificationException on a best-effort basis. * Therefore, it would be wrong to write a program that depended on this * exception for its correctness: the fail-fast behavior of iterators * should be used only to detect bugs. * *

This class is a member of the * * Java Collections Framework. * * @param the type of keys maintained by this map * @param the type of mapped values * * @author Doug Lea * @author Josh Bloch * @author Arthur van Hoff * @author Neal Gafter * @see Object#hashCode() * @see Collection * @see Map * @see TreeMap * @see Hashtable * @since 1.2 */

译文:

Map 接口的基于哈希表的实现,它实现了 Map 接口所有的抽象方法,并允许存储 null 键和 null 值。HashMap 与 HashTable 非常相似,除了它是非线程安全的和允许存储 null。HashMap 不保证元素的顺序,因为底层是哈希表和重新散列,它甚至不能保证在一段时间内顺序是一致的。

HashMap 为基本操作(get 和 set)提供了恒定的时间性能。假设哈希函数将元素平均的分配到 bucket 之中,对集合视图的迭代与 HashMap 中 bucket 的数量和 entry 的数量成正比,所以如果迭代性能很重要,那么不要将初始容量设置的太高(或者负载因子设置的太低),这一点非常重要。

HashMap 中有两个影响其性能的参数:初始容量和负载因子。容量指的是哈希表中 bucket 的数量,初始容量就是哈希表被创建时的大小;负载因子是衡量哈希表在自动扩容之前运行达到的满量的指标,当哈希表中 entry 的数量超过负载因子和当前容量的乘积时,哈希表会自动扩容为之前容量的两倍,即以新的容量创建新的哈希表,此时需要将旧的 entry 重新哈希到新的哈希表中,这个过程叫做哈希表的重建。

作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了良好的平衡,更高的负载因子可以减少空间开销,但增加了查找成本。在设置初始容量的时候,应该要考虑 HashMap 的预期条目数和负载因子的值,以最大限度减少哈希表重建的次数,如果最大条目数小于初始容量乘以负载因子,即初始容量大于最大条目数除以负载因子,则不会发生哈希表重建。

需要注意的是,如果多个键的 hashCode() 方法返回相同的值,会减低哈希表性能,因为它可能导致多个键散列到同一个桶中,从而形成长链表或者红黑树(JDK 8 之后链表长度超过一定的阈值会自动转换成红黑树),为了减轻这种影响,可以让键实现 Comparable 接口,HashMap 会使用键之间的比较顺序来辅助排序,以帮助解决键的散列冲突。

需要注意的是,HashMap 是非线程安全的,如果多个线程同时访问一个 HashMap 实例,至少有一个线程在修改它的结构(结构修改指的是添加和删除一个或多个 entry,改变一个已经存在的 key 的 value 并不算结构修改),则必须对它进行外部同步,这通常是通过对一些封装了 HashMap 的对象进行同步来实现的,如果不存在这样的对象,则应使用 Collections#synchronizedMap 方法来包装 HashMap,这最好在创建时完成,以防止意外的对 HashMap 进行非同步的访问 Map m = Collections.synchronizedMap(new HashMap(...))

HashMap 所有的 “collection view methods” 返回的迭代器都是 fail-fast 的,如果在创建 iterator 之后,HashMap 在任意时间被修改了结构(除了 iterator 自己的 remove 方法),iterator 将会抛出 ConcurrentModificationException。因此,面对并发修改,iterator 会快速而干净的失败,而不是冒着在未来不确定的时间出现任意的、不确定行为的风险。

值得注意的是,iterator 的 fail-fast 行为无法得到保证,因为只有在迭代的时候才会检查 modCount 的值是否改变,如果改变了会抛出 ConcurrentModificationException,所以当 modCount 改变(发生并发修改)的时候并不能保证马上检测到 modCount 的变化,抛出 ConcurrentModificationException。

注释已经介绍了 HashMap 具有的所有关键特性,我们在了解了这些特性之后,再来看看 HashMap 的源码是如何实现这些特性的。

源码分析

成员变量

首先看看 HashMap 中定义的成员变量:

/**
 * 初始容量的默认值,必须是 2 的倍数
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
 * 负载因子的默认值
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
 * 容量的最大值
 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
 * 触发 bucket 树化(链表转红黑树)的 table 的最小容量
 */
static final int MIN_TREEIFY_CAPACITY = 64;
/**
 * 链表中元素的个数超过这个值,链表会自动转红黑树
 */
static final int TREEIFY_THRESHOLD = 8;
/**
 * 红黑树中的元素个数低于这个值,红黑树会自动转链表
 */
static final int UNTREEIFY_THRESHOLD = 6;

上面介绍的是静态成员变量,通常用来作为其他非静态成员变量的默认值,下面看一下非静态成员变量。

/**
 * HashMap 修改结构的次数,迭代器的 remove 不算,它和迭代器的 fail-fast 有关
 */
transient int modCount;
/**
 * entry 的个数
 */
transient int size;
/**
 * 哈希表,存储 entry
 */
transient Node<K,V>[] table;
/**
 * 下次动态扩容的大小,它的值是 capacity * load factor.
 */
int threshold;
/**
 * 负载因子
 */
final float loadFactor;
/**
 * entry 的集合视图
 */
transient Set<Map.Entry<K,V>> entrySet;
/**
 * HashMap 中的值的集合视图,这个变量定义在 AbstractMap 中
 */
transient Collection<V> values;
/**
 * HashMap 中的值的集合视图,这个变量定义在 AbstractMap 中
 */
transient Set<K>        keySet;
方法

我们在使用 HashMap 的时候,通常是先实例化,然后调用 put 方法存储数据,之后调用 get 方法得到数据,或者使用 iterator 遍历数据,如下:

@Test
public void test07(){
    Map<String, String> map = new HashMap<>();
    map.put("001", "bob");
    map.put("002", "john");
    map.put("003", "slice");

    String value = map.get("001");
    System.out.println(value);
}
put

HashMap 的无参数构造方法没有什么好分析的,它只有一行设置负载因子的代码,我们用的最多的是 put 方法,HashMap#put 的键和值都可以为 null,我们来看看 put 方法做了些什么。

public V put(K key, V value) {
    // 将操作委托给了 putVal 方法
    return putVal(hash(key), key, value, false, true);
}

// 这是一个 final 方法,防止用户重写该方法
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)
        /* 
         * 如果 table 为 null 或者 table 的长度为 0 的时候,调用 resize() 方法扩容哈希表,如果是第一次调用 put 方法
         * 会进入这个 if 条件中
         */
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        /* 
         * 计算 key 的哈希值 i,从 tab[i] 中获取指定哈希值对应的 Node 对象 p,如果 p 为 null,则说明没有发生哈希冲突
         * 使用 key value 构建 Node 并填充到 tab[i]
         */
        tab[i] = newNode(hash, key, value, null);
    else {
        // 如果 p 不为 null,则发生了哈希冲突,链表和红黑树有不同的解决哈希冲突的逻辑,需要分开处理
        /* 
         * 如果发生了哈希冲突
         */
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // key 相同
            e = p;
        else if (p instanceof TreeNode)
            // 红黑树解决哈希冲突
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 链表解决哈希冲突,需要从头节点遍历,直到找到有相同 key 的 节点或者到达了尾节点
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    // p 是尾节点,此时 e = null,构建新的 Node 插入到 p 的后面
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 检查是否要将链表转红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                // e.key 和 key 相等,找到相同的节点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        // e 不为 null,则 e 和新插入具有相同的 key,用新的 value 覆盖 e.value,并返回旧的 value
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 给子类提供的回调函数
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 因为 put 方法会修改 HashMap 的结构,所以 modCount 自增
    ++modCount;
    /*
     * 检查 HashMap 的容量 size 是否超过了阈值 threshold,如果超过了就扩容
     * 从这里可以看出每次调用 put 方法都会检查是否需要扩容,且扩容是发生在数据插入 HashMap 之后的
     * 这里是不是在插入数据之前检查并扩容会好一点?因为这样就可以减少一次插入数据的损耗了
     * 因为如果扩容的就可以不将数据插入到旧哈希表,而是在扩容之后插入到新哈希表,但是整体逻辑可能就复杂了,JDK 的实现可读性更高
     */
    if (++size > threshold)
        resize();
    // 给子类提供的回调函数
    afterNodeInsertion(evict);
    return null;
}
get

然后我们来看看 get 方法,get 方法根据 key 从 Map 中得到指定的 value,如果 key 不存在则返回 null,返回 null 并不一定就代表 key 不存在,也有可能这个 key 对应的 value 就是 null。

// 委托给 getNode 方法
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

// getNode 方法也是一个 final 方法,防止子类重写该方法
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) {
        // 因为哈希冲突的缘故,所以可能需要对比多个值,首先比较第一个 node
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 如果第一个 node 的 key 不相等,则分从红黑树和链表两种数据结构获取下一个 node 继续比较,直到最后一个 node
            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;
}
remove

接下来看看 remove 方法,remove 方法用来删除 HashMap 中存在的 entry。

// 类似的,委托给 removeNode 方法
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

// remove 方法的代码乍一看和 get 方法的代码很相似,因为你要想 remove 它,必须先找到 它
final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 检查第一个 node 是否相等
            node = p;
        else if ((e = p.next) != null) {
            // 在红黑树和链表中查找要删除的 node,如果找到了就赋值给局部变量 node
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 如果 node != null,说明找到了要删除的节点
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                // 红黑树结构的删除,这里面还涉及了
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                // 如果找到的就是第一个节点,则将 table 中的 bucket 指向 node 的下一个节点
                tab[index] = node.next;
            else
                // 链表结构的删除
                p.next = node.next;
            // 修改了 HashMap 的结构,modeCount 自增
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}
resize

除了用户常用的 put、get 和 remove 方法之外,还有一个很重要的方法是 resize,它负责哈希表的重建。

// 同样是 final 方法
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // oldCap 表示旧容量大小,这个只有在初始化的时候为 0,其他情况都会大于 0
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 如果老容量大于等于最大容量 MAXIMUM_CAPACITY,则不会扩容
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 扩容成之前大小的两倍
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    // 新哈希表
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    // 如果当前 bucket 只有一个元素
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    /*
                     * 如果当前 bucket 使用红黑树解决哈希冲突
                     * 这里会检查红黑树的元素个数是否小于阈值 UNTREEIFY_THRESHOLD,如果小于会将红黑树转链表
                     */
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 如果当前 bucket 使用链表解决哈希冲突
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
keySet()、values()、entryKey()

这三个方法也是 HashMap 中比较常用的方法,它们分别返回 HashMap 中键、值和键值对(Entry)的集合,这些方法返回的集合是视图,它们是基于原始 HashMap 数据的快照,如果修改它们会影响到原始的 HashMap,反之亦然。这三个方法与 entrySet、values 和 keySet 三个成员变量相关。

这三个方法的实现逻辑类似,这里只分析 entrySet() 方法,它返回 HashMap 中键值对(Entry)的集合视图,我们一般用它来遍历 HashMap 中的键值对。

@Test
public void test08(){
    Map<String, String> map = new HashMap<>();
    map.put("001", "bob");
    map.put("002", "john");
    map.put("003", "slice");

    Set<Map.Entry<String, String>> entries = map.entrySet();
    Iterator<Map.Entry<String, String>> iterator = entries.iterator();
    while(iterator.hasNext()){
        Map.Entry<String, String> entry = iterator.next();
        String key = entry.getKey();
        String value = entry.getValue();
        System.out.println("key: " + key + ",value: " + value);
    }

}

我们接着看 entrySet() 方法在 HashMap 中源代码:

// 返回的是一个 EntrySet 实例
public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

//EntrySet 实现了 AbstractSet,它支持 iterator、contains、remove 和 clear 方法,但是不支持 add 和 addAll 方法,因为它没有重写这两个方法
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<Map.Entry<K,V>> iterator() {
        return new EntryIterator();
    }
    public final boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>) o;
        Object key = e.getKey();
        Node<K,V> candidate = getNode(hash(key), key);
        return candidate != null && candidate.equals(e);
    }
    public final boolean remove(Object o) {
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Object value = e.getValue();
            return removeNode(hash(key), key, value, true, true) != null;
        }
        return false;
    }
    public final Spliterator<Map.Entry<K,V>> spliterator() {
        return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}

// EntrySet 的 iterator 方法返回一个 EntryIterator 实例,它集成了 HashIterator 类
final class EntryIterator extends HashIterator
    implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}

/**
 * 到这里才看到 EntrySet 操作的底层数据结构,它通过操作 HashMap 的哈希表 table,来提供 Entry 的 Set 视图,
 * 这也是为什么 EntrySet 被称为视图的原因,因为底层并没有构建这样一个 Set,它是通过操作 table 来实现这些功能的
 */
abstract class HashIterator {
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        if ((next = (current = e).next) == null && (t = table) != null) {
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }

    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }
}

你可能感兴趣的:(java,hashmap)