面试官问:Java中如何处理含有泛型的JSON反序列化问题?

一、背景

今天无聊之园提了一个问题,涉及的示例大致如下:

public static void main(String[] args) {

    String jsonString = "[\"a\",\"b\"]";
    List<String> list = JSONObject.parseObject(jsonString, List.class);
    System.out.println(list);
}

例子中使用fastjson的类库。

为什么IDEA会受到以下警告,该如何解决?

689b97b8c665215577f755e881c2fee6.png

有些同学说直接使用抑制注解,抑制掉这个警告就好了。

抑制掉警告就可以了????

二、分析

2.1事出诡异必必妖

IDEA不会无缘无故提示警告提示,警告的原因上图已经通知。

把不带泛型的列表赋值给带泛型的列表,Java编译器并不知道重新返回不带泛型的实际列表是否符合带泛型的列表约束。

和下面的例子非常类似:

public static void main(String[] args) {
       List first = new ArrayList();
       first.add(1);
       first.add("2");
       first.add('3');

       // 提示上述警告
       List<String> third = first;
       System.out.println(third);
}

将first赋值给third时,不能保证first元素符合List的约束,即列表中全是String。

如果您执行上述代码,会发现没有报错,哈哈。

但是如果您使用foreach循环或继承器取String循环时会发生类型转换异常。

public static void main(String[] args) {
       List first = new ArrayList();
       first.add(1);
       first.add("2");
       first.add('3');

       List<String> third = first;
       for (String each : third) { // 类型转换异常
           System.out.println(each);
       }
}

类型转换异常?另外,公众号Java精选,回复java面试,获取在线资料,支持随时随地刷题。

我们使用IDEA的jclasslib反编译插件,得到main函数的代码如下:

0 new #2 <java/util/ArrayList>
 3 dup
 4 invokespecial #3 <java/util/ArrayList.<init>>
 7 astore_1
 8 aload_1
 9 iconst_1
10 invokestatic #4 <java/lang/Integer.valueOf>
13 invokeinterface #5 <java/util/List.add> count 2
18 pop
19 aload_1
20 ldc #6 <2>
22 invokeinterface #5 <java/util/List.add> count 2
27 pop
28 aload_1
29 bipush 51
31 invokestatic #7 <java/lang/Character.valueOf>
34 invokeinterface #5 <java/util/List.add> count 2
39 pop
40 aload_1
41 astore_2
42 aload_2
43 invokeinterface #8 <java/util/List.iterator> count 1
48 astore_3
49 aload_3
50 invokeinterface #9 <java/util/Iterator.hasNext> count 1
55 ifeq 79 (+24)
58 aload_3
59 invokeinterface #10 <java/util/Iterator.next> count 1
64 checkcast #11 <java/lang/String>
67 astore_4
69 getstatic #12 <java/lang/System.out>
72 aload_4
73 invokevirtual #13 <java/io/PrintStream.println>
76 goto 49 (-27)
79 return

从42到76行对应foreach循环的逻辑,可以修剪列表的迭代器进行遍历,取出每个元素后强转为String类型,存储到局部变量表索引为4的位置,然后进行打印。

如果对反编译不熟悉可以去目标目录,双击编译后的类文件,使用IDEA自带的插件进行反编译:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.chujianyun.common.json;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class JsonGenericDemo {
    public JsonGenericDemo() {
    }

    public static void main(String[] args) {
        List first = new ArrayList();
        first.add(1);
        first.add("2");
        first.add('3');
        List<String> third = first;
        Iterator var3 = first.iterator();

        while(var3.hasNext()) {
            String each = (String)var3.next();
            System.out.println(each);
        }
    }
}

印证了上述说法,可见在String each =(String)var3.next(); 这里出现了类型转换异常。

三、解决之道

3.1猜想验证

我们猜测是不是可以通过某种途径将泛型作为参数传递给fastjson,让fastjson某个返回值是带泛型的,从而解决这个矛盾呢?

显然我们要去原始码中寻找,在JSONObject类中找到了以下的方法:

/**
 * <pre>
 * String jsonStr = "[{\"id\":1001,\"name\":\"Jobs\"}]";
 * List<Model> models = JSON.parseObject(jsonStr, new TypeReference<List<Model>>() {});
 * </pre>
 * @param text json string
 * @param type type refernce
 * @param features
 * @return
 */
@SuppressWarnings("unchecked")
public static <T> T parseObject(String text, TypeReference<T> type, Feature... features) {
    return (T) parseObject(text, type.type, ParserConfig.global, DEFAULT_PARSER_FEATURE, features);
}

该函数的注释上还贴心地标注了相关用法,因此我们改造下:

public static void main(String[] args) {
        String jsonString = "[\"a\",\"b\"]";
        List<String> list = JSONObject.parseObject(jsonString, new TypeReference<List<String>>() {
        });
        System.out.println(list);
}

警告解除了。

所以大功告成?

难道上述做法可能为了消除一个警告,满足强迫症们的心愿而已吗???

