Java自带的序列化存在的问题-Java中含有泛型的JSON反序列化问题-《Java笔记》

admin 2025-10-19 02:59:14 编程 来源:ZONE.CI 全球网 0 阅读模式

Java 反序列化 JSON

一、背景

  1. public static void main(String[] args) {
  2. String jsonString = "[\"a\",\"b\"]";
  3. List<String> list = JSONObject.parseObject(jsonString, List.class);
  4. System.out.println(list);
  5. }

例子中使用fastjson 的类库。为什么 IDEA 会给出下面的警告,该如何解决?2021-05-13-13-38-31-117773.png

二、分析

2.1 事出诡异必有妖

IDEA 不会无缘无故给出警告提示,警告的原因上图已经给出。把不带泛型的 List 赋值给带泛型的 List, Java 编译器并不知道右侧返回不带泛型的实际 List 是否符合带泛型的 List 约束。和下面的例子非常类似:

  1. public static void main(String[] args) {
  2. List first = new ArrayList();
  3. first.add(1);
  4. first.add("2");
  5. first.add('3');
  6. // 提示上述警告
  7. List<String> third = first;
  8. System.out.println(third);
  9. }

将 first 赋值给 third 时,不能保证 first 元素符合 List的约束,即列表中全是 String。如果执行上述代码,会发现没有报错,哈哈。但是如果使用 foreach 循环或者迭代器取 String 循环时会发生类型转换异常。

  1. public static void main(String[] args) {
  2. List first = new ArrayList();
  3. first.add(1);
  4. first.add("2");
  5. first.add('3');
  6. List<String> third = first;
  7. for (String each : third) { // 类型转换异常
  8. System.out.println(each);
  9. }
  10. }

类型转换异常?使用 IDEA 的 jclasslib 反编译插件,得到 main 函数的 Code 如下:

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

从 42 到76 行 对应 foreach 循环的逻辑,可以看出底层使用 List 的迭代器进行遍历,取出每个元素后强转为 String 类型,存储到局部变量表索引为 4 的位置,然后进行打印。如果对反编译不熟悉可以去 target 目录,双击编译后的class 文件,使用 IDEA 自带的插件进行反编译:

  1. //
  2. // Source code recreated from a .class file by IntelliJ IDEA
  3. // (powered by Fernflower decompiler)
  4. //
  5. package com.chujianyun.common.json;
  6. import java.util.ArrayList;
  7. import java.util.Iterator;
  8. import java.util.List;
  9. public class JsonGenericDemo {
  10. public JsonGenericDemo() {
  11. }
  12. public static void main(String[] args) {
  13. List first = new ArrayList();
  14. first.add(1);
  15. first.add("2");
  16. first.add('3');
  17. List<String> third = first;
  18. Iterator var3 = first.iterator();
  19. while(var3.hasNext()) {
  20. String each = (String)var3.next();
  21. System.out.println(each);
  22. }
  23. }
  24. }

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

三、解决之道

3.1 猜想验证

猜测是不是可以通过某种途径将泛型作为参数传给 fastjson, 让 fastjson 某个返回值是带泛型的,从而解决这个告警呢?显然要去源码中寻找, 在 JSONObject 类中找到了下面的方法:

  1. /**
  2. * <pre>
  3. * String jsonStr = "[{\"id\":1001,\"name\":\"Jobs\"}]";
  4. * List<Model> models = JSON.parseObject(jsonStr, new TypeReference<List<Model>>() {});
  5. * </pre>
  6. * @param text json string
  7. * @param type type refernce
  8. * @param features
  9. * @return
  10. */
  11. @SuppressWarnings("unchecked")
  12. public static <T> T parseObject(String text, TypeReference<T> type, Feature... features) {
  13. return (T) parseObject(text, type.type, ParserConfig.global, DEFAULT_PARSER_FEATURE, features);
  14. }

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

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

警告解除了。所以大功告成?

  1. import lombok.Data;
  2. @Data
  3. public class User {
  4. private Long id;
  5. private String name;
  6. }
  7. mport com.alibaba.fastjson.JSON;
  8. import com.alibaba.fastjson.JSONObject;
  9. import java.util.ArrayList;
  10. import java.util.List;
  11. public class JsonGenericDemo {
  12. public static void main(String[] args) {
  13. // 构造数据
  14. User user = new User();
  15. user.setId(0L);
  16. user.setName("tom");
  17. List<User> users = new ArrayList<>();
  18. users.add(user);
  19. // 转为JSON字符串
  20. String jsonString = JSON.toJSONString(users);
  21. // 反序列化
  22. List<User> usersGet = JSONObject.parseObject(jsonString, List.class);
  23. for (User each : usersGet) {
  24. System.out.println(each);
  25. }
  26. }
  27. }

