HashMap实现原理分析

HashMap 介绍

HashMap在我们的日常开发中非常常见,其本质就是基于数组和链表组合实现。它提供了很方便的key-value的存取接口,通过对key进行Hash计算散列存储位置和快速查找,HashMap允许key和value为null。HashMap并不是线程安全的,如果存在多线程存取操作容易出现注明的ConcurrentModificationException异常。其存储方式可以由下图表示:
  这里写图片描述

HashMap 的实现

在前面的介绍中说道HashMap是基于数据和链表的,我们先看看它的构造方法中,这里我们从无参构造方法开始了解它的代码实现:

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

这里我们看到无参构造方法调用的是另外一个重载的构造方法,其中两个参数传入的分别是:

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


我们再看调用的构造方法的实现原理

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY) {
            initialCapacity = MAXIMUM_CAPACITY;
        } else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
            initialCapacity = DEFAULT_INITIAL_CAPACITY;
        }

        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        // Android-Note: We always use the default load factor of 0.75f.

        // This might appear wrong but it's just awkward design. We always call
        // inflateTable() when table == EMPTY_TABLE. That method will take "threshold"
        // to mean "capacity" and then replace it with the real threshold (i.e, multiplied with
        // the load factor).
        threshold = initialCapacity;
        init(); // 是一个空方法,忽略
    }

这里先解释下两个参数的意义:

  • initialCapacity 是HashMap存储数据的初始化大小,从传入的值可以知道默认的初始化大小是4。注意:为了方便做位运算,数组的大小都是大于且最接近指定初始化大小值的2的幂次方。例如,在使用中直接调用上面的有参构造函数,initialCapacity传入的是5,那么在构造时默认的数组大小将是8(这里初始化4,最终构造数组时大小也是8)
  • loadFactor 是HashMap 存储数组的扩容阀值比例,当数组的容量超过initialCapacity * loadFactor时,这里默认是0.75,即数组put进第3个元素时,数组将自动扩容。此后扩容的阀值一直等于当下的数组大小乘于loadFactor。具体扩容的原理稍后会讲到

接下来分析下HashMap的put方法,put方法不仅是容器的插入操作,并存在HashMap动态扩容的逻辑,其流程图大致如下:
HashMap#put(K,V) 流程

再结合插入源码

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold); // 初始化表
        }
        if (key == null) // 允许key为空
            return putForNullKey(value);
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key); // 计算key的哈希值,利用雪崩效应
        int i = indexFor(hash, table.length);// 通过key的hash值查找在数组上的散列位置
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {// 如果为同一个key,则覆盖原来的value
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;// 需提防concurrentModificationException
        addEntry(hash, key, value, i); // 插入一个新的元素,并存在数组扩容的逻辑
        return null;
    }

这里我们重点看下初始化数组和插入数组或者链表的实现

  • 数组的初始化
    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);// 获取初始化大小

        // Android-changed: Replace usage of Math.min() here because this method is
        // called from the <clinit> of runtime, at which point the native libraries
        // needed by Float.* might not be loaded.
        float thresholdFloat = capacity * loadFactor;
        if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
            thresholdFloat = MAXIMUM_CAPACITY + 1;
        }

        threshold = (int) thresholdFloat;
        table = new HashMapEntry[capacity];
    }

上面就是一个数组容器和扩容阀值的的初始化过程,代码比较易懂,主要关注roundUpToPowerOf2(int) 方法,它确保了传入的初始化大小不大于MAXIMUM_CAPACITY = 1 << 30 的数值,且不能为负数,且是一个大于并最接近传入数值的2的幂次方数值(为了方便做位运算),代码如下:

    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        int rounded = number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (rounded = Integer.highestOneBit(number)) != 0
                    ? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
                    : 1;

        return rounded;
    }

为方便理解,我用一个流程图并用一个实际数值举例上面的实现
这里写图片描述

  • Hash值的计算
    针对Key的Hash值的计算由方法:
    // Spread bits to regularize both segment and index locations,
    // using variant of single-word Wang/Jenkins hash.
    //
    // Based on commit 1424a2a1a9fc53dc8b859a77c02c924.
    public static int singleWordWangJenkinsHash(Object k) {
        int h = k.hashCode();

        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h <<   3);
        h ^= (h >>>  6);
        h += (h <<   2) + (h << 14);
        return h ^ (h >>> 16);
    }

这是很重要的一步,通过再Hash算法运算,减少Hash冲突,能让元素尽可能地分散在数组中不至于链表过长影响效率,具体Hash过程可以参考:
http://www.infoq.com/cn/articles/ConcurrentHashMap/

  • 插入元素流程
    首先要定位元素插入的位置,从上面的put方法中知道通过以下接口实现:
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

由于对key的hash计算非常得散列,而在之前构造数组大小时已经决定是2的幂次方。此时hash值和数组长度减一的数值(有效位数全是1)做&运算既可以保证散列性,有可以保证元素的位置可以落在数组的范围内不至于越界。

接着我们查看插入元素的方法实现:

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) { // 超过阀值并且该位置已经有元素占据
            resize(2 * table.length); // 扩容,并且重新定义其他元素的位置
            hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);// 加入元素
    }

扩容方法resize(int)中,重要的一点是重新定义旧元素的新位置,通过以下方法实现:

    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(HashMapEntry[] newTable) {
        int newCapacity = newTable.length;
        for (HashMapEntry<K,V> e : table) {
            while(null != e) {
                HashMapEntry<K,V> next = e.next;
                // 将链表上的数据放到数组当中,元素重新移动的概率是50%,取决与Hash值的与数组长度减一后的二进制最高位做&运算那一位是0还是1
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

由于是个链表结构,需要通过双层循环去重新排布元素的位置,性能是比较糟糕的。在java8当中已经改为红黑树结构。对上面的注释稍作下解释,假如当前数组的长度为4,其中某个元素A的hash值是110,则他在当前数组的位置110 & 011 = 010,即是第2位。然而当数组扩容到8时,A的位置应该是110 & 111 = 110,就被重新分配到第6位。而如果A的hash值是010,则扩容前后的位置仍然是2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值