Jdk1.7中HashMap结构及代码追踪

HashMap在JDK1.7版本中的数据存储结构实际上是一个 Entry<?, ?>[] EMPTY_TABLE数组

 static final Entry<?, ?>[] EMPTY_TABLE = {};

 // table就是HashMap实际存储数组的地方
 transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE;

 显而易见,其中的每个元素又是一个链表

    static class Entry<K, V> implements Map.Entry<K, V> {

        final K key;

        V value;

        Entry<K, V> next;

        int hash;
...//省略后续代码

因此,Java7 HashMap的结构大致如下图

 总结:简单来说,HashMap中的数据存储结构是个数组,而每个元素都是一个单向链表,链表中每个元素是一个Entry的内部类对象,每个对象包含四个属性:key,value,hash值,和用于单向链表的next

重要的成员变量:看一下其中的其他成员变量 

    // 默认的HashMap的空间大小16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认值是16

    // hashMap最大的空间大小
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // HashMap默认负载因子,负载因子越小,hash冲突机率越低
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //初始化的空数组
    static final Entry<?, ?>[] EMPTY_TABLE = {};

    // table就是HashMap实际存储数组的地方
    transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE;

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

    // 临界值(超过这个值则开始扩容),公式为(threshold = capacity * loadFactor)
    int threshold;

    // HashMap 负载因子
    final float loadFactor;

 构造方法:从源码中可以看出HashMap一共有个四个构造,他们分别为

 

    //1.默认构造,会调用默认默认空间大小16和默认负载因子0.75
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

 

  //2.指定大小但不指定负载因子
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    //3.指定大小和负载因子
    public HashMap(int initialCapacity, float loadFactor) {
        //此处对传入的初始容量进行校验,最大不能超过MAXIMUM_CAPACITY = 1<<30(2的30次方)
        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);
        this.loadFactor = loadFactor;

        threshold = initialCapacity;
        //init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现
        init();
    }
    //4.使用默认构造创建对象并将指定的map集合放入新创建的对象中
    public HashMap(Map<? extends K, ? extends V> m) {

        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,

                DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        //初始化数组
        inflateTable(threshold);
        //添加指定集合中的元素
        putAllForCreate(m);

    }

上面四个构造实际上都是在使用第三个构造方法:类中有几个比较重要的字段:

//实际存储的key-value键值对的个数
transient int size;

//当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
int capacity

//阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,
//threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到
int threshold;

//负载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;

//用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,
//如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException
transient int modCount;

 从源码中不难看出,实际上,在构造器中(第四个除外),并没有为数组分配内存空间,而是在put操作的时候才进行数据的构建

put操作 

下面看下put方法的执行过程 

  • 首先要判断数字是否为空数组,如果是空数组的话,需要对数组进行初始化
  • 如果key是null的话,回家元素的值仿佛唉table的0索引上,此时终止操作
  • 如果key值不是null的话
  1. 对key进行hash操作,获取到hash值
  2. 找到key对应的数组下标
  3. 获取到链表对象后遍历链表,看是否有重复的key存在,如果有直接覆盖并返回原来位置上的值,就此结束
  4. 如果不存在重复的key,将此该key和value组装程Entry对象添加到链表中(存在数组扩容问题-后面有介绍)
//put操作源码
 public V put(K key, V value) {
        // 当插入第一个元素的时候,需要先初始化数组大小
        if (table == EMPTY_TABLE) {
            // 数组初始化
            inflateTable(threshold);

        }
        // 如果 key 为 null,最终会将这个 entry 放到 table[0] 中
        if (key == null)

            return putForNullKey(value);
        // 1. 求 key 的 hash 值
        int hash = hash(key);
        // 2. 找到对应的数组下标
        int i = indexFor(hash, table.length);
        // 3. 遍历一下对应下标处的链表,看是否有重复的 key 已经存在,如果有,直接覆盖,put 方法返回旧值就结束了
        for (Entry<K, V> e = table[i]; e != null; e = e.next) {

            Object k;

            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // key -> value

                V oldValue = e.value;

                e.value = value;

                e.recordAccess(this);

                return oldValue;

            }
        }
        modCount++;
        // 4. 不存在重复的 key,将此 entry 添加到链表中
        addEntry(hash, key, value, i);
        return null;
    }

 数组初始化

   ps:在添加元素的开始,需要对数组是否初始化做判断,如果没有初始化需要做初始化处理

   保证数组大小是是2的N次方的好处:

