中招了,重写TreeMap的比较器引发的问题…
需求背景
- 给一个无序的map,按照value的值进行排序,value值越小,排在越前面。
- key和value都不为null
- value可能相同
- 返回结果为一个相同的有序map
代码如下所示:
1 // 假设,key=商品id,value=商品剩余库存
2 Map<Long, Integer> map = new HashMap<>();
3 map.put(1L, 10);
4 map.put(2L, 20);
5 map.put(3L, 10);
到这里,大家可以先想想,如果是你会怎么解决?
我的解决思路
1、使用TreeMap,因为TreeMap可以对元素进行排序
2、重写TreeMap的比较器
代码如下所示:
1 // 承接上面的代码
2 // 按照 value 排序
3 Map<Long, Integer> treeMap1 = new TreeMap<>(new Comparator<Long>() {
4 @Override
5 public int compare(Long o1, Long o2) {
6 // 1、如果v1等于v2,则值为0
7 // 2、如果v1小于v2,则值为-1
8 // 3、如果v1等于v2,则值为1
9 Integer value1 = map.get(o1);
10 Integer value2 = map.get(o2);
11 return value1.compareTo(value2);
12 }
13 });
14 treeMap1.putAll(map);
15 System.out.println(treeMap1);
运行后的结果为:
{1=10, 2=20}
what?为什么我们添加了3个元素,结果少了一个呢?

TreeMap putAll源码分析
让我们来看看 putAll 的具体过程
1、分析 TreeMap.putAll
源码如下所示:
1 public void putAll(Map<? extends K, ? extends V> map) {
2 // 一、获取待添加的map的大小
3 int mapSize = map.size();
4 // 二、当前的size大小等于0 且 待添加的map的大小不等于0 且 待添加的map是SortedMap的实现类,则执行以下逻辑
5 if (size==0 && mapSize!=0 && map instanceof SortedMap) {
6 // 1、获取待添加的map的比较器
7 Comparator<?> c = ((SortedMap<?,?>)map).comparator();
8 // 2、如果两个比较器相同,则执行以下逻辑
9 if (c == comparator || (c != null && c.equals(comparator))) {
10 // 3、修改次数+1
11 ++modCount;
12 try {
13 // 4、基于排序数据的线性时间树构建算法,进行build
14 buildFromSorted(mapSize, map.entrySet().iterator(),
15 null, null);
16 } catch (java.io.IOException cannotHappen) {
17 } catch (ClassNotFoundException cannotHappen) {
18 }
19 return;
20 }
21 }
22 // 三、如果不符合上面的条件,则执行父类的 putAll 方法
23 super.putAll(map);
24 }
从上面源码,不难看出,我们的数据符合 流程二,但是不符合 流程二-2,所以我们会执行父类的 putAll 方法,即流程三。
2、分析 AbstractMap.putAll
TreeMap 继承 AbstractMap,所以 super.putAll(map),执行的 putAll 为 AbstractMap 的 putAll 方法,源码如下所示:
1 public void putAll(Map<? extends K, ? extends V> m) {
2 // 遍历 m map,将它所有的值,使用put方法,全部添加到当前的map中
3 for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
4 put(e.getKey(), e.getValue());
5 }
这段代码简单,就是一个遍历添加元素的。
但是有一个问题,这里的 put 方法执行的是谁的 put 方法呢?
- 1、AbstractMap.put
- 2、TreeMap.put
这里大家可以先思考1分钟,然后再继续往下看。
答案是:
执行的是 TreeMap.put
回答错误 or 不知道真实原因的小伙伴,可以去网上搜搜答案,这里是一个很重要的基础知识点哦。
3、分析 TreeMap.put
源代码如下所示:
1 public V put(K key, V value) {
2 // 一、获取根节点
3 TreeMap.Entry<K,V> t = root;
4 // 二、判断跟节点是否为空
5 if (t == null) {
6 // 类型检查 and null 检查
7 compare(key, key); // type (and possibly null) check
8 // 创建根节点
9 root = new TreeMap.Entry<>(key, value, null);
10 size = 1;
11 // 修改次数加1
12 modCount++;
13 return null;
14 }
15
16 int cmp;
17 TreeMap.Entry<K,V> parent;
18 // 获取比较器
19 Comparator<? super K> cpr = comparator;
20 // 三、如果比较器不为空,则执行一下逻辑,即自定义比较器执行逻辑
21 if (cpr != null) {
22 do {
23 // 1、将t节点赋值给parent
24 parent = t;
25 // 2、比较t节点的key是否与待添加的key相等
26 cmp = cpr.compare(key, t.key);
27 // 3、如果返回值小于0,则将左子树赋值给t节点,即后续遍历左子树
28 if (cmp < 0)
29 t = t.left;
30 // 4、如果返回值大于0,则将右子树赋值给t节点,即后续遍历右子树
31 else if (cmp > 0)
32 t = t.right;
33 else
34 // 5、如果返回值为0,则覆盖原来的值
35 return t.setValue(value);
36 } while (t != null);
37 }
38 // 四、如果比较器为空,则执行以下逻辑,即默认执行逻辑
39 else {
40 // 这部分逻辑,先忽略
41 }
42 TreeMap.Entry<K,V> e = new TreeMap.Entry<>(key, value, parent);
43 if (cmp < 0)
44 parent.left = e;
45 else
46 parent.right = e;
47 fixAfterInsertion(e);
48 size++;
49 modCount++;
50 return null;
51 }
我们结合上面的源码和我们自定义的排序器,就可以发现以下问题:
1、我们比较的是两个 value 的大小,而 value 可能是一样的。

