Java中的HashMap源码详解

 更新时间:2023年09月06日 09:45:57   作者:李思苇  
这篇文章主要介绍了Java中的HashMap源码详解,当我们确切知道HashMap将要处理的数据量为n时,推荐调用构造函数public HashMap(int initialCapacity)来创建 HashMap,这样就不会发生扩容,需要的朋友可以参考下

HashMap

当我们确切知道HashMap将要处理的数据量为n时,推荐调用构造函数public HashMap(int initialCapacity)来创建 HashMap,这样就不会发生扩容。

以上构造函数并没有直接将table数组的大小设置为给定的initialCapacity参数的值n,但是会设定阈值threshold为大于用户给定的n的2的乘方的最小值(例如,假如参数initialCapacity的值是13-16中的任意一个值,threshold都会是16)。

而扩容的条件会分两种情况:

  • 当我们调用了如上构造函数时,扩容只在size()>=threshold时发生,只要我们确认实际的数据量不会大于在构造函数中传给参数initialCapacity的值,那么扩容就不会发生。
  • 如果我们调用的是无参构造函数,那么扩容会发生在size()>capacity*loadRefactor时。

Map接口

keySetvaluessizecontainsKeyputremove
entrySetisEmptycontainsValuegetclear

以下为部分JDK1.8添加的默认方法,default

getOrdefault(Object o,V v)replaceAll(BiFunction<K,V,V> f)remvoe(K k,V v)
forEach(BiConsumer<K,V> c)putIfAbsent(K k,V v)replace

Map.Entry接口

此接口是定义在Map接口内部的static的接口

getKeysetValuecomparingByKeycomparingByKey(Comparator c)
getValueequalscomparingByValuecomparingByValue(Comparator c)

源码解析

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

定义一些默认值

// table的初始大小默认值,即桶个数,必须是2的乘方
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	// table的大小的最大值,即桶数的最大值
    static final int MAXIMUM_CAPACITY = 1 << 30;
	// 负载因子,建议0.5-1.5
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当某个桶内元素个数大于等于此数,并且桶数大于64时,会将桶内元素的存储结构由单链表改为红黑树
    static final int TREEIFY_THRESHOLD = 8;
    // 当桶内元素个数小于或者等于此数时,会将桶内元素的存储结构由红黑树改为单链表
    static final int UNTREEIFY_THRESHOLD = 6;
     // 若table的大小小于MIN_TREEIFY_CAPACITY 时,即便某个桶内的元素个数达到了TREEIFY_THRESHOLD 后,也并不会对这个桶做树化操作,而是对map进行扩容resize()
    static final int MIN_TREEIFY_CAPACITY = 64;

定义内部类:封装链表的节点

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;
        }
        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

定义实例域

注意

  • 并没有定义一个capacity实例域来指明table数组的大小,尽管类中定义了一个静态常量DEFAULT_INITIAL_CAPACITY。
  • table数组的大小是在初始化时确定的:参看resize()方法。
  • 所有实例域的访问控制都是默认的
 /* ---------------- Fields -------------- */
	// 装桶的数组,存储每个桶内的单链表的头结点或者树的根节点
    transient Node<K,V>[] table;
    transient Set<Map.Entry<K,V>> entrySet;
    // Map中当前实际存储的元素个数
    transient int size;
	// 每次remove,add等都会++modCount,当并发时,发现自己的modCount不是原来的了,就会抛出异常,表示并行修改失败
    transient int modCount;
	// 阈值:当map中的元素个数大于等于threshold时,会触发resize()操作进行扩容。
    int threshold;
	// 负载因子:当用户调用的默认无参构造函数、或者map自动扩容时,新的threshold=新table的capacity*loadFactor;
    final float loadFactor;

定义构造函数

注意:

如果用户确切知道将要处理的数据量为capacity,则可以调用构造函数public HashMap(int initialCapacity) ,此构造函数会设定阈值threshold为大于用户给定的capacity的2的乘方的最小值。

