Java的HashMap源码解析

 更新时间:2023年11月15日 10:11:55   作者:龙三丶  
这篇文章主要介绍了Java的HashMap源码解析,HashMap是一个用于存储Key-Value键值对的集合,每一个键值对是一个Node,后台是用一个Node数组来存放数据,这个Node数组就是HashMap的主干,需要的朋友可以参考下

前言

以jdk1.8为例,HashMap是一个用于存储Key-Value键值对的集合,每一个键值对是一个Node(jdk1.7叫做Entry)。后台是用一个Node数组来存放数据,这个Node数组就是HashMap的主干。

这里我们主要来分析HashMap的get和put方法。

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;
		//如果是第一次put,就进行数组的大小初始化,默认是16
		if ((tab = table) == null || (n = tab.length) == 0)
			n = (tab = resize()).length;
		//根据hash值,找到在数组中的位置,如果此位置没有值,就new一个新的node插入
		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;
			//若果该节点是红黑树,则调用红黑树相关方法
			else if (p instanceof TreeNode)
				e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
			//到了这里,说明该节点是链表,调用链表相关方法
			else {
				for (int binCount = 0; ; ++binCount) {
					//循环到最后一个节点,然后插入新节点(1.7是往头结点插入,1.8是往尾部插入)
					if ((e = p.next) == null) {
						p.next = newNode(hash, key, value, null);
						//判断插入后的该链表的长度,如果大于8,就转成红黑树
						if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
							treeifyBin(tab, hash);
						break;
					}
					//这里表示链表中某个节点的key与即将插入的key相等,就跳出循环等待覆盖
					if (e.hash == hash &&
							((k = e.key) == key || (key != null && key.equals(k))))
						break;
					p = e;
				}
			}
			//这里表示有节点的key与新的key相等,那么就覆盖
			if (e != null) {
				V oldValue = e.value;
				if (!onlyIfAbsent || oldValue == null)
					e.value = value;
				afterNodeAccess(e);
				return oldValue;
			}
		}
		++modCount;
		//插入完之后,如果导致size超过了预设的阈值,就进行扩容(1.7是插入前判断,1.8是插入后判断)
		if (++size > threshold)
			resize();
		afterNodeInsertion(evict);
		return null;
	}

扩容步骤:

1、创建一个原数组两倍大小的新数组,并且把阈值扩大一倍。

2、遍历原数组,进行数据迁移。分为红黑树和链表两种情况。

 好了,扩容部分就不展开代码详细说明,接下来进入get方法,相较于put方法就没那么复杂了,且代码量也比较少

get

public V get(Object key) {
		Node<K,V> e;
		return (e = getNode(hash(key), key)) == null ? null : e.value;
	}
	final Node<K,V> getNode(int hash, Object key) {
		Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
		//判断底层node数组是否为空及该hash值对应的数组位置是否有值
		if ((tab = table) != null && (n = tab.length) > 0 &&
				(first = tab[(n - 1) & hash]) != null) {
			//判断该数组的节点是不是我们需要的值,是就直接返回
			if (first.hash == hash &&
					((k = first.key) == key || (key != null && key.equals(k))))
				return first;
			if ((e = first.next) != null) {
				//该节点是红黑树则直接调用红黑树的遍历方法
				if (first instanceof TreeNode)
					return ((TreeNode<K,V>)first).getTreeNode(hash, key);
				//遍历链表
				do {
					if (e.hash == hash &&
							((k = e.key) == key || (key != null && key.equals(k))))
						//是我们需要的值,返回
						return e;
				} while ((e = e.next) != null);
			}
		}
		return null;
	}

注意:

1、HashMap底层就是用一个个的Node来存储单个数据,每个Node有hash值、key、value、及指向下一个Node的引用(next)。Node数组中就是所有链表的头节点。

2、当出现hash冲突的情况,原Node的next就会指向新插入的Node,也就是形成了链表。

3、每次扩容的长度必须是2的幂,因为,根据key的hash值计算出的数组索引应尽量不要重复,实现均匀分布,均匀分布的话大部分查找的数据都是以数组的形式查找,就不会蜕变成链表,而数组的查找效率比链表高很多。

4、影响扩容的因素有两个:数组的长度(DEFAULT_INITIAL_CAPACITY)和负载因子(DEFAULT_LOAD_FACTOR),当这两个相乘大于等于当前HashMap的Size时,就进行扩容

5、扩容在并发情况下可能会形成链表环,存在并发安全问题,这点需要注意

6、当链表的节点超过8个时,会转成红黑树,链表的时间复杂度为O(n),而红黑树为O(logn)

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

相关文章

  • Java并发编程之创建线程

    Java并发编程之创建线程

    这篇文章主要介绍了Java并发编程中创建线程的方法,Java中如何创建线程,让线程去执行一个子任务,感兴趣的小伙伴们可以参考一下
    2016-02-02
  • 使用maven-archetype-plugin现有项目生成脚手架的方法

    使用maven-archetype-plugin现有项目生成脚手架的方法

    这篇文章主要介绍了使用maven-archetype-plugin现有项目生成脚手架的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • Java线程创建的四种方式总结

    Java线程创建的四种方式总结

    这篇文章主要介绍了Java线程创建的四种方式,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-09-09
  • 谈谈Java中的守护线程与普通线程

    谈谈Java中的守护线程与普通线程

    这篇文章主要介绍了Java中的守护线程与普通线程,帮助大家更好的理解和学习Java 多线程,感兴趣的朋友可以了解下
    2020-09-09
  • 在Java中使用日志框架log4j的方法

    在Java中使用日志框架log4j的方法

    Log4j有三个主要的组件/对象:Loggers(记录器),Appenders (输出源)和Layouts(布局)。这里可简单理解为日志类别,日志要输出的地方和日志以何种形式输出,今天通过本文给大家分享Java日志框架log4j的相关知识,感兴趣的朋友一起看看吧
    2021-08-08
  • 使用@Validated 和 BindingResult 遇到的坑及解决

    使用@Validated 和 BindingResult 遇到的坑及解决

    这篇文章主要介绍了使用@Validated 和 BindingResult 遇到的坑及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-10-10
  • JDK完全卸载超详细步骤

    JDK完全卸载超详细步骤

    这篇文章主要给大家介绍了关于JDK完全卸载超详细步骤的相关资料,在安装JDK之前,最好将原来可能安装过的JDK卸载掉,以免影响到新JDK的使用,需要的朋友可以参考下
    2023-08-08
  • java中对象转json字符串的几种常用方式举例

    java中对象转json字符串的几种常用方式举例

    这篇文章主要给大家介绍了关于java中对象转json字符串的几种常用方式,在Java中可以使用许多库将对象转换为JSON字符串,其中最常用的是Jackson和Gson,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2023-10-10
  • Java面试题之MD5加密的安全性详解

    Java面试题之MD5加密的安全性详解

    MD5 是 Message Digest Algorithm 的缩写,译为信息摘要算法,它是 Java 语言中使用很广泛的一种加密算法。本文将通过示例讨论下MD5的安全性,感兴趣的可以了解一下
    2022-10-10
  • 解决IDEA显示非法字符 \ufeff 的问题

    解决IDEA显示非法字符 \ufeff 的问题

    这篇文章主要介绍了解决IDEA显示非法字符 \ufeff 的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11

最新评论