且慢,我们看下面的例子:

import lombok.Data;

@Data
public class User {
    private Long id;

    private String name;
}
mport com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import java.util.ArrayList;
import java.util.List;

public class JsonGenericDemo {

    public static void main(String[] args) {
        // 构造数据
        User user = new User();
        user.setId(0L);
        user.setName("tom");

        List<User> users = new ArrayList<>();
        users.add(user);
        // 转为JSON字符串
        String jsonString = JSON.toJSONString(users);

        // 反序列化
        List<User> usersGet = JSONObject.parseObject(jsonString, List.class);

        for (User each : usersGet) {
            System.out.println(each);
        }
    }

}

大家执行上述示例会出现类型转换异常!

线程“主”中的异常java.lang.ClassCastException:com.alibaba.fastjson.JSONObject无法转换为com.chujianyun.common.json.com上的用户。com.chujianyun.common.json.JsonGenericDemo.main(JsonGenericDemo.java:26 )

有了第二部分的分析,大家可能就可以比较容易地想到

JSONObject.parseObject(jsonString, List.class) 构造出来的List存放的是JSONObject元素,foreach循环嵌套使用转换器遍历每个元素并强转为User类型是报类型转换异常。

那么为啥fastjson不能帮我们转换为List<User>类型呢?

有人说“由于泛型取向,没有泛型信息,所以无法逆向构造回初始类型”。

看下其实JSONObject.parseObject(jsonString, List.class);第一个参数的英文字符串,第二个参数是List.class。压根就没有提供泛型信息给FASTJSON。

作为这个工具函数本身,怎么猜得到要列出里面真正该存放啥类型呢?

因此如果能够通过某种途径,告诉它泛型的类型,就可以帮助您反序列化成真正的类型。

使用JSONObject.parseObject(jsonString, new TypeReference<List<User>>() { });即可。

因此我们使用TypeReference并同时为了消除警告,或者为了了解fastjson泛型的具体类型,正确反序列化泛型的类型。

那么突破原理是啥呢?我们看下com.alibaba.fastjson.TypeReference#TypeReference()

/**
 * Constructs a new type literal. Derives represented class from type
 * parameter.
 *
 * <p>Clients create an empty anonymous subclass. Doing so embeds the type
 * parameter in the anonymous class's type hierarchy so we can reconstitute it
 * at runtime despite erasure.
 */
protected TypeReference(){
   // 获取父类的 Type
    Type superClass = getClass().getGenericSuperclass();

  // 如果父类是参数化类型,会返回 java.lang.reflect.ParameterizedType
  // 调用 getActualTypeArguments 获取实际类型的数组 并拿到第一个
    Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0];

  // 缓存中有优先取缓存,没有则存入并设置
    Type cachedType = classTypeCache.get(type);
    if (cachedType == null) {
        classTypeCache.putIfAbsent(type, type);
        cachedType = classTypeCache.get(type);
    }

    this.type = cachedType;
}

通过代码和注释我们了解到:

创建一个空的匿名子类。将类型参数嵌入到匿名继承结构中,即使运行时类型替换也可以重建。

再回到parseObject函数,可以看到能够用的就是这个类型。

/**
 * <pre>
 * String jsonStr = "[{\"id\":1001,\"name\":\"Jobs\"}]";
 * List<Model> models = JSON.parseObject(jsonStr, new TypeReference<List<Model>>() {});
 * </pre>
 * @param text json string
 * @param type type refernce
 * @param features
 * @return
 */
@SuppressWarnings("unchecked")
public static <T> T parseObject(String text, TypeReference<T> type, Feature... features) {
    return (T) parseObject(text, type.type, ParserConfig.global, DEFAULT_PARSER_FEATURE, features);
}

3.2举一反三

很多其他框架也会采用类似的方法来获取泛型类型。

大家可以看看其他gson类库

<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.8.6</version>
</dependency>

看看其中的com.google.gson.reflect.TypeToken类,是不是似曾相识呢?

此外,如果我们自己除了JSON反序列化场景之外也有类似获取泛型参数的需求,是不是也可以采用类似的方法呢?

四、总结

希望大家能够意识到IDEA的警告。

遇到问题能够从更合理的角度思考,了解问题的本质。

学习一个问题可以尝试举一反三,活学活用。

作者:SimpleWu

blog.csdn.net/w605283073/article/details/107350113

公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!
最近有很多人问,有没有读者交流群!加入方式很简单,公众号Java精选,回复“加群”,即可入群!

Java精选面试题(微信小程序):3000+道面试题,包含Java基础、并发、JVM、线程、MQ系列、Redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架构设计等,在线随时刷题!
------ 特别推荐 ------
特别推荐:专注分享最前沿的技术与资讯,为弯道超车做好准备及各种开源项目与高效率软件的公众号,「大咖笔记」,专注挖掘好东西,非常值得大家关注。点击下方公众号卡片关注。

点击“阅读原文”,了解更多精彩内容!文章有帮助的话,点在看,转发吧!
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值