因此用户在主动设定capacity后不必担心自动扩容问题,因为扩容只会在实际数据量>=threshold时发生,而此种情况下threshold>=用户设定的capacity会一定成立。 参见:tableSizeFor()方法

  /* --------------------------构造函数,不会初始化table数组,table数组只有在首次调用put方法时才会被初始化------ */
    // 默认构造函数,只设置了负载因子的默认值
   public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
	public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }    
    // 初始化loadFactor 、threshold 的值
    // threshold = 大于initialCapacity的最小的2乘方(如,15 ->16 ,13->16)
 public HashMap(int initialCapacity, float loadFactor) {
       ...
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    // 计算出大于cap的最小的2的乘方
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;  // 00001 0010 | 0000 1001 -> 11011
        n |= n >>> 2;  //  0001 1011 | 0000 0110 -> 11111
        n |= n >>> 4;  //  0001 1111 | 0000 0001 -> 11111
        n |= n >>> 8;  //  
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

put方法

  • 如果table还未被初始化,则调用resize()进行初始化
  • 如果散列到桶中之后,桶内元素个数>=TREEIFY_THRESHOLD ,则调用treeifyBin()方法检查是否要将桶改为红黑树结构
  • 如果散列到Map中之后,Map中元素个数>=threshold了,则调用resize()方法进行扩容
    /* ------------------------------put方法------------------------------------------ */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab;  // 
        Node<K,V> p;   // 头结点
        int n, i;  // n:table的大小。i:新节点的桶号
        // 如果还未被初始化过,则调用resize(); HashMap在首次调用put方法之前,是不会初始化table的,因为那样的话会浪费一块连续内存。
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 根据key的哈希值,确定桶号,如果桶中还没有元素,则直接将其作为头结点存储到桶中
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        // 获取到了头结点
        else {
            Node<K,V> e; // 目标节点)
            K k;
            // 如果头结点的key和新节点的key相同,则头结点即为要被取代的目标节点
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果头结点是TreeNode类型的,则调用putTreeVal方法将新节点插入,并返回插入后的目标节点
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
			// 桶还未满
            else {
                for (int binCount = 0; ; ++binCount) {
                	// 目标指针指向链表中的下一个元素
                    if ((e = p.next) == null) {
                    	// 如果没有找到key相同的节点,就直接追加到链表尾部
                        p.next = newNode(hash, key, value, null);
                        // 如果桶中元素数是达到了设定的变树阈值(默认值8),则需要将桶内元素的存储结构更新为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果链表中有和新节点的key相同的元素
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // map中有与新节点的key相同的元素,那么根据条件做一些操作,就返回
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // onlyIfAbsent 是方法参数,表示只有不存在相同key的节点时,才进行更新操作,如果有相同节点,则不做任何操作。
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);  //此处是留给LinkedHashMap用的。
                return oldValue;
            }
        } // else结束
        // 更新了hashMap,就执行++modCount;
        ++modCount;
        // 当map中的元素数量大于阈值,就要扩容再散列
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

对table数组初始化 \ 扩容

resize()方法内有两种逻辑:

  • 一种是当前table为null时,会对table进行初始化操作;
  • 一种是当前table非null,会对table进行扩容操作;
    final Node<K,V>[] resize() {
    	// 当前table
        Node<K,V>[] oldTab = table;
        // 当前table的大小
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 当前的阈值
        int oldThr = threshold;
        int newCap, newThr = 0;
        // oldCap>0,说明是要做扩容操作
        if (oldCap > 0) {
	        // 如果原本的table的大小已经是最大值,无法继续扩容,直接退出
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 扩容:设置新的table的大小为原来的2倍,新的threshold也为原来的2背
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        // oldCap<= 0,说明table还未被初始化过,要进行初始化table的操作;oldThr>0,说明用户调用的有参构造函数,设置了threshold;直接将根据用户参数计算出的阈值设定为table的大小
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        // oldCap<= 0,说明是要进行初始化table的操作;oldThr<0,说明用户调用的默认无参构造函数;则将各个域变量的值设置为默认值。
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 如果经过以上设置,newThr 仍为0,(什么情况下会出现?)
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
		// 无论是要初始化table,还是要对table进行扩容,经过以上逻辑,都已经确定了要新创建的table的大小、threshold 。
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        // 如果当前table不为null,说明需要进行扩容操作
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

到此这篇关于Java中的HashMap源码详解的文章就介绍到这了,更多相关HashMap源码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 在 Spring Boot 中集成 MinIO 对象存储

    在 Spring Boot 中集成 MinIO 对象存储

    MinIO 是一个开源的对象存储服务器,专注于高性能、分布式和兼容S3 API的存储解决方案,本文将介绍如何在 Spring Boot 应用程序中集成 MinIO,以便您可以轻松地将对象存储集成到您的应用中,需要的朋友可以参考下
    2023-09-09
  • 总结十个实用但偏执的Java编程技术

    总结十个实用但偏执的Java编程技术

    Java是世界上最流行的程序语言,从1995年问世以来,Java的生态系统在一直在蓬勃的发展着。下面这篇文章主要总结了十个实用但偏执的Java编程技术,需要的朋友可以参考借鉴,下面来一起学习学习吧。
    2017-01-01
  • 解决idea启动报错javax.imageio.IIOException的问题

    解决idea启动报错javax.imageio.IIOException的问题

    这篇文章主要介绍了idea启动报错javax.imageio.IIOException,解决打不开idea问题,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-09-09
  • Java代码注释规范详解

    Java代码注释规范详解

    代码附有注释对程序开发者来说非常重要,随着技术的发展,在项目开发过程中,必须要求程序员写好代码注释,这样有利于代码后续的编写和使用。下面给大家分享java代码注释的规范,需要的朋友参考下
    2016-02-02
  • SpringBoot自动配置源码深入刨析讲解

    SpringBoot自动配置源码深入刨析讲解

    这篇文章主要介绍了SpringBoot自动配置原理分析,SpringBoot是我们经常使用的框架,那么你能不能针对SpringBoot实现自动配置做一个详细的介绍。如果可以的话,能不能画一下实现自动配置的流程图。牵扯到哪些关键类,以及哪些关键点
    2022-09-09
  • MyBatis实现递归查询的方法详解

    MyBatis实现递归查询的方法详解

    在项目开发过程中,往往会遇到多级菜单、分类等多层级结构数据的查询。本文就来为大家讲讲MyBatis实现递归查询的方法,感兴趣的可以动手尝试一下
    2022-08-08
  • 实现Servlet程序的三种方法(小结)

    实现Servlet程序的三种方法(小结)

    这篇文章主要介绍了实现Servlet程序的三种方法(小结),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • 东八区springboot如何配置序列化

    东八区springboot如何配置序列化

    本文主要介绍了东八区springboot如何配置序列化,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • Spring中TransactionSynchronizationManager的使用详解

    Spring中TransactionSynchronizationManager的使用详解

    这篇文章主要介绍了Spring中TransactionSynchronizationManager的使用详解,TransactionSynchronizationManager是事务同步管理器,监听事务的操作,来实现在事务前后可以添加一些指定操作,需要的朋友可以参考下
    2023-09-09
  • Eclipse+Webservice简单开发实例

    Eclipse+Webservice简单开发实例

    这篇文章主要介绍了Eclipse+Webservice简单开发实例的相关资料,需要的朋友可以参考下
    2016-02-02

最新评论