文章目录
-
- 前言
- 一、`JSON.parseObject()`是什么
-
- [1.0 `json`是什么](#1.0
json是什么) -
- [1. 格式规范](#1. 格式规范)
- [2. `JSON`常见类型如下:](#2.
JSON常见类型如下:)
- [1.1 转成通用的` JSONObject`](#1.1 转成通用的
JSONObject) - [1.2 转成自定义的 Java 实体类](#1.2 转成自定义的 Java 实体类)
- [1.3 `JSON.parseObject` 的反射原理解析](#1.3
JSON.parseObject的反射原理解析) - [1.4 经典问题:如何解析泛型集合](#1.4 经典问题:如何解析泛型集合)
- [1.5 Java 的"类型擦除"](#1.5 Java 的“类型擦除”)
- [1.0 `json`是什么](#1.0
- 二、代码实战
-
- [2.1 准备测试数据](#2.1 准备测试数据)
- [2.2 实体类定义](#2.2 实体类定义)
- [2.3 解析实体类内部的泛型集合](#2.3 解析实体类内部的泛型集合)
- [2.4 直接解析顶层匿名泛型集合](#2.4 直接解析顶层匿名泛型集合)
- [2.5 `TypeReference` 终极破解](#2.5
TypeReference终极破解)
- 总结
前言
在 Java 开发尤其是 Web 后端开发中,我们每天都在和前后端的数据交互打交道。而这其中,出场率最高、也最容易让人踩坑的 API 之一,绝对有阿里巴巴 Fastjson 库中的 JSON.parseObject()。
在使用 Fastjson(或其他 JSON 解析库)进行日常开发时,解析普通对象往往手到擒来。但一旦遇到多层嵌套的对象 或泛型集合 ,各种 ClassCastException 就会接踵而至。
很多开发者只知道去搜一行 TypeReference 的代码贴上去解决问题,却不明白底层到底发生了什么。今天,我们就用循序渐进的实战代码,结合底层 JVM 的"类型擦除"机制,彻底弄清楚JSON.parseObject().
一、JSON.parseObject()是什么
JSON.parseObject() 的本质是一个"极速翻译官"。
在互联网传输中,数据必须以**纯文本(字符串)**的形式流动,比如 {"name": "Alice", "age": 20}。但是,Java 是一门面向对象的强类型语言,它只认识 对象(Object) 。
JSON.parseObject() 的工作,就是读取这段字符串,然后在 Java 的内存中创造出一个对应的、活生生的 Java 对象,把数据严丝合缝地塞进去。
这个过程在计算机科学中被称为反序列化(Deserialization)。

1.0 json是什么
JSON本质就是一种"有结构的字符串格式",用来表示数据。
1. 格式规范
-
key 必须加双引号
{ "name": "Alice" } ✅
{ name: "Alice" } ❌ -
字符串必须用双引号
"name": "Alice" ✅
"name": 'Alice' ❌ -
不能有多余逗号
{
"name": "Alice",
"age": 20
} ✅{
"name": "Alice",
"age": 20,
} ❌
2. JSON常见类型如下:
JSON 中
{}表示对象(必须是键值对结构),[]表示数组(可包含多个元素)。如果需要表达多个对象,必须使用数组,而不能在
{}中直接堆叠对象。
- 对象(Object)------用
{}表示
-
类似 Java 中的
Map<String, Object>json{ "name": "Alice", "age": 20, "isStudent": true } -
特点:
-
用
{}包裹 -
数据是 键值对(key-value)
-
key 必须是字符串(必须加双引号)
-
- 数组(Array)------用
[]表示
-
类似 Java 中的
Listjson[ "Apple", "Banana", "Orange" ] -
特点:
-
用
[]包裹 -
里面是有序数据集合
-
每个元素可以是任意类型
-
JSON的嵌套结构
-
JSON最大的特点是:可以无限嵌套json{ "name": "Agent", "actions": [ { "actionName": "Click", "target": "ButtonA" }, { "actionName": "Input", "target": "SearchBox" } ], "scores": { "confidence": 0.95 } }这对应 Java 结构:
javaclass Agent { String name; List<Action> actions; Map<String, Double> scores; }
1.1 转成通用的 JSONObject
不想写实体类,或者外部传来的 JSON 字段很不稳定,只需要提取其中一两个值的时候,用这种方式最合适。
深度理解: JSONObject 底层其实就是实现了 Map<String, Object> 接口。它相当于给你提供了一个万能容器,不管 JSON 长什么样都能装下。
java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class Demo1 {
public static void main(String[] args) {
String jsonStr = "{\"orderId\":\"O_123\", \"amount\": 99.8, \"status\":\"SUCCESS\"}";
// 核心代码:直接转为 JSONObject
JSONObject jsonObj = JSON.parseObject(jsonStr);
// 像 Map 一样去取值
String orderId = jsonObj.getString("orderId");
Double amount = jsonObj.getDouble("amount");
System.out.println("订单号: " + orderId + ", 金额: " + amount);
}
}
1.2 转成自定义的 Java 实体类
这是企业级开发中最规范的做法。你需要明确指定一个"模具"(Class),让 Fastjson 按照这个模具去浇筑对象。
为 JSON 建立对应的 Java 实体类,有3个要点:
- 必须保留公共无参构造函数(Fastjson 靠它通过反射实例化对象)。
- 嵌套类尽量使用
public static class(静态内部类),否则 Fastjson 无法独立实例化它。 - 必须提供标准的 Getter/Setter (可以使用 Lombok 的
@Data注解)。
java
public class User {
private String name;
private Integer age;
// 省略 Getter 和 Setter 方法 (极其重要,后面会讲坑点)
}
public class Demo2 {
public static void main(String[] args) {
String jsonStr = "{\"name\": \"李白\", \"age\": 18}";
// 核心代码:传入字符串和目标类的 Class 对象
User user = JSON.parseObject(jsonStr, User.class);
System.out.println("用户名: " + user.getName());
}
}
1.3 JSON.parseObject 的反射原理解析
执行 JSON.parseObject(json, User.class) 时,Fastjson 底层借助java的反射机制 实现json字符串和类对象的转换。原理如下:
-
词法解析 (Lexical Analysis -
JSONLexer)-
Fastjson 不会一开始就去管你的 Java 类。它首先像编译器一样,逐个字符读取 JSON 字符串,将其拆解成一个个 Token(标识符)。
-
比如遇到
{就知道是一个对象的开始,遇到"name"就知道是一个 Key。
-
-
获取元数据 (Metadata &
JavaBeanInfo)-
解析器拿到目标类
User.class后,会启动反射引擎。 -
它通过
Class.getDeclaredFields()和Class.getMethods()扫描这个类的所有结构。 -
它会重点寻找:无参构造函数、所有的 Setter 方法。
-
为了极致的性能,Fastjson 会把这些反射拿到的类结构信息缓存起来(存入
ParserConfig的 ConcurrentHashMap 中),下次再解析同一个类时,直接从内存取,不再进行耗时的反射扫描。
-
-
实例化空对象 (Instantiation)
- 通过反射拿到无参构造函数后,执行
constructor.newInstance(),在内存中创造出一个全是 null 值的空对象。
- 通过反射拿到无参构造函数后,执行
-
类型推导与反序列化 (Deserialization)
-
读取 JSON 里的键值对。假设读到
"age": "18"。 -
通过前面缓存的元数据,发现
User类里有一个setAge(Integer age)方法。 -
此时触发类型转换 (TypeUtils) :JSON 里是字符串
"18",目标方法要的是Integer,Fastjson自动将其转换为整数18。
-
-
动态注入 (Method.invoke)
- 最后一步,通过反射机制执行
method.invoke(userInstance, 18),把值硬塞进刚才创建的空对象里。整个对象拼装完成。
- 最后一步,通过反射机制执行

1.4 经典问题:如何解析泛型集合
java
String jsonArrayStr = "[{\"name\":\"李白\"}, {\"name\":\"杜甫\"}]";
// User是一个类
// ❌ 错误做法:
// 1. JSON.parseObject不知道List列表中是什么对象
List<User> list = JSON.parseObject(jsonArray, List.class);
// 2.JSON.parseObject(..., User.class) 的返回值类型是 User,提示 Incompatible types(类型不兼容),代码根本跑不起来。
List<User> list = JSON.parseObject(jsonArray, User.class);
// 正确写法
// 1. 使用 parseArray,它专门用来对付以 [ 开头的数组
List<User> userList = JSON.parseArray(jsonArrayStr, User.class);
// 2. 使用TypeReference
Map<String, List<User>> complexData = JSON.parseObject(
"复杂的JSON串",
new TypeReference<Map<String, List<User>>>(){}
);
要用 parseObject 解析极其复杂的结构,必须加上大括号 {} 创建匿名内部类,把泛型写进去。
关于
parseObject解析极其复杂的结构,Fastjson 祭出了大杀器:TypeReference。既然运行时的对象记不住泛型,那我们就"现场造一个带泛型基因的新图纸"。
代码末尾的
{}表示我们在现场创建了一个继承自TypeReference的匿名内部类 。Java 规定,类的父类泛型信息会永久保留在字节码中。Fastjson 拿到这个匿名类后,顺藤摸瓜查它的"族谱"(父类),就能找回完整的<List<AgentResponse>>基因信息!
1.5 Java 的"类型擦除"
对于比较老的java版本,这个写法可以通过编译,但是依然没有实现json到类对象的转化,原因是java的类型"擦除":
java
List<User> list = JSON.parseObject(jsonArray, List.class);
这里传给 Fastjson 的目标类型是 List.class,在运行时,泛型 <User> 被彻底擦除了,Fastjson 只知道你要一个 List,但不知道里面装什么,于是只能按最通用的 JSONObject 来兜底。
假设 Java 的世界是一个大型的物流中心。
泛型(比如
List<AgentResponse>)是什么?它就像是贴在普通快递纸箱外面的一张便利贴 ,上面写着:"这里面只能装 AgentResponse!"
- 在发货前(写代码、编译时): Java 编译器(安检员)会死死盯着这张便利贴。如果你试图往纸箱里塞一个苹果(比如
String),安检员会立刻报错,阻止你发货。这就是泛型最大的作用:在写代码时保证类型安全。- 在运输途中(程序运行起来时,即 Runtime): Java 有一个极其坑爹的机制叫做"类型擦除"。为了省事和兼容老版本的 Java,纸箱一旦装上车(程序跑起来了),外面的那张便利贴就会被撕掉!
这就是真相:在程序真正运行的那一刻,所有的
List<AgentResponse>、List<String>、List<User>,在 JVM(Java虚拟机)眼里,全都变成了一个光秃秃的普通纸箱 ------ 仅仅只是一个List。Fastjson的设计流程如下:
- Fastjson 拿到了 JSON 字符串:
[{"intent":"search"}]。- Fastjson 看到了你传进来的目标类型:
List.class。- Fastjson 问 Java 虚拟机(JVM):"老哥,他让我解析成一个
List,但这纸箱里面到底该装什么类型的对象啊?"- JVM 摊摊手:"不知道啊,便利贴(泛型)在程序一跑起来就被撕掉了(擦除了),我只知道它是个纸箱(List)。"
- Fastjson 无奈了:"既然不知道里面装啥,那我只能随便拿个最基础的容器给你兜着了。"于是,Fastjson 把 JSON 里的每一个
{}都变成了通用的JSONObject(相当于一个 Map),统统扔进了 List 里。最终,你拿到的
list里面,装的全是JSONObject,而不是你想要的User。
**
二、代码实战
测试源码链接 :https://github.com/likerhood/CodeDesignWork/tree/main/codedesign0.0-0
2.1 准备测试数据
准备一份模拟外部 AI Agent 系统传来的 JSON 数据。
这是一个包含复杂嵌套(对象内部有 List 和 Map)的 JSON 数组:
JSON
[
{
"intent": "execute",
"actions": [
{"actionName": "Click", "target": "ButtonA"},
{"actionName": "Input", "target": "SearchBox"}
],
"confidenceScores": {
"execution_rate": 0.95
}
},
{
"intent": "summarize",
"actions": [
{"actionName": "Scroll", "target": "PageBottom"}
],
"confidenceScores": {
"summary_rate": 0.88
}
}
]
2.2 实体类定义
我们需要为上面的 JSON 建立对应的 Java 实体类,注意以下三点:
- 必须保留公共无参构造函数
- 嵌套类尽量使用
public static class(静态内部类) - 必须提供标准的 Getter/Setter(这里为了代码直观,使用了 Lombok 的
@Data注解)。
Java
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class AgentResponse {
private String intent;
// 重点关注:这是一个包含泛型的集合属性
private List<Action> actions;
private Map<String, Double> confidenceScores;
@Data
public static class Action {
private String actionName;
private String target;
}
}
2.3 解析实体类内部的泛型集合
疑惑点: Java 运行时会发生"类型擦除",那 AgentResponse 内部的 List<Action> 里的泛型 <Action> 会被擦除吗?Fastjson 会不会解析失败?
结论: 不会!完美解析。
底层原理: Java 的类型擦除只擦除内存中实例对象 的泛型,绝对不会擦除类结构声明上的泛型 .编译器会把 List<Action> 这个泛型签名死死地烙印在 .class 字节码文件中。Fastjson 底层通过 Field.getGenericType() ,查阅字节码文件,就能精准得知必须装入 Action 对象。
Java
import com.alibaba.fastjson.JSON;
// 1. 读取本地 test.json 文件(包含多个 Agent 响应的数组)
String jsonStr = new String(Files.readAllBytes(Paths.get("test.json")));
// 2. 先解析为 List 集合
List<AgentResponse> list = JSON.parseObject(jsonStr, new TypeReference<List<AgentResponse>>(){});
// 3. 取出第一个对象来验证"内部嵌套泛型"
AgentResponse response = list.get(0);
// 4. 核心验证:测试能否正常拿取内部嵌套的 Action 对象
// 如果泛型被擦除,这里拿到的会是 JSONObject,调用 getActionName() 会直接报错
System.out.println("外层意图: " + response.getIntent());
System.out.println("内层第一个动作名: " + response.getActions().get(0).getActionName());
System.out.println("内层第一个动作目标: " + response.getActions().get(0).getTarget());
运行结果:成功输出,没有任何报错。
2.4 直接解析顶层匿名泛型集合
💡 疑惑点: 如果我们直接把刚才那个整体的 JSON 数组 传进去,并试图用 List.class 去接,会发生什么?
Java
import com.alibaba.fastjson.JSON;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
public class DemoTrap {
public static void main(String[] args) throws Exception {
// 1. 读取我们的 test.json 文件(一个完整的 [] 数组字符串)
String jsonArrayString = new String(Files.readAllBytes(Paths.get("test.json")));
// 2. ❌ 致命错误示范:直接用 List.class 去接
List<AgentResponse> list = JSON.parseObject(jsonArrayString, List.class);
// 3. 表面上看没有报错,它甚至能打印出正确的大小!
System.out.println("集合大小: " + list.size());
// 4. 🚨 运行时爆炸:这一步必定抛出 ClassCastException 异常!
AgentResponse firstResponse = list.get(0);
}
}
原因: 当我们在方法参数中传递
List.class时,程序已经运行起来了。此时,JVM 遵循"类型擦除",把泛型便利贴无情撕掉。Fastjson 问 JVM:"这个 List 里面装什么?"JVM答:"不知道,就个普通 List。"既然不知道,
Fastjson只能用最基础的通用字典JSONObject来兜底。所以,list.get(0)拿到的根本不是AgentResponse,而是一个JSONObject!强转必定报错。
2.5 TypeReference 终极破解
代码末尾的 {} 表示我们在现场创建了一个继承自 TypeReference 的匿名内部类。
Java 规定,类的父类泛型信息会永久保留在字节码中。
Fastjson 拿到这个匿名类后,顺藤摸瓜查它的"族谱"(父类),就能找回完整的 <List<AgentResponse>> 基因信息!
Java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
public class DemoSuccess {
public static void main(String[] args) throws Exception {
// 1. 读取我们一开始准备的 JSON 测试文件
String jsonStr = new String(Files.readAllBytes(Paths.get("test.json")));
// 2. ✅ 正确示范:使用带大括号 {} 的 TypeReference 锁死完整的泛型树
List<AgentResponse> list = JSON.parseObject(
jsonStr,
new TypeReference<List<AgentResponse>>() {}
);
// 3. 验证成果
for (AgentResponse agent : list) {
System.out.println("----------");
System.out.println("Agent 意图: " + agent.getIntent());
System.out.println("第一个动作目标: " + agent.getActions().get(0).getTarget());
System.out.println("执行置信度: " + agent.getConfidenceScores());
}
}
}
总结
JSON.parseObject() 本质是通过 反射 + 类型推导 将 JSON 字符串还原为 Java 对象。
在实际开发中,问题的核心并不在 API 本身,而在于 Java 的类型机制:
- 普通对象解析:依赖无参构造 + Setter,按字段映射即可完成
- 类内部泛型:泛型信息保存在字节码中,Fastjson 可通过反射获取,解析无压力
- 顶层泛型集合 :由于类型擦除 ,运行时丢失泛型信息,必须借助
TypeReference补全类型
👉 一句话记住:
能解析 ≠ 写对了,关键在于"类型信息是否在运行时可获取"