HashMap概述

HashMap是基于Map接口实现的,采取(key,value)的储存方式。其中key和value可以为空,但是key不能重复。下面我们来详细解读一下HashMap的底层实现原理。在jdk1.8之前,HashMap采用的是数组+链表的方式储存,在jdk1.8之后采用了数组+链表+红黑树,很重要的一个问题:既然数组+链表已经能实现,为什么又要加上红黑树?下面我们一起来对jdk1.8的HashMap做出详细说明。

HashMap储存结构

1、数组

 Integer[] a = new Integer[2];a[0]=1;a[1]=2;//插入System.out.println(a[0]);//查询

数组的特点是插入慢,查找快

2、链表

public class Node {public Node  next;private Object data;public Node(Object data) {this.data = data;}public static void main(String[] args) {Node node=new Node(1);node.next.next=new Node(1);}}

链表是一种物理存储单元上非连续、非顺序的存储结构,有多个节点组成,每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。特点是插入快,查找慢,和数组相反。

3、红黑树

红黑树的根本是一种特化的平衡二叉树:1.所有节点最多拥有两个子节点,即度不大于2;2.左子树的键值小于根的键值,右子树的键值大于根的键值;3.任何节点的两个子树的高度最大差为1。红黑树核心是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。红黑树有5个特点在平衡过程中作为依据:性质1. 节点是红色或黑色。性质2. 根节点是黑色。
性质3.所有叶子都是黑色。(叶子是NUIL节点) 性质4. 每个红色节点的

两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点) 性质5.. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

红黑树结构的核心类就是java.util.TreeMap ,从JDK1.8源码中我们来分析红黑树的结构:1、put存储

