HashMap源码解析(JDK1.7)

概念

HashMap是基于Hash表的Map实现,提供所有可选的键值映射操作。底层实现是数组单链表实现,数组中的每个元素是一个链表节点,通过链表来处理hash冲突。

源码解析

源码解析只涉及到一些核心功能实现分析,不会对HashMap的所有源码进行展开。

字段属性
    // 默认HashMap容量
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    // 最大HashMap容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 结点数组
    transient Entry<K,V>[] table;
    // 数组大小
    transient int size;
    // 阈值。等于size * loadFactor, 当容量超过阈值会进行扩容操作;
    int threshold;
    // 加载因子, 衡量HashMap满的指标, 默认0.75。
    final float loadFactor;
单链表节点结构
	// 单链表结点结构, 实现Map.Entry接口来操作Map键值对
    static class Entry<K, V> implements Map.Entry<K, V> {
        final K key;
        V value;
        Entry<K, V> next;
        int hash;//对输入对象进行hash函数生成,相同的对象只会生成相同的hash值, 不同的对象可能生成相同的hash值(碰撞或者冲突)。

        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    }
构造函数
  public HashMap(int initialCapacity, float loadFactor) {
        // 参数边界值判断
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        // 寻找一个大于等于initialCapacity的2次幂的数
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
        // 初始化参数和节点数组
        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
 }
方法分析
put
    // 添加键值对
    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        // 计算key的hash值
        int hash = hash(key);
        // 通过hash值计算出table下标
        int i = indexFor(hash, table.length);
        for(Entry<K, V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 结点已存在值,覆盖并返回旧值
            if (e.hash == hash && ((k = e.key) == key) || e.value == value) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        // 添加键值对到结点数组中
        addEntry(hash, key, value, i);
        return null;
    }
	添加键值对步骤:
		1. 计算key的hash值。
		2. 通过hash值查找节点数组下标。
		3. 如果所在下标节点链表存在value覆盖旧值。
		4. 添加键值对到节点数组中。
	// hash函数
	final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        // 因为HashMap是通过hash值与HashMap的长度-1进行位运算取得其桶的下标来取代通过hash值对HashMap的长度进行取模运算
        // 但是通过位运算进行定位桶下标只利用了hash值的低位值,这样很容易产生hash冲突,因此通过位移将高位和低位进行混合扰动来
        // 规避只利用hash值的低位弊端来减少hash冲突的产生。
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
	// 通过hash值寻找桶下标
	static int indexFor(int h, int length) {
        // 因为位运算比取模运算更加高效,这里通过位运算来取代取模运算,但是这里有个限制那就是其length长度必须是2的幂次数。
        // 例如:2^n表示2的n次方,也就是说,一个数对2^n取模 == 一个数和(2^n - 1)做按位与运算 。
        // 假设n为3,则2^3 = 8,表示成2进制就是1000。2^3 -1 = 7 ,即0111。此时X & (2^3 - 1) 就相当于取X的2进制的最后三位数。
        // 从2进制角度来看,X / 8相当于 X >> 3,即把X右移3位,此时得到了X / 8的商,而被移掉的部分(后三位),则是X % 8,也就是余数。[全网把Map中的hash()分析的最透彻的文章,别无二家](https://coolshell.cn/articles/9606.html%5D%28https://coolshell.cn/articles/9606.html)
        return h & (length-1);
    }
	// 构建键值对节点添加到数组链表中
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 数组大小超过threshold, 哈希表进行rehash操作(原来数组大小扩展成两倍,将原有数组数据复制到新建数组)。
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            // 重新计算hash值和数组下标
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        // 插入键值对到数组链表中
        createEntry(hash, key, value, bucketIndex);
    }
	// 新建链表节点,插入到单链表头部。
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        // 新建Entry结点,插入到单链表头部。
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        size++;
    }
resize
    // 当HashMap容量超过阈值会进行rehash扩容操作
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        // 如果数组超过最大容量不扩容直接返回
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        // 新建数组,大小为原来数组大小的两倍
        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        // 将老的数组数据复制到新的数组中
        transfer(newTable, rehash);
        // 重新设置table和threshold
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
	// 将旧数组各个元素重新hash到新数组中
	void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        // 遍历数组每个结点
        for (Entry<K,V> e : table) {
            // 遍历每个结点链表
            while(null != e) {
                // 记录下一个结点
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                // 计算key值在新数组的下标
                int i = indexFor(e.hash, newCapacity);
                // 将老的数组下标结点链表数据反向插入到新数组下标结点。这个操作可能导致在多线程环境下导致回环链表。[疫苗:JAVA HASHMAP的死循环](https://coolshell.cn/articles/9606.html)
                // to[i] = e->f->null;
                // t[i] = e->null
                // f->e->null;
                // t[i] = f->e->null
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
get
    // 通过键获取值
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
	final Entry<K,V> getEntry(Object key) {
        // 获取key的hash值
        int hash = (key == null) ? 0 : hash(key);
        // 根据hash值得到数组下标。对数组下标结点链表进行遍历
        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 != null && key.equals(k))))
                return e;
        }
        return null;
    }
remove
    // 根据键删除键值对
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }
    final Entry<K,V> removeEntryForKey(Object key) {
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
		// 前驱节点
        Entry<K,V> prev = table[i];
		// 当前节点
        Entry<K,V> e = prev;
        // 遍历链表
        while (e != null) {
			// 当前节点的后继节点
            Entry<K,V> next = e.next;
            Object k;
			// 找到要删除的节点
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                // 要删除的结点是第一个结点,数组第一个元素指向当前节点的后继节点
                if (prev == e)
                    table[i] = next;
                else
                	// 将删除节点的前驱的后继链接到删除结点的后继节点
                    prev.next = next;
                e.recordRemoval(this);
                // 返回删除元素结点
                return e;
            }
			// 更新前驱和当前节点
            prev = e;
            e = next;
        }

        return e;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值