这种情况下,就会覆盖原来的值,这个就是我们执行 putAll 后,元素缺失的原因了。

好了既然问题找到了,那如何解决这个问题呢?
如果是你,你会怎么解决呢?可以花一分钟时间思考一下,再看后面的内容。
4、解决 TreeMap.putAll,元素缺失的问题
我当时想到最直接的方案就是,在 value 相等的情况下,不返回 0,返回1 or -1,这样就可以最简单、最快捷的解决这个问题了。
修改后的代码如下所示:
1 // 这里换了一种写法,是java8的特性,简化了代码(为了偷懒)
2 Map<Long, Integer> treeMap2 = new TreeMap<>((key1, key2) -> {
3 // 1、如果v1等于v2,则值为0
4 // 2、如果v1小于v2,则值为-1
5 // 3、如果v1等于v2,则值为1
6 Integer value1 = map.get(key1);
7 Integer value2 = map.get(key2);
8
9 int result = value1.compareTo(value2);
10
11 if (result == 0) {
12 return -1;
13 }
14 return result;
15 });
16
17 treeMap2.putAll(map);
18 System.out.println(treeMap2);
运行后的结果为:
{3=10, 1=10, 2=20}
我们可以发现,3个值都有了,并且是有序的,完美符合需求!好了,关机下班!

然而事情并没有结束 (大家可以想一下,这样写会有什么问题呢?)!
新的问题出现
第二天,高高兴兴的写着业务代码、调试逻辑,突然一个 空指针 的报错,出现了。这也太常见了吧,3分钟内解决!

