Java 源码分析HashMap的工作原理及实现

  • HashMap是什么?有什么特点?
  • initialCapacity 和 loadFactor 是什么?
  • 有哪些常用的方法?分别是如何实现的?

实例引出HashMap

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();
        map.put("一月", 1);
        map.put("二月", 2);
        map.put("三月", 3);
        map.put("四月", 4);
        map.put("五月", 5);
        map.put("六月", 6);
        map.put("七月", 7);
        map.put("八月", 8);
        System.out.println(map);
        for(Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

结果如下:

{一月=1, 六月=6, 二月=2, 三月=3, 四月=4, 八月=8, 七月=7, 五月=5} 一月: 1 六月: 6 二月: 2 三月: 3 四月: 4 八月: 8 七月: 7 五月: 5

下面我们来一步步看这个过程。

HashMap的定义

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

这是HashMap类的一个定义部分,继承了类 AbstractMap ,实现了 Map 接口。

官方文档中还有这样的描述:

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.

有几个关键点:
1. HashMap是一个实现了 Map 接口的hash table
2. 允许空值(null value)和空键(null key)
3. 和HashTable的区别是:HashMap是非同步,允许空values和key
4. HashMap中的序不保证和插入顺序一致,也不保证序不随时间变化
5. 由于基于Hash原理,所以 put 和 get 都是线性的

HashMap的构造函数

构造函数一


   /** * The load factor used when none specified in constructor. */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;


    ····//省略无关代码

   /** * The load factor for the hash table. * * @serial */
    final float loadFactor;

    ····


    /** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

在我们的例子中,

 HashMap<String, Integer> map = new HashMap<>();

用的就是这个无参的构造函数,引出一个关键点,

loadFactor 装载引子

Load factor The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.

Load factor就是bucket填满程度的最大比例, 默认值(DEFAULT_LOAD_FACTOR)为 0.75.

构造函数二

    /** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

引出第二个关键点,

Initial capacity 初始容量


Initial capacity The capacity is the number of buckets in the hash table, The initial capacity is simply the capacity at the time the hash table is created.

容量就是指哈希表创建时表中桶的个数。

好接着看这句:

 this(initialCapacity, DEFAULT_LOAD_FACTOR);

调用了另外一个构造函数

构造函数三

/** * 生成一个空的HashMap,并指定其容量大小和负载因子 * */
    public HashMap(int initialCapacity, float loadFactor) {
    //保证初始容量大于等于0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
  initialCapacity);
  //保证初始容量不大于最大容量MAXIMUM_CAPACITY
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
   //loadFactor小于0或为无效数字
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

总结

当我们创建 HashMap 的时候,可以根据根据需求去指定 initialCapacity 和 loadFactor ,也可以让系统使用默认值 16 和 0.75

如果对迭代性能要求很高的话不要把capacity设置过大,也不要把load factor设置过小。

当bucket中的entries的数目大于capacity*load factor时就需要调整bucket的大小为当前的2倍。

    /** * The default initial capacity - MUST be a power of two. */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
   /** * The load factor used when none specified in constructor. */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

put方法

好,我们已经分析了

HashMap<String, Integer> map = new HashMap<>();

这行代码,希望我已经讲清楚了HashMap的定义,以及它的构造函数,以及 initialCapacity 和 loadFactor 。桶式也希望读者在读我这篇文章的时候,能边看Java源码边思考。

接着

 map.put("一月", 1);

put函数无疑是最常用的方法之一,关于HashMap的put方法在面试中也会经常问到。

  1. 底层维护一个 Node 数组,数组中每一项都是一个Node
  2. 向hashMap放置的对象实际上是存储在数组中,Map中的 key value 则以Node 的形式存放在数组中。
  3. 数组中的每个位置,通常被成为位桶和 hash 桶, 即hash相同的Node全放在同一个位置,冲突用 链表相连或者红黑树处理。

    put 函数的过程如下:

  4. 对key的hashCode()做hash,然后再计算index;
  5. 如果当前map中无数据,执行resize方法。并且返回n
  6. 如果要插入的键值对要存放的这个位置刚好没有元素,那么把他封装成Node对象,放在这个位置上
  7. 否则的话,说明这上面有元素(也就是说要hash碰撞)
  8. 如果这个元素的key与要插入的一样,那么就替换一下
  9. 如果当前节点是TreeNode类型的数据,执行putTreeVal方法
  10. 该链为链表,遍历这条链子上的数据
  11. 超过load factor*current capacity,resize
    public V put(K key, V value) {
    // 1. 对key的hashCode()做hash,然后再计算index;
        return putVal(hash(key), key, value, false, true);
    }


    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
                   // 2. 底层维护一个 Node 数组,数组中每一项都是一个Node,关于这个Node后面再详细介绍
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 如果当前map中无数据,执行resize方法。并且返回n
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            //如果要插入的键值对要存放的这个位置刚好没有元素,那么把他封装成Node对象,放在这个位置上
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        //否则的话,说明这上面有元素(也就是说要hash碰撞)
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                //如果当前节点是TreeNode类型的数据,执行putTreeVal方法
            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;

        // 超过load factor*current capacity,resize
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

Node

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

静态内部类Node实现了Map接口中的Map.Entry接口,有四个成员变量, hash 值, key, value, 以及指向下一个元素的next 。

更多

Java HashMap工作原理及实现

HashMap工作原理

    原文作者:小风筝0010
    原文地址: https://blog.csdn.net/Zheng548/article/details/72522740
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