用 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

相关文章

git ssh key相关

step1、进入.ssh文件夹   (windows下 下载git客户端)   cd ~/.ssh(windows mkdir ~/.ssh) step2、配置name和email git config --global user.name "你的名称"git config --global user.email "你的邮箱" step3、生成key ssh-keygen

Collection List Set Map的区别和联系

Collection List Set Map的区别和联系 这些都代表了Java中的集合,这里主要从其元素是否有序,是否可重复来进行区别记忆,以便恰当地使用,当然还存在同步方面的差异,见上一篇相关文章。 有序否 允许元素重复否 Collection 否 是 List 是 是 Set AbstractSet 否

Java第二阶段---09类和对象---第三节 构造方法

第三节 构造方法 1.概念 构造方法是一种特殊的方法,主要用于创建对象以及完成对象的属性初始化操作。构造方法不能被对象调用。 2.语法 //[]中内容可有可无 访问修饰符 类名([参数列表]){ } 3.示例 public class Car {     //车特征(属性)     public String name;//车名   可以直接拿来用 说明它有初始值     pu

HTML5自定义属性对象Dataset

原文转自HTML5自定义属性对象Dataset简介 一、html5 自定义属性介绍 之前翻译的“你必须知道的28个HTML5特征、窍门和技术”一文中对于HTML5中自定义合法属性data-已经做过些介绍,就是在HTML5中我们可以使用data-前缀设置我们需要的自定义属性,来进行一些数据的存放,例如我们要在一个文字按钮上存放相对应的id: <a href="javascript:" d

DBeaver 连接 MySQL 报错 Public Key Retrieval is not allowed

DBeaver 连接 MySQL 报错 Public Key Retrieval is not allowed 文章目录 DBeaver 连接 MySQL 报错 Public Key Retrieval is not allowed问题解决办法 问题 使用 DBeaver 连接 MySQL 数据库的时候, 一直报错下面的错误 Public Key Retrieval is

PHP7扩展开发之对象方式使用lib库

前言 上一篇文章,我们使用的是函数方式调用lib库。这篇文章我们将使用对象的方式调用lib库。调用代码如下: <?php $hello = new hello(); $result = $hello->get(); var_dump($result); ?> 我们将在扩展中实现hello类。hello类中将依赖lib库。 代码 基础代码 这个扩展,我们将在say扩展上增加相关代码。sa

hibernate修改数据库已有的对象【简化操作】

陈科肇 直接上代码: /*** 更新新的数据并并未修改旧的数据* @param oldEntity 数据库存在的实体* @param newEntity 更改后的实体* @throws IllegalAccessException * @throws IllegalArgumentException */public void updateNew(T oldEntity,T newEntity

类和对象的定义和调用演示(C++)

我习惯把类的定义放在头文件中 Student.h #define _CRT_SECURE_NO_WARNINGS#include <string>using namespace std;class student{public:char m_name[25];int m_age;int m_score;char* get_name(){return m_name;}int set_name

react笔记 8-19 事件对象、获取dom元素、双向绑定

1、事件对象event 通过事件的event对象获取它的dom元素 run=(event)=>{event.target.style="background:yellowgreen" //event的父级为他本身event.target.getAttribute("aid") //这样便获取到了它的自定义属性aid}render() {return (<div><h2>{

Python---文件IO流及对象序列化

文章目录 前言一、pandas是什么?二、使用步骤 1.引入库2.读入数据总结 前言 前文模块中提到加密模块,本文将终点介绍加密模块和文件流。 一、文件流和IO流概述         在Python中,IO流是用于输入和输出数据的通道。它可以用于读取输入数据或将数据写入输出目标。IO流可以是标准输入/输出流(stdin和stdout),也可以是文件流,网络流等。