排查了半天,发现又回到了昨天的修改的那段逻辑了。
1、TreeMap.get 获取不到值
简化版代码如下所示:
1 // 假设,key=商品id,value=商品剩余库存
2 Map<Long, Integer> map = new HashMap<>();
3 map.put(1L, 10);
4 map.put(2L, 20);
5 map.put(3L, 10);
6
7 // 排序
8 Map<Long, Integer> treeMap2 = new TreeMap<>((key1, key2) -> {
9 Integer value1 = map.get(key1);
10 Integer value2 = map.get(key2);
11
12 int result = value1.compareTo(value2);
13
14 if (result == 0) {
15 return -1;
16 }
17 return result;
18 });
19 treeMap2.putAll(map);
20 System.out.println(treeMap2);
21
22 // 获取商品1的剩余数量
23 Integer quantity = treeMap2.get(1L);
24 System.out.println(quantity);
运行后的结果为:
{3=10, 1=10, 2=20}
null
这个结果令我百思不得其解,只能看看源码咯。
2、分析 TreeMap.get
源码如下所示:
1 public V get(Object key) {
2 // 根据key获取节点
3 TreeMap.Entry<K,V> p = getEntry(key);
4 // 节点为空则返回null,否则返回节点的 value 值
5 return (p==null ? null : p.value);
6 }
7
8 final TreeMap.Entry<K,V> getEntry(Object key) {
9 // 一、如果比较器不为空,则执行一下逻辑
10 if (comparator != null)
11 // 1、使用自定义比较器取出key对应的节点
12 return getEntryUsingComparator(key);
13 // 二、如果比较器为空,且key为null,则抛空指针异常
14 if (key == null)
15 throw new NullPointerException();
16 @SuppressWarnings("unchecked")
17 Comparable<? super K> k = (Comparable<? super K>) key;
18 TreeMap.Entry<K,V> p = root;
19 // 三、取出key对应的节点
20 while (p != null) {
21 int cmp = k.compareTo(p.key);
22 if (cmp < 0)
23 p = p.left;
24 else if (cmp > 0)
25 p = p.right;
26 else
27 return p;
28 }
29 return null;
30 }
从上面的源码,我们可以发现,问题肯定就是出现在 getEntryUsingComparator 方法里了。
2、分析 TreeMap.getEntryUsingComparator
源码如下所示:
1 final TreeMap.Entry<K,V> getEntryUsingComparator(Object key) {
2 // 一、将key转换成对应的类型
3 @SuppressWarnings("unchecked")
4 K k = (K) key;
5 // 二、获取比较器
6 Comparator<? super K> cpr = comparator;
7 // 三、判断比较器是否为空
8 if (cpr != null) {
9 // 1、遍历map,取出key对应的节点对象
10 TreeMap.Entry<K,V> p = root;
11 while (p != null) {
12 int cmp = cpr.compare(k, p.key);
13 // 2、如果小于0,则将左节点的值赋值给p
14 if (cmp < 0)
15 p = p.left;
16 // 3、如果大于0,则将右节点的值赋值给p
17 else if (cmp > 0)
18 p = p.right;
19 else
20 // 4、如果等于0,则返回p节点
21 return p;
22 }
23 }
24 return null;
25 }
结合上面的源码,和我们之前自定义的比较器,我们不难发现问题出现在哪里:

