Java中HashMap的底层实现原理

HashMap基本结构

我们常见的两种数据结构:
数组:数据存储地址连续。查询快,寻址容易。但是插入删除困难
链表:数据散列存储。查询慢,但是增删快

上述两个结构各有优缺,HashMap 就是将这两种结构进行结合,即采用数组+链表的形式,其中,每一个数组中的值存放的是一个Entry类,属性有key(键),value(值),next(下一个)
HashMap 是基于哈希表的 Map 接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键在这里插入图片描述
部分源码如下:

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

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

从源码可以看出,HashMap 里实现一个静态内部类Entry,Entry就是数组中的元素,其重要的属性有 key , value, next,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,从而构成了链表。
HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。

HashMap 实现存储与读取

由上面我们知道,HashMap 将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对。
因此,当需要存储一个 Entry 对象时,会根据 hash 算法来决定其在数组中的存储位置,再根据equals() 方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据 equals() 方法从该位置上的链表中取出该Entry。

1. 存储:
部分源码如下:

public V put(K key, V value) {
    // HashMap允许存放null键和null值。
    // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
    if (key == null)
        return putForNullKey(value);
    // 根据key的keyCode重新计算hash值。
    int hash = hash(key.hashCode());
    // 搜索指定hash值在对应table中的索引。
    int i = indexFor(hash, table.length);
    // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
    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))) {
            // 如果发现已有该键值,则存储新的值,并返回原始值
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    // 如果i索引处的Entry为null,表明此处还没有Entry。
    modCount++;
    // 将key、value添加到i索引处。
    addEntry(hash, key, value, i);
    return null;
}

由源码可以看出,当 HashMap 需要存储一个 key-value 对时,会根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置(即这个元素在数组中的下标),如果数组该位置上已经存放有其他元素了(即hashCode() 返回值相同),那么,若这两个 Entry 的 key 通过 equals() 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。若这两个 Entry 的 key 通过 equals 比较返回 false,在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

其中:

  • hashCode() 函数:
    将想存放在 HashMap 中的 key 转化为数组中的index,最后再对数组长度取余
    int hash = key.hashCode();
    int index = hash%num.length;
    其中,取余的目的时,如果 hashCode() 得到的 index 较大的话,则需要较大的数组空间,所以进行取余,便于将较大的数据放在较小的数组空间中
  • equal() 函数:
    equal() 函数在 HashMap 中被重写了,而且如果要重写 equal() 必须重写 hashCode(),通过这两个函数来确定hashMap中存入键值对的唯一性。
    hashmap中约定,如果两个对象 equal() 相等,那么这两个对象产生的 hashcode() 也应该相等。若 equal() 不等,hashcode() 可能相等。
    hashcode() 是根据对象的地址空间取计算的,String等很多对象都是重写的 equal() 方法,变成了值相等,所以如果你不重写,那么 equal() 出来的对象可能是一样的,但是地址空间却又不一样。这与 HashMap 中的约定相矛盾。

2. 读取

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    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.equals(k)))
            return e.value;
    }
    return null;
}

从 HashMap 中get元素时,首先计算 key 的 hashCode,找到数组中对应位置的某一元素,然后通过 key 的 equals() 方法在对应位置的链表中找到需要的元素。

解决 hash 冲突

哈希函数的设计至关重要,好的哈希函数会尽可能地保证计算简单和散列地址分布均匀。若两个不同的元素,通过哈希函数得出的实际存储地址相同,也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,这就是所谓的哈希冲突,也叫哈希碰撞。

解决 hash 冲突的方法有:

  • 开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
  • 再哈希法
  • 链地址法
  • 建立一个公共溢出区

其中,HashMap 的解决冲突办法为:链地址法

再散列resize过程

当 HashMap 中的元素越来越多的时候,碰撞的几率也就越来越高,所以为了提高查询的效率,当哈希表的容量超过默认容量时,必须调整 table 的大小。需要通过 resize 对 HashMap 的数组进行扩容。创建一张新表,容量扩大至原数组的两倍,原数组中的数据必须重新计算其在新数组中的位置,映射到新表中。

/**
     * Rehashes the contents of this map into a new array with a
     * larger capacity.  This method is called automatically when the
     * number of keys in this map reaches its threshold.
     *
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.
     *
     * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    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);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

 

    /**
     * Transfers all entries from current table to newTable.
     */
    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) {
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;
                    //重新计算index
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值