执行上述例子会出现类型转换异常!

  1. Exception in thread main java.lang.ClassCastException: com.alibaba.fastjson.JSONObject cannot be cast to com.chujianyun.common.json.User at 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。作为这个工具函数本身,怎么猜得到要 List 里面究竟该存放啥类型呢?因此如果能够通过某种途径,告诉它泛型的类型,就可以反序列化成真正的类型。使用JSONObject.parseObject(jsonString, new TypeReference<List<User>>() { });即可。因此使用 TypeReference 并不仅仅是为了消除警告,而是为了告知 fastjson 泛型的具体类型,正确反序列化泛型的类型。那么底层原理是啥呢?看下com.alibaba.fastjson.TypeReference#TypeReference()

  1. /**
  2. * Constructs a new type literal. Derives represented class from type
  3. * parameter.
  4. *
  5. * <p>Clients create an empty anonymous subclass. Doing so embeds the type
  6. * parameter in the anonymous class's type hierarchy so we can reconstitute it
  7. * at runtime despite erasure.
  8. */
  9. protected TypeReference(){
  10. // 获取父类的 Type
  11. Type superClass = getClass().getGenericSuperclass();
  12. // 如果父类是参数化类型,会返回 java.lang.reflect.ParameterizedType
  13. // 调用 getActualTypeArguments 获取实际类型的数组 并拿到第一个
  14. Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
  15. // 缓存中有优先取缓存,没有则存入并设置
  16. Type cachedType = classTypeCache.get(type);
  17. if (cachedType == null) {
  18. classTypeCache.putIfAbsent(type, type);
  19. cachedType = classTypeCache.get(type);
  20. }
  21. this.type = cachedType;
  22. }

通过代码和注释了解到:创建一个空的匿名子类。将类型参数嵌入到匿名继承结构中,即使运行时类型擦除也可以重建。再回到 parseObject 函数,可以看到底层用的就是这个 type。

  1. /**
  2. * <pre>
  3. * String jsonStr = "[{\"id\":1001,\"name\":\"Jobs\"}]";
  4. * List<Model> models = JSON.parseObject(jsonStr, new TypeReference<List<Model>>() {});
  5. * </pre>
  6. * @param text json string
  7. * @param type type refernce
  8. * @param features
  9. * @return
  10. */
  11. @SuppressWarnings("unchecked")
  12. public static <T> T parseObject(String text, TypeReference<T> type, Feature... features) {
  13. return (T) parseObject(text, type.type, ParserConfig.global, DEFAULT_PARSER_FEATURE, features);
  14. }

3.2 举一反三

很多其他框架也会采用类似的方法来获取泛型类型。大家可以看看其他 gson 类库

  1. <dependency>
  2. <groupId>com.google.code.gson</groupId>
  3. <artifactId>gson</artifactId>
  4. <version>2.8.6</version>
  5. </dependency>

看看其中的com.google.gson.reflect.TypeToken类,是不是似曾相识呢?此外,如果自己除了JSON反序列化场景之外也有类似获取泛型参数的需求,是不是也可以采用类似的方法呢?

    以太坊cppgolang区别 编程

    以太坊cppgolang区别

    以太坊是一种去中心化的开源平台,它采用智能合约技术,旨在构建和运行不受干扰的分布式应用程序。作为目前最受欢迎的区块链平台之一,以太坊提供了多种编程语言的支持,其
    progolang 编程

    progolang

    Go语言(Golang)是由Google开发的一门静态类型编程语言。作为一名专业的Golang开发者,我深知这门语言的优势和特点。在本文中,我将介绍Golang
    golangn个发送者 编程

    golangn个发送者

    Golang是一种开源的编程语言,由Google团队开发,旨在提高程序的并发性和简化软件开发过程。在Go语言中,有时需要向多个接收者发送信息。本文将介绍如何在G
    golang技能图谱 编程

    golang技能图谱

    从互联网行业的快速发展到人工智能技术的日益成熟,各种编程语言也应运而生。而在这众多的编程语言中,Golang(即Go)作为一门强大且高效的开发语言备受关注。Go
    评论:0   参与:  10