用 Long 做 Map 的 Key,存的对象花一下午才取出来,坑惨了

2024-02-29 16:30

本文主要是介绍用 Long 做 Map 的 Key,存的对象花一下午才取出来,坑惨了,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

大家好,我是一航!

事情是这样!某天中午午休完,正在开始下午的搬砖任务,突然群里面热闹起来,由于忙,也就没有去看,过了一会儿,突然有伙伴在群里@我,就去爬楼看了一下大家的聊天记录,结果是发现了一个很有意思的Bug;看似很基础Map的取值问题,对于基础不是特别扎实的朋友来说,但如果真的遇到,可能会被坑惨,群里这位老弟就被坑了一下午,在这里分享给大家。

讨论的起因是一个老弟问了这样一个问题:

简单一句话表述就是:接口回了个Map,key是Long型的,Map中有数据,可取不到值;

由于基础数据类型的Key在以Json返回的时候,都被转成了String,有伙伴儿很快提出确认Key是不是被转成了String,结果都被否认了;但对于这个否认,我是持有怀疑态度的,所以,这里得必须亲自验证一下;

问题梳理

为了搞清楚状况,需要先简单的梳理一下;

  • 业务场景是这样:

    1. A服务提供了一个接口,返回了一个Map<Long , Object>
    2. B服务通过RestTemplate调用A服务对应的接口,入参就就是一个Long
    3. B服务通过得到Map<Long , Object>响应之后,再通过Long值作为Key,去得到Object
  • 问题点:

    至于这种接口设计方式是否合理,文末另说,这位老弟遇到的问题是:B服务能正常接收到Map<Long , Object>对象,也就是log.info("map:{}",map)都能正常输出对应的key和Object;但是通过map.get(sourceId)取Object,有时候正常,有时候取出来的null;这一下子就变的有意思了;程序员遇到Bug,只要是必现或者能百度到的,那都不算bug,轻轻松松拿下;唯独那种时而出现时而正常的bug,是最头疼的,可能让你一度怀疑人生;

复现Bug

