Java中的HashMap集合源码详细解读

 更新时间:2023年11月15日 10:29:48   作者:缘之妙不可言  
这篇文章主要介绍了Java中的HashMap集合源码详细解读,hash表是一种数据结构,它拥有惊人的效率,它的时间复杂度低到接近O(1)这样的常数级,需要的朋友可以参考下

什么是Hash?

hash表是一种数据结构,它拥有惊人的效率,它的时间复杂度低到接近O(1)这样的常数级。

hash表的实现主要是:

1.计算存储位置的hash函数。

2.处理哈希冲突的方法。

3.hash的物理存储。

hash函数

它的目的是通过一个key选出(映射)一个唯一的存储地址。

最常见的hash函数:f(key)=a*key+b 这里a,b为常数(不为0),f(key)就是计算出的哈希值 一般一个hash函数的设计好坏,直接影响到效率。

哈希冲突

hash冲突定义:当两个key相同时计算的hash值会一样,导致冲突。

hash冲突解决部分方法: **开放定地址:**找下一块地址(另一个hash表(另一个未用过的hash值·),无限多) **再散列函数法:**一个hash函数解决不了,就两个,两个不行就三个… **链地址法(hash桶(此处链表)的概念):**数组+链表,将具有相同hash的同义词按次序放入链表,并链表存在数组。链表的hash值以数组hash值开始确定新的hash(不担心与其它数组的hash相同) 还有的就不做介绍了

物理存储

物理存储结构:顺序|链式存储 hash表的主干永远都是一个数组 一般的hash只需要一个数组来存储,一般以数组下标做hash值。 在链地址法中需要用到链表。

什么是Map?

Map在计算机概念是一个key-value(唯一性)键值对

什么是HashMap?

根据前面的铺垫, hashMap的存储主干是一个数组(源码中的Node(有些是Entry)对象数组), Node(Entry)对象包含了Key-Value属性

hashMap处理hash冲突:java1.8后,使用的是链地址法。

数据存储结构如下:

在这里插入图片描述

在拥有链表的情况下,hashMap的查询效率必然是低一些的,复杂度提高到o(n) 但1.8利用了红黑树数据结构,又将复杂度降为了o(Log(n))

HashMap的源码分析

构造函数(4个)

部分成员变量:

  • transient 关键字:再被修饰后变量序列化将不可见
  • threshold:初始空间(initialCapacity传入参数)
  • loadFactor:负载因子 modCount:是快速失败的判断标准(在迭代时,其他线程访问Map,并导致其结构改变,会抛异常)
  • table:节点Set集合(HashMap主干,是个数组)
  • initialCapacity默认为16,loadFactory默认为0.75

负载因子在0.75时效率最好(数学统计学验证),用于衡量空间利用的方法选择

hashMap会扩容且容量永远是2的幂

	合理判断参数,就结束构造
 	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);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

 	 合理判断空间大小参数,使用默认负载参数就结束构造
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

 	 /使用默认参数就结束构造
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

	构造一个具有相同的映射关系与指定Map一个新的HashMap。 HashMap中与默认负载因数(0.75)和初始容量足以容纳在指定的地图的映射创建
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

发现此时table变量未分配空间。根据put函数源码发现,table变量在put()分配空间

再看关键对象Node(Entry)

	
	//Map.Entry<K,V>是一个接口
	//Node
    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;
        }
        //other
        }

再看其部分关键函数 hash() : key.hashCode()产生依靠equals()方法,位移异或等操作的哈希值 ,这个根据版本不同也计算方法不同,但是hashCode()几乎一直存在 put() : 计算hash值然后对table

	/*单位计算key.hashCode()和差(异或)的散列以降低的较高位。
	 因为表使用功率的两掩蔽,套散列的,只有在当前的掩模将总是碰撞上述位变化。 
	 (其中著名的例子是一组浮动键的小桌子控股连续整数)。
	 所以我们施加向下变换利差较高位的影响。 
	 有速度,实用,和位传播质量之间的权衡。 
	 由于哈希许多共同的集已合理分配(所以不要蔓延受益),因为我们用树来处理大型成套碰撞的垃圾箱,
	 我们只是XOR一些最便宜的方式转移位降低系统lossage,
	 以及纳入,否则将永远不会因为表界的指数计算中使用的最高位的影响。
	 */

	 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
 	public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            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) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //快速失败
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