public V put(K key, V value) {Entry<K,V> t = root;if (t == null) {.....}int cmp;Entry<K,V> parent;// split comparator and comparable pathsComparator<? super K> cpr = comparator;if (cpr != null) {do {parent = t;cmp = cpr.compare(key, t.key);if (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;elsereturn t.setValue(value);} while (t != null);}else {....}Entry<K,V> e = new Entry<>(key, value, parent);if (cmp < 0)parent.left = e;elseparent.right = e;**fixAfterInsertion(e);**size++;modCount++;return null;}

整个过程是从根结点循环地向下查找,找到目标位置(t == null),就把新建的Entry 插入到树中。不过最重要的是平衡过程fixAfterInsertion(e);

private void fixAfterInsertion(Entry<K,V> x) {x.color = RED;while (x != null && x != root && x.parent.color == RED) {if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {Entry<K,V> y = rightOf(parentOf(parentOf(x)));if (colorOf(y) == RED) {setColor(parentOf(x), BLACK);setColor(y, BLACK);setColor(parentOf(parentOf(x)), RED);x = parentOf(parentOf(x));} else {if (x == rightOf(parentOf(x))) {x = parentOf(x);rotateLeft(x);}setColor(parentOf(x), BLACK);setColor(parentOf(parentOf(x)), RED);rotateRight(parentOf(parentOf(x)));}} else {Entry<K,V> y = leftOf(parentOf(parentOf(x)));if (colorOf(y) == RED) {setColor(parentOf(x), BLACK);setColor(y, BLACK);setColor(parentOf(parentOf(x)), RED);x = parentOf(parentOf(x));} else {if (x == leftOf(parentOf(x))) {x = parentOf(x);rotateRight(x);}setColor(parentOf(x), BLACK);setColor(parentOf(parentOf(x)), RED);rotateLeft(parentOf(parentOf(x)));}}}root.color = BLACK;}

从代码上看第一次判断if (parentOf(x) == leftOf(parentOf(parentOf(x)))) 意思是如果x的父亲是x的祖父(父亲的父亲)的左孩子的情况,y显然就是x的叔叔结点Entry<K,V> y = rightOf(parentOf(parentOf(x)))。内层 if (colorOf(y) == RED) 如果 y 是红色,把父亲和叔叔都变黑,把爷爷变红。然后令 x 指向爷爷结点,继续下一次循环。如果 y 是黑色的,if (x == rightOf(parentOf(x))) 判断x 是其父亲的左孩子还是右孩子,如果是左孩子,就把父结点变为黑,祖父结点变为红,然后在祖父结点上做一次右旋rotateLeft(x)。整体就是判断红黑树的5个特点,不符合则做出改变。

HashMap的存储扩容机制

HashMap就是使用哈希表来存储的。Java中HashMap采用了链地址法。链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。在说HashMap的扩容之前,我们先熟悉几个重要属性

/* 默认的数组的长度 <<:是逻辑左移,对于正数和负数,左移n位就相当于乘以2的n次方 */static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/* 最大长度 */static final int MAXIMUM_CAPACITY = 1 << 30;
/* 默认的加载因子 用于计算阈值(threshold) */static final float DEFAULT_LOAD_FACTOR = 0.75f;
/* 用于红黑树树化的节点个数阈值 */static final int TREEIFY_THRESHOLD = 8;
/* 用于解除红黑树树化(将红黑树转换为链表)的节点个数阈值 */static final int UNTREEIFY_THRESHOLD = 6;
/* 用于红黑树树化时要求数组的最小长度 */static final int MIN_TREEIFY_CAPACITY = 64;
/* 用于存节点的数组(节点会hash到table中的某一个index) */transient Node<K, V>[] table;
/* 用于存HashMap中的所有节点 */
transient Set<Map.Entry<K, V>> entrySet;/* 节点的总个数 */
transient int size;
/* 修改的次数 后续会看到modCount的作用 */transient int modCount;/* 决定是否扩容的阀值 */int threshold;
/* 加载因子 用于计算阈值(threshold) */final float loadFactor;

下面我们从源码看hashmap如何存储以及合适扩容

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {Node<K, V>[] tab;Node<K, V> p;int n, i;/** 如果tab还没有创建数组的话,则需要去resize方法中创建* */if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;/*** 1. 首先根据hash值计算出该节点属于哪个index* 2. 如果此时的bucket是空,表明这个bucket还没有任何节点存入,*    因此生成新节点后直接放入到该数组中*/if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {/*** 进入else模块就说明 p此时不为null,所以这个节点应该放到这个bucket的后面* 接下来如果这个节点之前有插入过,就会节点赋给e* 有三种情况(互斥条件,要么1要么2要么3出现):* 1. * 2. * 3. */Node<K, V> e;K k;/*此节点已经存在数组的一个位置并且key一致,则覆盖,直接把p赋给e */if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode) /* 情况2 p是一个TreeNode节点,(TreeNode是Node的一个间接子类,红黑树的分析会专门放到一个博客分析)*    那表明这个bucket已经红黑树树化了,因此调用红黑树树化去插入或者更新*/e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);else {/* 情况3在链表中,所以直接遍历链表即可,有一点需要注意,如果是新增加的节点要么链表中的数量就会增加一个*    有可能会达到阀值,一旦到达阀值就需要调用treeifyBin方法树化,至于会不会树化已经怎么树化后面会*    解析,这里先有个概念理解逻辑就可以*/for (int binCount = 0;; ++binCount) {             // 遍历链表并且记录链表个数if ((e = p.next) == null) {                   // 从链表的第二个开始,因为p在第一种情况已经比较过了p.next = newNode(hash, key, value, null); // 插入到链表尾if (binCount >= TREEIFY_THRESHOLD - 1)    // 树化 这个时候就可以看到TREEIFY_THRESHOLD的作用了treeifyBin(tab, hash);break;}if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) //表明链表中已经存在了这个节点break;p = e;}}if (e != null) {    // e不为null表明此节点已经存入过 所以这里没有modCount++V oldValue = e.value;//这里就可以很明确的看到onlyIfAbsent的作用,对了如果oldValue为null,那onlyIfAbsent就不起作用了if (!onlyIfAbsent || oldValue == null) e.value = value;afterNodeAccess(e); // 用于子类LinkedHashMap的方法return oldValue;}}/***  进行到这里之前没有此(节点或者说key也行)存入过*/++modCount;                // modCount++if (++size > threshold)    // 先增加size,mappings的个数 判断是否需要扩容resize();afterNodeInsertion(evict); // 用于子类LinkedHashMap的方法return null;}/* 将hash对应的bucket链表红黑树树化*/final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;/***  有两个条件会先采用扩容而不是直接树化*  1. tab为null*  2. tab的length也就是capacity的大小比MIN_TREEIFY_CAPACITY=64小*     因为这个时候认为扩容的效果比树化要好*/if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize();else if ((e = tab[index = (n - 1) & hash]) != null) { //如果当前bucket不为nullTreeNode<K,V> hd = null, tl = null;/*** 循环遍历整个链表* 1. 先把Node节点转换成TreeNode节点* 2. 红黑树的所有节点按原来的顺序利用指针(prev和next)形成了一个双向链表*    这也是多次遍历链表的时候顺序也不会变化的原因,后续有专门的一个博客来分析HashMap的遍历*/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);/*** 将TreeNode形成的双向链表转化成红黑树*/if ((tab[index] = hd) != null)hd.treeify(tab);}}// 将Node节点转化成TreeNode节点TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {return new TreeNode<>(p.hash, p.key, p.value, next);}// Callbacks to allow LinkedHashMap post-actionsvoid afterNodeAccess(Node<K,V> p) { }void afterNodeInsertion(boolean evict) { }void afterNodeRemoval(Node<K,V> p) { }

整体从代码我们可以看到hashmap的储存过程就可以明白HashMap1.8之后为什么要采用数组+链表+红黑树的储存方式?

一开始数据存数组如果发生hash冲突,这个时候需要把冲突的数据放到后面的链表中(链地址法),如果hash冲突的数据过多,就会让链表过长,查询效率会变低,所以jdk1.8之后当链表长度大于8时就是转化为红黑树。其中换会牵涉到一个数组扩容

HashMap的扩容机制

HashMap的扩容机制

当数组长度达到(最大程度*扩容因子)的就会扩容数组。扩容大小为原数组的二倍。看源码片段如下:

   final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;/*如果旧数组的长度不是空*/if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}
/*把新数组的长度设置为旧数组长度的两倍,newCap=2*oldCap*/else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)/*把新数组的门限设置为旧数组门限的两倍,newThr=oldThr*2*/newThr = oldThr << 1; // double threshold}/*如果旧数组的长度的是0,就是说第一次初始化*/else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;//新表长度乘以加载因子newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;

GitHub: https://github.com/zhudongsheng110/JavaPointer包含 java后端知识宇宙汇集,java基础(面向对象、集合、多线程)、框架(spring、springboot、doubbo、springcloud)、数据库(mysql 、oracle)、消息中间件(rabbitmq、kafka) 正更新中............

“ 技术类、生活类文章会持续更新,可以微信搜一搜「 Rise1024」可以领域java面试大纲。

存数组元素的个数_HashMap1.8之后为什么要采用数组+链表+红黑树的储存方式?相关推荐

  1. php 统计数组个数,php统计数组元素的个数和唯一性

    大家好,今天给大家分享的是php统计数组元素的个数和唯一性,希望大家喜欢. 我们在学习php数组的时候,如何来统计数组元素的个数和唯一性呢? 那么下面我们来说下 1,函数count() 统计数组元素个 ...

  2. C语言中调用数组元素的三种方法:下标法、数组名法、指针法

    /*调用数组元素的三种方法:下标法.数组名法.指针法*/ #include<stdio.h> int main() {int a[] = { 1,2,3,4,5 }, i, * p;pri ...

  3. 编写一个函数get_average()获取整型数组元素的平均值。要求这个函数既可以用来求一维数组元素的平均值,也可以求二维数组元素的平均值。

    题目内容: 编写一个函数get_average()获取整型数组元素的平均值.要求这个函数既可以用来求一维数组元素的平均值,也可以求二维数组元素的平均值.在main()函数中通过具体的一维数组Array ...

  4. 常见的数据结构:栈 队列 数组 链表 红黑树——List集合 _ HashSet集合、可变参数 collections集合 Map集合

    2021-06-07复习java 一.常见的数据结构 栈(先进后出) 队列 数组 链表 红黑树 二.List集合_介绍&常用方法 ArrayList集合 Linkedlist集合 三.Hash ...

  5. Java集合常见数据结构-栈/队列/数组/链表/红黑树

    数组 链表 红黑树

  6. php数组的元素个数,php怎么统计数组元素的个数

    这篇文章主要介绍了php统计数组元素个数的方法的相关资料,需要的朋友可以参考下 count():对数组中的元素个数进行统计; sizeof():和count()具有同样的用途,这两个函数都可以返回数组 ...

  7. php如何统计数组的个数,如何用php统计数组元素的个数(附代码)

    这篇文章主要介绍了php统计数组元素个数的方法的相关资料,需要的朋友可以参考下 count():对数组中的元素个数进行统计; sizeof():和count()具有同样的用途,这两个函数都可以返回数组 ...

  8. mysql统计唯一个数_统计数组元素的个数和唯一性的函数

    有些函数可以用来确定数组中的值总数及唯一值的个数.使用函数count()对元素个数进行统计,sizeof()函数时count()的别名,他们的功能是一样的. ①函数count() 函数count()的 ...

  9. 【Java8】堆栈/队列/数组/链表/红黑树,List/set子接口,hashcode/hashset,Map/内部接口,/统计字符个数,debug,斗地主,Collections,TreeSet

    文章目录 1.堆栈/队列/数组/链表:数据结构即计算机组织管理数据的方式,堆栈指的是内存图中的栈,不是堆 2.红黑树:二查,二查平,二查平1倍 3.List子接口:集合,IndexOutOfBound ...

最新文章

  1. 2022-2028年中国农副产品行业市场供需规模及未来前景分析报告
  2. JSP显示页面和数据库乱码
  3. C# 系统应用之透明罩MyOpaqueLayer实现360界面阴影效果
  4. docker启动sqlserver_Docker搭建SQLServer
  5. 数据可视化 -- Python
  6. DB天气app冲刺第十一天
  7. 简单的解决达梦数据库查询 dm.jdbc.driver.DmdbNClob@1064bb3e 问题
  8. html调用网易云播放器无法自动播放,网页内嵌网易云插件全程(包括生成自己歌单的外链)...
  9. java课程设计模拟科学计算器_JAVA课程设计科学计算器
  10. 惠普总裁口述的职业规划(3)
  11. THUOCL:清华大学开放中文词库
  12. Android中如何利用Minui显示字符的最简单Demo
  13. Kettle carte部署与运行
  14. 3D点云的快速分割:自动驾驶汽车应用的LiDAR处理实例
  15. nodejs中使用ioredis库操作redis
  16. word选中所有图形
  17. 2021年高处安装、维护、拆除考试内容及高处安装、维护、拆除复审考试
  18. Asterisk中订阅分机/中继状态(配合BLF显示订阅分机状态)
  19. 企业数字化转型,不止实施数字技术
  20. windows XP系统输入法不见了怎么办?

热门文章

  1. Java 基础 —— enum
  2. apt-get install 的参数(add-apt-repository)
  3. 数据结构的时间复杂度与空间复杂度、及相关证明
  4. 可变对象 vs 不可变对象(Python)
  5. 二叉树的遍历(先序/中序/后序,递归/迭代)与搜索
  6. and/or(||)的理解
  7. 因子(factor)的研究
  8. Python 数据结构与算法——递归
  9. plantuml最大宽度_设置TH最小和最大宽度非常缓慢
  10. python自学网站-python自学网站