当数组长度为2的n次幂的时候,
1、位移运算效率较高
2、不同的key的hash计算结果相同的几率较低,减少hash碰撞,使得数据在数组上分布的比较均匀,
   查询的时候就不用遍历某个位置上的链表,可以提升定位元素的的效率

 


    private void inflateTable(int toSize) {

        // Find a power of 2 >= toSize 保证数组大小一定是 2 的 n 次方。
        // new HashMap(519),大小是1024
        //将数组大小保持为2的n次方,在Java7和Java8的HashMap和 ConcurrentHashMap 都有相应的要求,实现代码略有不同
        int capacity = roundUpToPowerOf2(toSize);

        // 计算扩容阈值:capacity * loadFactor
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        // 初始化数组
        table = new Entry[capacity];

        initHashSeedAsNeeded(capacity);

    }
//    确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.
    static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        // 返回最接近临界值的2的N次方
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;

    }

 

 计算元素在数组的具体位置

//简单说就是取 hash 值的低 n 位。如在数组长度为 32 的时候,其实取的就是 key 的 hash 值的低 5 位,作为它在数组中的下标位置
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        // 简单理解就是hash值和长度取模
        return h & (length - 1);
    }

 添加到链表

  ps:找到数组位置之后,就需要对key进行判重处理,如果有的话,就覆盖重复key的值,返回旧值(判断重复逻辑为:可以的hash值相同,且原来key和当前key相等),如果没有重复值,将新值放在链表的表头

 

//主要逻辑为,先判断是否需要扩容,如果需要扩容就先扩容,最后再把数据封装程Entry对象加入到链表表头
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 如果当前 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经有元素了,那么要扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            // 扩容,容量 * 2
            resize(2 * table.length);
            // 扩容以后,重新计算 hash 值
            hash = (null != key) ? hash(key) : 0;
            // 重新计算扩容后的新的下标
            bucketIndex = indexFor(hash, table.length);

        }

        // 创建元素
        createEntry(hash, key, value, bucketIndex);

    }

    // 将新值放到链表的表头,然后 size++
    void createEntry(int hash, K key, V value, int bucketIndex) {

        Entry<K, V> e = table[bucketIndex];

        table[bucketIndex] = new Entry<>(hash, key, value, e);

        size++;

    }

数组扩容 

 长度为当前长度的2倍

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        // 如果之前的HashMap已经扩充到最大了,那么就将临界值threshold设置为最大的int值
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        // 新的数组
        Entry[] newTable = new Entry[newCapacity];
        // 将原来数组中的值迁移到新的更大的数组中
        transfer(newTable, initHashSeedAsNeeded(newCapacity));

        table = newTable;
        // 阈值计算
        threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

    }
    //数组的拷贝
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        // 遍历旧的数组
        for (Entry<K, V> e : table) {
            while (null != e) {
                //获取下一个entry对象
                Entry<K, V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //当前对象的hash值
                int i = indexFor(e.hash, newCapacity);
                //头插法,Entry对象放在新数组上第一位置,其他对象放在该对象的后一位置
                e.next = newTable[i];
                //将整体的对象放在指定的索引位置
                newTable[i] = e;
                //继续循环下一个Entry
                e = next;
            }
        }
    }

get方法跟踪

  • 根据key计算出key的hash值
  • 找到对应的数组下标
  • 遍历该数组下的链表,直到找到与之相等的key的值,或找不到返回null
    //获取数据
    public V get(Object key) {

        if (key == null)
            //如果key为null,就从table[0]获取(put中,key为null也是存储在该位置)
            return getForNullKey();
        Entry<K, V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }
//    从链表中查询数据
    final Entry<K, V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key);
        // 确定key对应的数组位置,遍历链表直至找到,或者最终找不到返回null
        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;

    }

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值