自定义比较器,没有返回0的情况
问题找到了,解决吧!
中招了,重写TreeMap的比较器引发的问题…的更多相关文章
- Java中HashMap和TreeMap的区别深入理解
首先介绍一下什么是Map.在数组中我们是通过数组下标来对其内容索引的,而在Map中我们通过对象来对对象进行索引,用来索引的对象叫做key,其对应的对象叫做value.这就是我们平时说的键值对. Has ...
- java中sort方法的自定义比较器写法(转载)
java中sort方法的自定义比较器写法 摘要 在做一些算法题时常常会需要对数组.自定义对象.集合进行排序. 在java中对数组排序提供了Arrays.sort()方法,对集合排序提供Collecti ...
- java中的重写与重载
重写(Override) 重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变.即外壳不变,核心重写! 重写的好处在于子类可以根据需要,定义特定于自己的行为. 也就是说子类 ...
- 重写TreeMap的compare方法处理配置表
需要处理的配置表如下: 接上一篇的优化,接着优化,优化代码如下: 这段代码的关键在于重写TreeMap的compare方法. 关于如何重写TreeMap的compare方法,以及返回值代表的意义,可 ...
- Java中方法重写和方法重载
首先方法重写和方法重载是建立在Java的面向对象的继承和多态的特性基础上而出现的.至于面向对象的继承和多态的特性我就不在这里多说了.继承是指在一个父类的基础再创建一个子类,这样子类就拥有了父类的非私 ...
- Web前端攻防,一不小心就中招了
随着各浏览器安全功能的提高,前端防御面临的问题也没有之前那么复杂,但浏览器的防御措施并不能百分百的保证网站的安全. 浏览器的XSS Auditor,使得反射型xss几乎被废:CSP(Content-S ...
- Java中的重写
以下内容引用自http://wiki.jikexueyuan.com/project/java/overriding.html: 如果一个类从它的父类继承了一个方法,如果这个方法没有被标记为final ...
- Go1.13 标准库的 http 包爆出重大 bug,你的项目中招了吗? 原创: 王亚楼 Go语言中文网 今天
Go1.13 标准库的 http 包爆出重大 bug,你的项目中招了吗? 原创: 王亚楼 Go语言中文网 今天
- IIS-详解IIS中URL重写工具的规则条件(Rule conditions)
出处:https://shiyousan.com/post/635654920639643421 本文结合官方文档和相关示例,详细记录了在IIS中URL重写工具下的规则条件(Rule conditio ...
随机推荐
- 逆波兰(非与或)表达式原理及C++代码实现
p.p1 { margin: 0; font: 11px Menlo; color: rgba(209, 47, 27, 1); background-color: rgba(255, 255, 25 ...
- 【九度OJ】题目1434:今年暑假不AC 解题报告
[九度OJ]题目1434:今年暑假不AC 解题报告 标签(空格分隔): 九度OJ http://ac.jobdu.com/problem.php?pid=1434 题目描述: "今年暑假不A ...
- 第九届河南理工大学算法程序设计大赛 正式赛L:最优规划(最小生成树)
单测试点时限: 1.0 秒 内存限制: 512 MB 有很多城市之间已经建立了路径,但是有些城市之间没有路径联通.为了联通所有的城市,现在需要添加一些路径,为了节约,需要满足添加总路径是最短的. 输入 ...
- Theoretically Principled Trade-off between Robustness and Accuracy
目录 概 主要内容 符号说明 Error Classification-calibrated surrogate loss 引理2.1 定理3.1 定理3.2 由此导出的TRADES算法 实验概述 代 ...
- zbar解析二维码demo
开发环境;ubuntu 18.04 IDE:clion 2019 源文件.cpp #include <opencv2/opencv.hpp> #include <zbar.h> ...
- Count Sequences
\(考虑问题的转换,即把用n个球,分为r-l+2个部分,其中第1部分表示该区域的球值为l,第二部分表示该区域的球值为l+1\) \(......第r-l+2部分为不选该区域的球\) \(该问题等价于在 ...
- hadoop 之 hadoop2.7.7升级到hadoop2.8.5
服务器规划 准备阶段 停服务 stop-yarn.sh stop-dfs.sh 备份 备份NameNode目录 ---------- 101,102操作 ----------------- hdfs- ...
- 使用 spring security 中的BcryptPasswordEncoder对象对用户密码进行加密
一.引入security启动器 在子工程中直接引入,不用指定版本号 二.在启动类中把BCryptPasswordEncoder对象注入到容器中 三.在service 层注入 四. 调用encode方法 ...
- CentOS 7 把已登录的用户断开
1. 查看登陆用户 [root@localhost ~]# w 18:29:30 up 377 days, 8:44, 4 users, load average: 0.05, 0.12, 0.09 ...
- PyCharm - 关联mysql失败 - Server returns invalid timezone. Go to 'Advanced' tab and set 'serverTimezone' property manually.
时区错误,MySQL默认的时区是UTC时区,比北京时间晚8个小时. 所以要修改mysql的时长 在mysql的命令模式下,输入: set global time_zone='+8:00'; 再次连接成 ...