JDK1.8在JDK1.7的基础上针对增加了红黑树来进行优化。即当链表超过8时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。

看上面的put函数的putVal()就含有红黑树的插入方法, 一开始普通的链表不需要红黑树。 下列源码可以看出, 转换红黑树条件是 链表(是在每次遍历到数组时每个单元对于的链表,参考上图)节点数大于等于8且数组长度大于等于64,

详见方法:treeifyBin();

//判断处理方法
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
//红黑树构造类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;//是红节点吗
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
        }

HashMap源码探究到此为止,以后可以继续深入,学习红黑树的实际应用等等

在此附上一张网上Copy过来的图: 是讲1.8版本的HashMap在增加元素后一系列的操作步骤,以及优化方式流程图

在这里插入图片描述

集合源码魅力无穷。 踏踏实实读源码。

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

相关文章

  • 定义hashcode时使用31系数的原因

    定义hashcode时使用31系数的原因

    这篇文章主要介绍了定义hashcode时使用31系数的原因,具有一定借鉴价值,需要的朋友可以参考下
    2018-01-01
  • 基于Java回顾之I/O的使用详解

    基于Java回顾之I/O的使用详解

    我计划在接下来的几篇文章中快速回顾一下Java,主要是一些基础的JDK相关的内容
    2013-05-05
  • 如何使用mybatis-plus实现分页查询功能

    如何使用mybatis-plus实现分页查询功能

    最近在研究mybatis,然后就去找简化mybatis开发的工具,发现就有通用Mapper和mybatis-plus两个比较好的可是使用,可是经过对比发现还是mybatis-plus比较好,下面这篇文章主要给大家介绍了关于如何使用mybatis-plus实现分页查询功能的相关资料,需要的朋友可以参考下
    2022-06-06
  • springboot项目使用nohup将日志指定输出文件过大问题及解决办法

    springboot项目使用nohup将日志指定输出文件过大问题及解决办法

    在Spring Boot项目中,使用nohup命令重定向日志输出到文件可能会使日志文件过大,文章介绍了两种解决方法:一是创建脚本直接清除日志文件,二是创建脚本保留部分日志内容,并将这些脚本加入定时任务中,这可以有效控制日志文件的大小,避免占用过多磁盘空间
    2024-10-10
  • Java 导出 CSV 文件操作详情

    Java 导出 CSV 文件操作详情

    这篇文章主要介绍了Java导出CSV文件操作详情,文章通过导入坐标展开详细内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-08-08
  • JAVA中的延迟队列DelayQueue应用解析

    JAVA中的延迟队列DelayQueue应用解析

    这篇文章主要介绍了JAVA中的延迟队列DelayQueue应用解析,DelayQueue是一个根据元素的到期时间来排序的队列,而并非是一般的队列那样先进先出,最快过期的元素排在队首,越晚到期的元素排得越后,需要的朋友可以参考下
    2023-12-12
  • 浅析Java 反射机制的用途和缺点

    浅析Java 反射机制的用途和缺点

    这篇文章给大家分析了Java 反射机制的用途和缺点以及相关知识点内容,有兴趣的朋友可以参考学习下。
    2018-07-07
  • spring cloud如何修复zuul跨域配置异常的问题

    spring cloud如何修复zuul跨域配置异常的问题

    最近的开发过程中,使用spring集成了spring-cloud-zuul,在配置zuul跨域的时候遇到了问题,下面这篇文章主要给大家介绍了关于spring cloud如何修复zuul跨域配置异常的问题,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-09-09
  • 使用maven的profile构建不同环境配置的方法

    使用maven的profile构建不同环境配置的方法

    这篇文章主要介绍了使用maven的profile构建不同环境配置的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • Java Web关键字填空示例详解

    Java Web关键字填空示例详解

    最近在工作中使用了java web,发现有些难度,下面这篇文章主要给大家介绍了关于Java Web关键字填空的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-04-04

最新评论