为了能把这个问题点说清楚,按他的写法,我模拟了一下他的业务逻辑,写了一段简单代码复现一下正常情况异常情况

  • 能正常取值

    key为Long l = 123456789000L;,代码如下:

    @Slf4j
    public class Main {public static void main(String[] args) throws Exception {//A服务的数据Map<Long,String> mp = new HashMap<>();Long l = 123456789000L;mp.put(l,"123");log.info("key:{}",l);// B服务通过网络请求得到A服务的响应文本String s1 = JSON.toJSONString(mp);log.info("json文本:{}",s1);// 将文本转换成Map对象Map<Long,String> mp2 = JSON.parseObject(s1,Map.class);log.info("json文本转换的Map对象:{}",mp2);// 通过key取值log.info("通过key:{}得到的值:{}",l,mp2.get(l));}
    }
    

    运行结果

  • 取值为null

    异常情况下唯一的区别是key换成了Long l = 123456789L;

    public class Main {public static void main(String[] args) throws Exception {//A服务的数据Map<Long,String> mp = new HashMap<>();Long l = 123456789L;mp.put(l,"123");// B服务通过网络请求得到A服务的响应文本String s1 = JSON.toJSONString(mp);log.info("json文本:{}",s1);// 将文本转换成Map对象Map<Long,String> mp2 = JSON.parseObject(s1,Map.class);log.info("json文本转换的Map对象:{}",mp2);// 通过key取值log.info("通过key:{}得到的值:{}",l,mp2.get(l));}
    }
    

    运行结果

结果分析

发现没有!两段代码,除了key不一样,逻辑部分没有任何区别,均无报错,且都能正常运行,那为何一段正常一段结果为null呢?

bug场景复现了,一切就别的简单多了,既然mp2.get(l)取的值不同,问题点也肯定就出现在这个附近了,debug去分析一下mp2里面到底放了些啥:

好家伙!事出反常必有妖;

一看这两种情况下mp2对应key的类型(上图箭头部分),应该就明白,为什么key是long l = 123456789l的时候,mp2取不到值了吧;因为转换后mp2里面存的压根儿就不是Long型的key,而是一个Integer的key?当Key是Long型的时候,就能正常取到值,当为Integer的时候,取出来的就是null

为什么变成了Integer

明明我存的是一个Long作为key,Json文本转mp2的时候我也是通过Map<Long,String>去接收,似乎一切都有理有据,为什么最后mp2的key一会儿是Integer,一会儿是Long呢?

毕竟核心代码只有这么简单的5行,稍作分析就能知道,问题点是出在这行代码

Map<Long,String> mp2 = JSON.parseObject(s1,Map.class);

类型转换传递的对象仅仅是一个Map.class;并没有指明Map中的key和value的具体类型是什么;因为泛型擦除,导致fastJson在遇到基础数字类型key的时候,无法判断其具体的类型,只能通过长度去匹配一个最合适的数据类型;由于123456789可以使用Integer去接收,就将其转换成了Integer;而123456789000就只能通过Long型接收,就转换成了Long型;

以下是fastJson源码中关于数字类型判断的一段代码;用来匹配当前的数字需要转换成什么类型逻辑判断:

        if (negative) {if (i > this.np + 1) {if (result >= -2147483648L && type != 76) {if (type == 83) {return (short)((int)result);} else if (type == 66) {return (byte)((int)result);} else {return (int)result;}} else {return result;}} else {throw new NumberFormatException(this.numberString());}} else {result = -result;if (result <= 2147483647L && type != 76) {if (type == 83) {return (short)((int)result);} else if (type == 66) {return (byte)((int)result);} else {return (int)result;}} else {return result;}}

这样也就能明确解释这个bug所出现的原因了;

如何解决呢?

fastJson

如果单纯是通过fastJson将Json文本转对象,其实处理起来就很简单了,只需要指明一下Map中的key和value是什么类型的即可,代码如下

Map<Long,String> mp2 = JSON.parseObject(s1,new TypeReference<Map<Long,String>>(){});

即使当key为123456789的时候,依然能够造成获取到值

RestTemplate

本文的起因,是因为通过RestTemplate请求另外一个服务没有指明泛型对象造成的,因此也需要指明一下;

  • 示例接口

    @RestController
    @RequestMapping("/a")
    public class TestController {@GetMapping("/b")public Map<Long, String> b() {Map<Long, String> mp = new HashMap<>();mp.put(1L,"123");mp.put(123456789L,"456");mp.put(123456789000L,"789");return mp;}
    }
    
  • restTemplate请求

    @Autowired
    RestTemplate restTemplate;@Test
    public void restTemplate() throws Exception {ParameterizedTypeReference<Map<Long, String>> typeRef = new ParameterizedTypeReference<Map<Long, String>>() {};Map<Long, String> mp = restTemplate.exchange("http://127.0.0.1:8080/a/b", HttpMethod.GET, new HttpEntity<>(null), typeRef).getBody();log.info("mp:{}", mp);log.info("获取key为:{} 的值:{}",1L,mp.get(1L));log.info("获取key为:{} 的值:{}",123456789L,mp.get(123456789L));log.info("获取key为:{} 的值:{}",123456789000L,mp.get(123456789000L));
    }
    

思考

到这里,整个问题算是解决了!

但有另外一个点,也不得不说一下;这位老弟采用的是Map作为报文交互的对象,是非常不建议用的,通过Map,看似提高了灵活性,毕竟啥对象都可以扔进去,实则给代码的可读性、维护性带来了很大的障碍,因为我没有办法一眼看出这个Map中放了些什么数据,也不知道何时放了数据进去;如果我只是作为一个调用方,想去看一下你返回了些什么,仅仅通过接口定义,我是没办法清晰的看出,而是要深入阅读详细的代码,看你在Map中塞了些什么值,分别代表什么意思,才能加以明确。

而这一系列的问题,可能终将自己挖个深坑把自己给埋了

那么为了提高接口的灵活性、可阅读性以及可扩展性,基于泛型的接口报文数据抽象化是一个重要手段;将报文的Json格式分为公共部分业务数据部分,让整个数据结构变的更加灵活,但又不失整体的规范,通过响应对象,一眼就能明确你要返回的数据;可参考以下简单示例:

// 公共部分
{"code":0,"msg":"成功","data":{// 业务数据}
}

对应的代码:

@Data
public class BaseBean<T> {private Integer code;private String msg;private T data;
}

通过泛型,即可灵活表达任意响应

  • 用户

    @GetMapping("/user")
    public BaseBean<User> user() {// 这里去获取UserBaseBean<User> user = new BaseBean<>();return user;
    }
    
  • 商品

    @GetMapping("/goods")
    public BaseBean<Goods> goods() {// 这里去获取商品BaseBean<Goods> goods = new BaseBean<>();return user;
    }
    

好了,今天就分享到这里,愿看到此文的朋友,今后,再无Bug!!!

这篇关于用 Long 做 Map 的 Key,存的对象花一下午才取出来,坑惨了的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/759375

相关文章

JavaScript对象转数组的三种方法实现

《JavaScript对象转数组的三种方法实现》本文介绍了在JavaScript中将对象转换为数组的三种实用方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友... 目录方法1:使用Object.keys()和Array.map()方法2:使用Object.entr

java中判断json key是否存在的几种方法

《java中判断jsonkey是否存在的几种方法》在使用Java处理JSON数据时,如何判断某一个key是否存在?本文就来介绍三种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的... 目http://www.chinasem.cn录第一种方法是使用 jsONObject 的 has 方法

使用MapStruct实现Java对象映射的示例代码

《使用MapStruct实现Java对象映射的示例代码》本文主要介绍了使用MapStruct实现Java对象映射的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、什么是 MapStruct?二、实战演练:三步集成 MapStruct第一步:添加 Mave

Java中实现对象的拷贝案例讲解

《Java中实现对象的拷贝案例讲解》Java对象拷贝分为浅拷贝(复制值及引用地址)和深拷贝(递归复制所有引用对象),常用方法包括Object.clone()、序列化及JSON转换,需处理循环引用问题,... 目录对象的拷贝简介浅拷贝和深拷贝浅拷贝深拷贝深拷贝和循环引用总结对象的拷贝简介对象的拷贝,把一个

Redis高性能Key-Value存储与缓存利器常见解决方案

《Redis高性能Key-Value存储与缓存利器常见解决方案》Redis是高性能内存Key-Value存储系统,支持丰富数据类型与持久化方案(RDB/AOF),本文给大家介绍Redis高性能Key-... 目录Redis:高性能Key-Value存储与缓存利器什么是Redis?为什么选择Redis?Red

MySQL中On duplicate key update的实现示例

《MySQL中Onduplicatekeyupdate的实现示例》ONDUPLICATEKEYUPDATE是一种MySQL的语法,它在插入新数据时,如果遇到唯一键冲突,则会执行更新操作,而不是抛... 目录1/ ON DUPLICATE KEY UPDATE的简介2/ ON DUPLICATE KEY UP

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

使用Java读取本地文件并转换为MultipartFile对象的方法

《使用Java读取本地文件并转换为MultipartFile对象的方法》在许多JavaWeb应用中,我们经常会遇到将本地文件上传至服务器或其他系统的需求,在这种场景下,MultipartFile对象非... 目录1. 基本需求2. 自定义 MultipartFile 类3. 实现代码4. 代码解析5. 自定

javaSE类和对象进阶用法举例详解

《javaSE类和对象进阶用法举例详解》JavaSE的面向对象编程是软件开发中的基石,它通过类和对象的概念,实现了代码的模块化、可复用性和灵活性,:本文主要介绍javaSE类和对象进阶用法的相关资... 目录前言一、封装1.访问限定符2.包2.1包的概念2.2导入包2.3自定义包2.4常见的包二、stati

shell脚本批量导出redis key-value方式

《shell脚本批量导出rediskey-value方式》为避免keys全量扫描导致Redis卡顿,可先通过dump.rdb备份文件在本地恢复,再使用scan命令渐进导出key-value,通过CN... 目录1 背景2 详细步骤2.1 本地docker启动Redis2.2 shell批量导出脚本3 附录总