Java重点源码回顾——HashMap1.7

1. 概述

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

HashMap在我们的日常使用中非常多,所以今天来阅读下它的源码,了解它具体的设计思想,能够帮助我们扩宽视野。

HashMap两个主要的版本是1.7和1.8,都是线程不安全的。1.8版本是在1.7版本的基础上引入了红黑树等优化,提升整体的效率。今天我们先来简单了解下HashMap1.7版本的源码。它的主要特点总结如下:
在这里插入图片描述

在HashMap1.7中,并没有引入红黑树。所以采用的方式还是拉链法。
在这里插入图片描述

2. 成员变量

因为HashMap1.7是采用拉链法存储数据的,所以需要将key-value键值对封装成一个节点类,如下所示:

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;  // 存储key
        V value;  // 存储value
        Entry<K,V> next;  // next指针,又来指向下一个节点
        final int hash;  // hash值
    }

从代码上可以看到,实现方法是使用静态内部类Entry来充当节点类的。next指针是当出现哈希冲突的时候,用来指向下一个节点的指针。

我们再看看其它的成员变量:

    // 默认初始化容量
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    // HashMap能够存储的最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认负载因子,用来指示元素个数占整体容量的比例
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 用来存储键值对的Entry数组
    transient Entry[] table;

    // 存储实际元素个数
    transient int size;

    // 阈值=容量 * 负载因子。当超过这个数字,说明可能哈希冲突严重,需要扩容
    int threshold;

    // 实际的负载因子
    final float loadFactor;

    // 更新操作计数器,线程不安全时进行fail-fast机制
    transient int modCount;

成员变量需要注意的地方主要有两点:

  1. HashMap的容量在任何时刻都是2的n次幂,包括在扩容的时候也是扩容为原来的2倍。这主要是因为当容量为2的n次幂的时候,可以减少哈希碰撞的概率。具体可见HashMap初始容量为什么是2的n次幂及扩容为什么是2倍的形式
  2. 阈值threshold=容量 * 负载因子,当超过阈值,可能有比较严重的哈希冲突,需要进行扩容。

3. 构造方法

    // 两个参数的构造方法
    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);

        // 传入的容量并不一定是2的n次幂,所以要找到最小的二次幂容量
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
        // 实际负载因子
        this.loadFactor = loadFactor;
        // 阈值
        threshold = (int)(capacity * loadFactor);
        // 根据容量创建数组
        table = new Entry[capacity];
        init();
    }

    // 一个参数的构造方法,使用默认负载因子0.75
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    // 无参构造方法,使用默认容量16,默认负载因子0.75
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }

    // Nap迁移
    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
    }

可以看到,有4个构造方法,会使用2的n次幂来计算初始容量,并通过负载因子计算阈值。

4. put方法

    public V put(K key, V value) {
        // 如果key为null,单独处理
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());  // 计算hash值
        int i = indexFor(hash, table.length);  // 计算位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 相同的key进行覆盖
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;  // 更新次数+1
        addEntry(hash, key, value, i);  // 没有找到相同的
        return null;
    }

    // 如果key为null
    private V putForNullKey(V value) {
        // key为null的键值对,默认存放在0号位置
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            // 找到相同的key进行覆盖
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0); // 没有找到相同的
        return null;
    }

    // 扰动函数
    static int hash(int h) {
        // 通过位运算进行扰动,减少哈希冲突
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    // 计算Entry在数组中的位置
    static int indexFor(int h, int length) {
        // 和长度-1进行与运算,相当于进行模n计算。
        return h & (length-1);
    }

    // 添加Entry
    void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];  // 获取数组中bucketIndex的位置
        // 头插法插入元素
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        // 插入阈值就进行扩容,为2倍原来的大小
        if (size++ >= threshold)
            resize(2 * table.length);
    }

在上面的代码中,我们可以看到HashMap1.7中,是对于key为null的情况单独处理,放在
数组中0的位置上。我们对put方法的流程进行下总结

  1. 如果key为null,则单独处理。否则进入下一步;
  2. 通过扰动函数计算哈希值,并计算出要插入的位置i;
  3. 如果在i位置上找到了相同的key,则将value进行覆盖;如果没有找到相同的key,则创建新的Entry,利用头插法进行插入;
  4. 如果发现Entry个数超过阈值,则进入扩容,扩大容量为原来的2倍。

5. resize方法

    // 扩容的方法
    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];  // 创建新的数组
        transfer(newTable);  // 迁移旧的Entry到扩容后的数组中
        table = newTable;   // 将table指向新的数组
        threshold = (int)(newCapacity * loadFactor);  // 重新计算阈值
    }

    // Entry进行迁移
    void transfer(Entry[] newTable) {
        Entry[] src = table;   // 旧数组
        int newCapacity = newTable.length;   // 新的容量
        for (int j = 0; j < src.length; j++) {  // 遍历旧数组中的每个桶
            Entry<K,V> e = src[j];  // 桶中的第一个元素
            if (e != null) {   // 如果不为null
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;  // 先保存下一个要transfer的Entry
                    int i = indexFor(e.hash, newCapacity); // 计算新数组中的位置
                    e.next = newTable[i];  // 头插法插入元素
                    newTable[i] = e;  // 更新新数组中i位置第一个元素为e
                    e = next;  // e更新为旧数组中i位置下一个元素为e
                } while (e != null);
            }
        }
    }

在HashMap1.7扩容方法中,是将容量扩大为原来的2倍。扩容的过程如下:

  1. 创建2倍容量的新数组;
  2. 遍历旧数组中的每个桶,再遍历每个桶中的Entry,重新计算Entry在新数组中的位置,然后利用头插法的方式插入元素。

6. get方法

    // get元素
    public V get(Object key) {
        // key为null单独处理
        if (key == null)
            return getForNullKey();
        // 根据key计算hash值
        int hash = hash(key.hashCode());
        // 遍历桶中的Entry
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            // 如果发现相同的key,就返回value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

    // 如果key为null,就去数组中0位置去查找
    private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

get方法就比较简单,先根据hash值计算出在数组中的位置,如果发现相同的key的Entry,就返回值。

7. remove方法

    // remove方法
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }

    // 根据key删除Entry
    final Entry<K,V> removeEntryForKey(Object key) {
        // 计算hash值
        int hash = (key == null) ? 0 : hash(key.hashCode());
        // 计算数组中的位置
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];  // 前驱节点
        Entry<K,V> e = prev;  // 当前节点

        // 遍历桶中所有的Entry
        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            // 如果发现key相同的Entry
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;  // 元素个数-1
                if (prev == e)  // 如果删除的是头节点
                    table[i] = next;  // 那么下个节点就作为新的头节点
                else  // 删除的是中间节点
                    prev.next = next;  // 前驱节点指向下一个节点
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }

remove方法也比较简单,通过hash值计算出在数组中桶的位置,然后遍历桶中的Entry,找到相同的key就删除。

8. 扩容出现死循环

HashMap1.7是使用头插法插入节点的,在进行扩容调用resize方法,进而调用transfer方法迁移元素的时候,如果多线程并发,就有可能出现链表死循环的问题。具体描述可见Java:手把手带你源码分析 HashMap 1.7

参考文章:
Java:手把手带你源码分析 HashMap 1.7
为什么HashMap会产生死循环
HashMap1.7 最最最最最详细源码分析

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值