点击关注公众号,实用技术文章及时了解

需求背景

  • 给一个无序的map,按照value的值进行排序,value值越小,排在越前面。

  • key和value都不为null

  • value可能相同

  • 返回结果为一个相同的有序map

代码如下所示:

// 假设,key=商品id,value=商品剩余库存
Map<Long, Integer> map = new HashMap<>();
map.put(1L, 10);
map.put(2L, 20);
map.put(3L, 10);

到这里,大家可以先想想,如果是你会怎么解决?

我的解决思路

1、使用TreeMap,因为TreeMap可以对元素进行排序

2、重写TreeMap的比较器

代码如下所示:

// 承接上面的代码
// 按照 value 排序
Map<Long, Integer> treeMap1 = new TreeMap<>(new Comparator<Long>() {@Overridepublic int compare(Long o1, Long o2) {// 1、如果v1等于v2,则值为0// 2、如果v1小于v2,则值为-1// 3、如果v1等于v2,则值为1Integer value1 = map.get(o1);Integer value2 = map.get(o2);return value1.compareTo(value2);}
});
treeMap1.putAll(map);
System.out.println(treeMap1);

运行后的结果为:

{1=10, 2=20}

what?为什么我们添加了3个元素,结果少了一个呢?

TreeMap putAll源码分析

让我们来看看 putAll 的具体过程

1、分析 TreeMap.putAll

源码如下所示:

public void putAll(Map<? extends K, ? extends V> map) {// 一、获取待添加的map的大小int mapSize = map.size();// 二、当前的size大小等于0 且 待添加的map的大小不等于0 且 待添加的map是SortedMap的实现类,则执行以下逻辑if (size==0 && mapSize!=0 && map instanceof SortedMap) {// 1、获取待添加的map的比较器Comparator<?> c = ((SortedMap<?,?>)map).comparator();// 2、如果两个比较器相同,则执行以下逻辑if (c == comparator || (c != null && c.equals(comparator))) {// 3、修改次数+1++modCount;try {// 4、基于排序数据的线性时间树构建算法,进行buildbuildFromSorted(mapSize, map.entrySet().iterator(),null, null);} catch (java.io.IOException cannotHappen) {} catch (ClassNotFoundException cannotHappen) {}return;}}// 三、如果不符合上面的条件,则执行父类的 putAll 方法super.putAll(map);
}

从上面源码,不难看出,我们的数据符合 流程二,但是不符合 流程二-2,所以我们会执行父类的 putAll 方法,即流程三。

2、分析 AbstractMap.putAll

TreeMap 继承 AbstractMap,所以 super.putAll(map),执行的 putAll 为 AbstractMap 的 putAll 方法,源码如下所示:

public void putAll(Map<? extends K, ? extends V> m) {// 遍历 m map,将它所有的值,使用put方法,全部添加到当前的map中for (Map.Entry<? extends K, ? extends V> e : m.entrySet())put(e.getKey(), e.getValue());
}

这段代码简单,就是一个遍历添加元素的。

但是有一个问题,这里的 put 方法执行的是谁的 put 方法呢?

  • 1、AbstractMap.put

  • 2、TreeMap.put

这里大家可以先思考1分钟,然后再继续往下看。

答案是:

执行的是 TreeMap.put

回答错误 or 不知道真实原因的小伙伴,可以去网上搜搜答案,这里是一个很重要的基础知识点哦。

3、分析 TreeMap.put

源代码如下所示:

public V put(K key, V value) {// 一、获取根节点TreeMap.Entry<K,V> t = root;// 二、判断跟节点是否为空if (t == null) {// 类型检查 and null 检查compare(key, key); // type (and possibly null) check// 创建根节点root = new TreeMap.Entry<>(key, value, null);size = 1;// 修改次数加1modCount++;return null;}int cmp;TreeMap.Entry<K,V> parent;// 获取比较器Comparator<? super K> cpr = comparator;// 三、如果比较器不为空,则执行一下逻辑,即自定义比较器执行逻辑if (cpr != null) {do {// 1、将t节点赋值给parentparent = t;// 2、比较t节点的key是否与待添加的key相等cmp = cpr.compare(key, t.key);// 3、如果返回值小于0,则将左子树赋值给t节点,即后续遍历左子树if (cmp < 0)t = t.left;// 4、如果返回值大于0,则将右子树赋值给t节点,即后续遍历右子树else if (cmp > 0)t = t.right;else// 5、如果返回值为0,则覆盖原来的值return t.setValue(value);} while (t != null);}// 四、如果比较器为空,则执行以下逻辑,即默认执行逻辑else {// 这部分逻辑,先忽略}TreeMap.Entry<K,V> e = new TreeMap.Entry<>(key, value, parent);if (cmp < 0)parent.left = e;elseparent.right = e;fixAfterInsertion(e);size++;modCount++;return null;
}

我们结合上面的源码和我们自定义的排序器,就可以发现以下问题:

1、我们比较的是两个 value 的大小,而 value 可能是一样的。

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

好了既然问题找到了,那如何解决这个问题呢?

如果是你,你会怎么解决呢?可以花一分钟时间思考一下,再看后面的内容。

4、解决 TreeMap.putAll,元素缺失的问题

我当时想到最直接的方案就是,在 value 相等的情况下,不返回 0,返回1 or -1,这样就可以最简单、最快捷的解决这个问题了。

修改后的代码如下所示:

// 这里换了一种写法,是java8的特性,简化了代码(为了偷懒)
Map<Long, Integer> treeMap2 = new TreeMap<>((key1, key2) -> {// 1、如果v1等于v2,则值为0// 2、如果v1小于v2,则值为-1// 3、如果v1等于v2,则值为1Integer value1 = map.get(key1);Integer value2 = map.get(key2);int result = value1.compareTo(value2);if (result == 0) {return -1;}return result;
});treeMap2.putAll(map);
System.out.println(treeMap2);

运行后的结果为:

{3=10, 1=10, 2=20}

我们可以发现,3个值都有了,并且是有序的,完美符合需求!好了,关机下班!

然而事情并没有结束 (大家可以想一下,这样写会有什么问题呢?)

新的问题出现

第二天,高高兴兴的写着业务代码、调试逻辑,突然一个 空指针 的报错,出现了。这也太常见了吧,3分钟内解决!

排查了半天,发现又回到了昨天的修改的那段逻辑了。

1、TreeMap.get 获取不到值

简化版代码如下所示:

// 假设,key=商品id,value=商品剩余库存
Map<Long, Integer> map = new HashMap<>();
map.put(1L, 10);
map.put(2L, 20);
map.put(3L, 10);// 排序
Map<Long, Integer> treeMap2 = new TreeMap<>((key1, key2) -> {Integer value1 = map.get(key1);Integer value2 = map.get(key2);int result = value1.compareTo(value2);if (result == 0) {return -1;}return result;
});
treeMap2.putAll(map);
System.out.println(treeMap2);// 获取商品1的剩余数量
Integer quantity = treeMap2.get(1L);
System.out.println(quantity);

运行后的结果为:

{3=10, 1=10, 2=20}
null

这个结果令我百思不得其解,只能看看源码咯。

2、分析 TreeMap.get

源码如下所示:

public V get(Object key) {// 根据key获取节点TreeMap.Entry<K,V> p = getEntry(key);// 节点为空则返回null,否则返回节点的 value 值return (p==null ? null : p.value);
}final TreeMap.Entry<K,V> getEntry(Object key) {// 一、如果比较器不为空,则执行一下逻辑if (comparator != null)// 1、使用自定义比较器取出key对应的节点return getEntryUsingComparator(key);// 二、如果比较器为空,且key为null,则抛空指针异常if (key == null)throw new NullPointerException();@SuppressWarnings("unchecked")Comparable<? super K> k = (Comparable<? super K>) key;TreeMap.Entry<K,V> p = root;// 三、取出key对应的节点while (p != null) {int cmp = k.compareTo(p.key);if (cmp < 0)p = p.left;else if (cmp > 0)p = p.right;elsereturn p;}return null;
}

从上面的源码,我们可以发现,问题肯定就是出现在 getEntryUsingComparator 方法里了。

2、分析 TreeMap.getEntryUsingComparator

源码如下所示:

final TreeMap.Entry<K,V> getEntryUsingComparator(Object key) {// 一、将key转换成对应的类型@SuppressWarnings("unchecked")K k = (K) key;// 二、获取比较器Comparator<? super K> cpr = comparator;// 三、判断比较器是否为空if (cpr != null) {// 1、遍历map,取出key对应的节点对象TreeMap.Entry<K,V> p = root;while (p != null) {int cmp = cpr.compare(k, p.key);// 2、如果小于0,则将左节点的值赋值给pif (cmp < 0)p = p.left;// 3、如果大于0,则将右节点的值赋值给pelse if (cmp > 0)p = p.right;else// 4、如果等于0,则返回p节点return p;}}return null;
}

结合上面的源码,和我们之前自定义的比较器,我们不难发现问题出现在哪里:

自定义比较器,没有返回0的情况

问题找到了,解决吧!

推荐

主流Java进阶技术(学习资料分享)

Java面试题宝典

加入Spring技术开发社区

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!

中招了,重写TreeMap的比较器引发的问题...相关推荐

  1. Java中HashMap和TreeMap的区别深入理解,java开发面试笔试题

    我总结出了很多互联网公司的面试题及答案,并整理成了文档,以及各种学习的进阶学习资料,免费分享给大家. 扫描二维码或搜索下图红色VX号,加VX好友,拉你进[程序员面试学习交流群]免费领取.也欢迎各位一起 ...

  2. 详解IIS中URL重写工具的规则条件(Rule conditions)

    本文结合官方文档和相关示例,详细记录了在IIS中URL重写工具下的规则条件(Rule conditions)的相关说明.规则条件允许我们通过额外的逻辑规则来过滤和匹配规则模式( rule patter ...

  3. bilibili有电脑版吗_虚充制冷剂、谎称电脑版故障...空调维修的这些套路,你 中招了吗...

    2015年至今,上海市消保委共受理空调维修类的相关投诉数量就达2121件 空调维修投诉竟然这么多? 究其原因有以下两点 ↓ ↓ ↓ 一方面是由于一些空调企业缺乏诚信的经营意识,维修工通过虚构故障.小病 ...

  4. java 重写方法 访问权限_为何Java中子类重写方法的访问权限不能低于父类中权限(内含里氏替换原则)...

    为何Java中子类重写方法的访问权限不能低于父类中权限 因为 向上转型及Java程序设计维护的原因 例: 假设一个父类A 拥有的方法public void setXXX(){}可以被其他任意对象调用这 ...

  5. Java重载和重写6_深入理解Java中的重写和重载

    深入理解Java中的重写和重载 重载(Overloading)和重写(Overriding)是Java中两个比较重要的概念.但是对于新手来说也比较容易混淆.本文通过两个简单的例子说明了他们之间的区别. ...

  6. C#中引用第三方ocx控件引发的问题以及解决办法

    C#中引用第三方ocx控件引发的问题以及解决办法 参考文章: (1)C#中引用第三方ocx控件引发的问题以及解决办法 (2)https://www.cnblogs.com/XuPengLB/p/759 ...

  7. java布尔类型比较器_浅谈Java中几种常见的比较器的实现方法

    在java中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题. 通常对象之间的比较可以从两个方面去看: 第一个方面:对象的地址是否一样,也就是是否引用自同一个对象.这种方式可以直接使用& ...

  8. 特别提醒:人脸识别时,一定要穿衣服,一不小心就中招了

    程序员求职简历,项目经验怎么写?免费修改简历.提供模板并内部推荐 先说结论,人脸识别时,一定要穿上衣服. 你以为人脸识别拍的照片是这样的. 实际上,拍到的照片这样的. 道理很简单,人脸识别的时候,虽然 ...

  9. 职工福利费有误区,财务老师中招了吗?

    财务老师在工作中,最少不了的就是和各种费用打交道,职工福利费作为其中之一,财务老师应该也有很多心得吧!今天,我们就来了解下职工福利费的误区,财务老师可以来看看,自己中招了吗? 其实对于职工福利费的标准 ...

最新文章

  1. python 命令行参数处理 getopt模块详解
  2. vimproc_mac.so” is not found
  3. iOS程序启动画面的制作
  4. python.freelycode.com-Python数据可视化2018:为什么这么多的库?
  5. maven项目 ant_将旧项目从Ant迁移到Maven的4个简单步骤
  6. Linux 启/关 自启动服务
  7. 开机一直转圈_电脑开机后网络一直转圈,程序也打不开?
  8. 从源码角度理解 FragmentTransaction实现
  9. 使用动态优先权的进程调度算法的模拟_我爱OS第12讲:系统调度
  10. 雷林鹏分享:MySQL ALTER命令
  11. 学C++的室友手握这个项目,面试稳了
  12. 比特币 以太坊 真的是去中心化的吗?
  13. vs自拉软件全名_vs全新操作软件说明书
  14. 让手机可以边打电话边上网
  15. 辞旧迎新,新工作感悟!
  16. java awt生成签名图片消除锯齿化
  17. Oracle中joint,Nape中的LineJoint-线段关节
  18. 电子产品PCB电路板散热的方法
  19. Wildfish框架的实现原理之系统工具篇
  20. [转载]你所不了解的DevOps

热门文章

  1. 突然!高通骁龙855 Plus公布:手机厂商们集体沸腾
  2. 刘作虎曝光一加7真机视频 最流畅的手机来了!
  3. 苹果iOS 13暗黑模式概念图曝光 将于iOS 13.1版本更新
  4. 佛系听歌?Beats推出“串珠”耳机 盘它?
  5. 史上最拉风年货?苏宁门店私人飞机开售 网友:这个真香不了吧
  6. 内核模块编程之_初窥门径【ZT】
  7. 用c/c++实现linux下检测网络接口状态【ZT】
  8. 功能测试点有哪些?怎么做好软件功能测试?
  9. centos7网卡编辑_CentOS7网卡命名中碰到的一个坑
  10. 如何做带宽估计和丢包策略