查看原文
其他

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

Java精选 2022-08-09
>>号外:关注“Java精选”公众号,回复“2021面试题”关键词,领取全套500多份Java面试题文件。

一、背景

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

public static void main(String[] args) {

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

例子中使用fastjson的类库。

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

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

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

二、分析

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);
       }
}

类型转换异常?

我们使用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

往期精选  点击标题可跳转

MySQL 数据库中 52 条 SQL 语句性能优化方法,干货必收藏!

“装 X ”就是牛,试一试 IDEA 解决 Maven 依赖冲突的超级神器!

如何用 Spring Cloud 构建面向企业的大型分布式、微服务快速开发框架?

代码总是被嫌弃写的太烂?装上这个 IDEA 插件再试试!

Java 中注解与反射的使用方法及场景分析,有必要解释一下!

终于来了,IDEA 2021.1 正式版本发布,一起看看又有哪些神奇功能!

你还在用 Logback ?Log4j2 功能与异步性能已经无敌了,还不快试试?

Spring Boot 项目不同环境打包配置与Shell脚本部署实践,太实用了!

Spring boot 项目中如何优雅停止服务的五种方法,值得收藏!

Java 中什么是 IO 流,字节流、字符流两者区别,缓冲流原理代码剖析

你可能忽视的 MyBatis 3.5.X 在 JDK8 中存在的性能问题

点个赞,就知道你“在看”!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存