HashMap源码分析与实现

本文详细解析了HashMap的工作原理,包括其内部结构、构造函数、put和get方法,以及如何处理碰撞和扩容策略。探讨了HashMap如何利用数组、链表和红黑树存储数据,解释了扰动函数的作用和性能影响因素。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:Map实现类用于保存具有映射关系的数据,每项数据保存的都是key,value键值对。Map里面的key是不可重复的,key是用于标识集合里的每项数据,是由Set来组织起来的,通过Set也就意味着有去重功能。而Map里面的value是可以重复的,是通过Collection组织起来的。 

HashMap(Java8以前):数组+链表

数组的特点:查询速度快,增删较慢;链表的特点:查询速度慢,增删较快。

因此HashMap结合了两者的优势,同时HashMap的操作是非synchronized的,因此效率比较高,存储的内容是键值对映射。

HashMap(Java8及以后):数组+链表+红黑树

java8及以后是使用一个常量(TREEIFY_THRESHOLD)是否将链表转换成红黑树来存储它们,

来看看HashMap的内部结构:HashMap可以看作是数组 Node<k,v>[] table 链表来组成的复合结构,在Java8以前数组里面的元素叫Entry,无论是树还是链表,里面的元素都是节点。

Node的组成:

   注:next是指向下一个节点

 

HashMap的成员变量:

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

    // 序列号

    private static final long serialVersionUID = 362498820763181265L;   

    // 默认的初始容量是16

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  

    // 最大容量

    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认的填充因子

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 当桶(bucket)上的结点数大于这个值时会转成红黑树

    static final int TREEIFY_THRESHOLD = 8;

    // 当桶(bucket)上的结点数小于这个值时树转链表

    static final int UNTREEIFY_THRESHOLD = 6;

    // 桶中结构转化为红黑树对应的table的最小大小

    static final int MIN_TREEIFY_CAPACITY = 64;

    // 存储元素的数组,总是2的幂次倍

    transient Node<k,v>[] table;

    // 存放具体元素的集

    transient Set<map.entry<k,v>> entrySet;

    // 存放元素的个数,注意这个不等于数组的长度。

    transient int size;

    // 每次扩容和更改map结构的计数器

    transient int modCount;  

    // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容

    int threshold;

    // 填充因子

    final float loadFactor;

}

 

HashMap的构造函数:

// 默认构造函数。
HashMap()

// 指定“容量大小”的构造函数
HashMap(int capacity)

// 指定“容量大小”和“加载因子”的构造函数
HashMap(int capacity, float loadFactor)

// 包含“子Map”的构造函数
HashMap(Map<? extends K, ? extends V> map)

Put方法核心解读(添加元素):

put调用了putval ,当put调用,tabel为空的时候,就会调用resize()方法来初始化tab,继续进行hash运算,算出具体的tab位置,resize不仅可以初始化还可以扩容。

逻辑总结:

  1. 如果HashMap未被初始化,则初始化;
  2. 对key求hash值,再计算出tab的下标;
  3. 如果没有碰撞(tab数组里面对应的位置没有相应的键值对,则将键值对直接放入到相应的数组位置中),直接放进桶中;
  4. 如果有碰撞了(数组位置已经有元素了),就以链表的形式链接到后面;
  5. 判断链表的长度(一旦超过阈值,就会转化成红黑树);
  6. 判断链表长度低于6,就会把红黑树转化成链表;
  7. 如果桶需要扩容就调用resize()进行扩容;

Get方法解析:

当你传递一个key从HashMap总获取value的时候,对key进行null检查。如果key是null,table[0]这个位置的元素将被返回。key的HashCode()方法被调用,然后计算hash值。

indexFor(hash,table.length)用来计算要获取的Entry对象在table数组中的精确的位置,使用刚才计算的hash值。

在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
 
        return null == entry ? null : entry.getValue();
    }

要牢记以下关键点:

  1. · HashMap有一个叫做Entry的内部类,它用来存储key-value对。
  2. · 上面的Entry对象是存储在一个叫做table的Entry数组中。
  3. · table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。
  4. · key的hashcode()方法用来找到Entry对象所在的桶。
  5. · 如果两个key有相同的hash值,他们会被放在table数组的同一个桶里面。
  6. · key的equals()方法用来确保key的唯一性。
  7. · value对象的equals()和hashcode()方法根本一点用也没有。

 

如何减少碰撞?

  • 扰动函数:促使元素位置分布均匀,减少碰撞几率;
  • 使用final对象,并采用合适的equals()和hashCode()方法将会减少碰撞的发生;

 

影响HashMap性能的有两个参数:初始容量(initialCapacity) 和加载因子(loadFactor)。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。

1、key值可以为空吗?

hashMap把Null当做一个key值来存储,看源码

 public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //源码中判断了,如果key值未空的时候,没有返回错误信息,也是允许存储的
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        .............
}

 

2、如果Hash key重复了,那么value值会覆盖吗?

不会覆盖。在Entry类中,有个Entry< K,V > next 实例变量;它是来存储hashKey冲突时,存放就的value值。不会覆盖。上源码

  static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
       ......
   }
 public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        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 || key.equals(k))) {
                //这里实际上是先获取原来的value,保存老的备份,可以通过  xxx.get("Jack_wu").next.getValue()获取。
                V oldValue = e.value; // 假设原来的是30,传进来的是31 。 目前还是30
                //然后在把当前的value赋给它
                e.value = value;  // 31
                e.recordAccess(this);
                return oldValue; 
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

 

说说hashmap如何处理碰撞的,或者说说它的扩容?

先说碰撞吧,由于hashmap在存值的时候并不是直接使用的key的hashcode,而是通过扰动函数算出了一个新的hash值,这个计算出的hash值可以明显的减少碰撞。还有一种解决碰撞的方式就是扩容,扩容其实很好理解,就是将原来桶的容量扩为原来的两倍。这样争取散列的均匀,比如:原来桶的长度为16,hash值为1和17的entry将会都在桶的0号位上,这样就出现了碰撞,而当桶扩容为原来的2倍时,hash值为1和17的entry分别在1和17号位上,整号岔开了碰撞。

下面说说何时扩容,扩容都做了什么。

  1.7中,在put元素的过程中,判断table不为空、切新增的元素的key不与原来的重合之后,进行新增一个entry的逻辑。

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, bucketIndex);
    }

由源代码可知,在新增元素时,会先判断:

  1)当前的entry数量是否大于或者等于阈值(loadfactory*capacity);

  2)判断当前table的位置是否存在entry。

  经上两个条件联合判定,才会进行数组的扩容工作,最后扩容完成才会去创建新的entry。

  而扩容的方法即为:resize()

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值