昨天一位粉丝朋友和我聊天,说遇到了一个莫名奇妙的问题,让我帮着分析一下;经过他的一轮描述,发现是一个HashMap元素顺序小'坑';但是一不留神,老司机也容易在这里翻车。
一句话来描述一下他的问题:明明我数据库语句使用了Order by进行了排序,日志中也看到数据是按顺序查出来了,但业务层收到数据依然还是乱序的呢?;
整个过程,确实出现了好几处的迷惑现象,影响了他对问题的判断;下面就从一个小案例加上源码分析,来看看到底发生了什么。
问题复现
为了方便说明问题,这里用一个简单的业务场景来模拟一下;
数据表
一张简单的学生信息表
需求
需要按id顺序(order by id)获取出学号(student_id)对应的学生信息;为了方便业务层面根据学号获取数据,这位朋友采用了如下的数据格式,并用了HashMap在业务代码中接收数据:
代码实现
- Dao
- xml
- 测试用例
问题点
以上代码的执行日志;
当学号是如下数据再查询的时候,整个数据查询和使用都是没有任何问题的
但是当把学号换成以下数据之后:
根据查询结果和输出日志可以看到,数据库查询出来的数据是顺序的;但是返回到业务层的HashMap中的数据就变成无序的了;
明明前面查询出来是正常的,咋换了个数据就不正常呢?原因只是前面的那批数据碰巧是对的而已,不要被表象给迷惑了,实际上两组数据这样返回都是无序的。
而实际的业务Bug中,往往也会因为这些迷惑数据(A用户访问正常,B用户访问就不正常了),导致一度怀疑人生...
问题分析
既然数据库的日志是顺序的获取出了数据,说明SQL层面是没有问题的,那问题点肯定是出在了HashMap;了解HashMap都知道,HashMap本身是无序的,但最让很多新手朋友疑惑的是,你无序没关系,但我是插入的时候是按我需要的顺序插入,这难道不行?如果你疑惑的是这个点,那说明你还没有理解这个无序的意思;HashMap的插入顺序和迭代取出顺序是没有任何关系的;
“
除非你在获取的时候,已知了插入时的所有key且都保存了下来;就可以按这个顺序key去获取;但实际的使用过程,很少会出现这么使用的情况;
简单的插入、获取示例:
执行日志:
源码原因分析
接下来就从源码的角度来详细的说一说HashMap的无序到底是怎么回事!
HashMap的数据结构
Java8 HashMap的数据结构如下图;采用的是数组+联表+红黑树的方式;因此元素所处的数组(黄色区域)下标位置是通过(n - 1) & hash])
来定位的,当如果出现不同key的hash值相同时,就会将同下标的值以联表
或者红黑树
的方式存起来;整个结构如下图所示:
HashMap的插入过程
- 根据key的hash值和数组长度的与运算定位数组下标
关键代码(n - 1) & hash])
,其中n为数组的长度,hash
值是通过hash(key)
运算得来
- tab[i]下标i的值为null时,就直接实例化放在下标位置
也就是上面的
- 如果出现hash冲突,tab[i]已经有值了,就会以链表方式跟在Node后面
- 联表中key存在了
修改值 - 当
if ((e = p.next) == null)
下一个节点为空的时候
就实例化一个新的Node,跟在后面
- 如果联表的长度大于8个的时候,就会转换为红黑树的结构存储
经过上面4个步骤,元素并没有按顺序存储,而是被打散在数组的各个下标下面;链表或红黑树的元素位置也没有固定顺序;同一hash的key,插入的时机不同,所处的位置也就不同;
示例数据保存分析
有了源码分析的支持,下面就来详细的看一下保存在hashMap中的key、value在使用(n - 1) & hash])
计算之后的下标位置:
key | value | 数组下标i = (n - 1) & hash]) |
key:0 | va0 | 6 |
key:1 | va1 | 5 |
key:2 | va2 | 4 |
key:3 | va3 | 11 |
key:4 | va4 | 10 |
最终key、value分布情况:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ... |
key | key:2 | key:1 | key:0 | key:4 | key:3 | ||||||||
值1 | va2 | va1 | va0 | va4 | va3 | ||||||||
值2 |
迭代获取
HashMap元素的迭代获取,就是先从左到右遍历数组,当数组索引位置有值时,再从上往下遍历联表或者红黑树;
源码如下:
核心代码
第一个循环for (int i = 0; i < tab.length; ++i)
:左到右遍历数组
第二个循环for (Node<K,V> e = tab[i]; e != null; e = e.next)
:从上往下遍历联表或者红黑树
对着前面key、value分布情况表格,再来回看输出日志,就能明白,为啥迭代出来是下面这样的输出结果了
有序问题如何解决
当需要保证插入顺序和获取顺序一致时,可以采取有序的数据结构来保存数据,如List,LinkedHashMap等...
MyBatis数据查询
例如一开始列举的示例;当数据查询需要按顺序返回时,可以变换一下方式,采用List接收数据;如果业务真的需要通过Map参与,可以通过转换,来重新构造一个LinkedHashMap的有序数据结构用于业务逻辑的需要;
示例如下:
- dao
- xml
- test
测试输出
LinkedHashMap
- 简介
当Map需要有序时,也只需将HashMap换成LinkedHashMap即可保证插入和取出的顺序一致;
LinkedHashMap是HashMap的子类
因此LinkedHashMap拥有了HashMap的所有功能;但是LinkedHashMap采用的是双向联表的方式保存数据;因此就能保证数据保存顺序和读取顺序的一致性
以下是读取数据的源码;可以看出是有序的遍历了整个联表;
- 测试示例改造:
输出:
总结
每一种数据结构,都有他其独有的特性;因此,基础知识的部分,一定要将差异部分的原理了解清楚,只要这样,在遇到问题的时候,才能准确分析出问题的本质,否则很容易被表象,被日志给迷惑而陷入迷茫;
好了,今天的分享就到这里,不介意的话,三连给安排一下!感激不尽!