12.01 数据解析-JSON解析
JSON 是 Android 接口数据的事实标准。本节以 Gson 为主线(Retrofit 的默认搭档),讲清双向工作流、泛型解析、注解定制与高频坑,末尾对比其他解析库。
# 一、Gson 工作流:反射驱动的双向映射
Gson 的核心机制:通过反射读取实体字段名,与 JSON 的 key 一一匹配赋值/提取。所以它对实体只有两个要求:字段可访问 + 有无参构造(Kotlin data class 天然满足)。理解了"反射匹配"这个原理,后面所有坑(字段名不一致、泛型擦除、类型不匹配)都能自己推出来。
implementation 'com.google.code.gson:gson:2.10.1'
# 二、基本用法:fromJson 与 toJson
/**
* 用户实体:字段与后端JSON一一对应.
*/
public class User {
private long id;
private String name;
private int age;
private List<String> tags;
// 无参构造(Gson反射实例化需要;不写则依赖默认构造)
public User() { }
// getter/setter省略
}
Gson gson = new Gson();
// 1. 反序列化:JSON字符串 → 对象
String json = "{\"id\":1,\"name\":\"JiangMing\",\"age\":26,\"tags\":[\"Android\",\"Java\"]}";
User user = gson.fromJson(json, User.class);
Log.i(TAG, user.getName()); // JiangMing
// 2. 序列化:对象 → JSON字符串(Retrofit @Body 内部就是这一步)
String out = gson.toJson(user);
// 3. 基本类型/集合也支持
int id = gson.fromJson("42", int.class);
List<String> tags = gson.fromJson("[\"Android\",\"Java\"]", List.class); // ⚠ 泛型问题见下节
# 三、泛型解析:TypeToken 与擦除陷阱
List<User> 直接解析会踩泛型擦除的坑:
// 💥 错误:运行时List里装的是LinkedTreeMap,不是User!
List<User> users = gson.fromJson(jsonArray, List.class);
User u = users.get(0); // 💥 ClassCastException(取出来才炸,更隐蔽)
// ✔ 正确:用TypeToken保留完整泛型类型
Type type = new TypeToken<List<User>>() {}.getType();
List<User> users = gson.fromJson(jsonArray, type);
User u = users.get(0); // 真正的User对象
原因:Java 泛型在编译后擦除,List.class 运行时不知道元素类型,Gson 只能解析成 Map。TypeToken<List<User>>(){} 利用匿名子类把泛型信息固化到字节码里。Retrofit 中 Call<List<User>> 的泛型解析同理由转换器处理(它自己会取方法返回值的完整类型,所以声明接口时写全泛型即可)。
# 四、注解定制:@SerializedName 与字段控制
后端字段名与 Java 命名规范不一致(如 user_name vs userName)是最常见的对接摩擦:
public class User {
private long id;
// 后端返回 user_name:映射到Java的userName
@SerializedName("user_name")
private String userName;
// 一个字段对应多个可能的key(多版本接口兼容)
@SerializedName(value = "nickname", alternate = {"nick_name", "nickName"})
private String nickname;
// 序列化时跳过(不提交给服务端)
@Expose(serialize = false, deserialize = true)
private String cachedAvatar;
// transient:双向都跳过
private transient String tempState;
}
序列化/反序列化的其他控制:
// null字段不输出(默认Gson会输出"field":null)
Gson gson = new GsonBuilder()
.serializeNulls() // 反过来:强制输出null
.excludeFieldsWithoutExposeAnnotation() // 只处理@Expose标注的字段
.setDateFormat("yyyy-MM-dd HH:mm:ss") // Date字段格式(默认是US格式,必配!)
.setPrettyPrinting() // 格式化输出(调试用)
.create();
Date类型的坑:Gson 默认把 Date 序列化成美式英文格式(Nov 14, 2026),本地化差异大。项目里统一setDateFormat或干脆约定接口用时间戳(long),实体用 long 而非 Date——这是实践里最省心的做法。
# 五、常见坑清单
- 字段缺失/为 null 不崩溃,但要防空:JSON 里没有的字段,解析后保持 Java 默认值(对象=null、int=0)。基本类型 int/long 无法表达"缺失",可能把"没有数据"误当"0"——对语义敏感的字段用包装类型
Integer(null 可判)。 - 类型不匹配直接抛异常:后端把数字改成字符串(
"age":"26")会导致整个解析失败。防御方式:Gson 注册自定义 TypeAdapter 宽容处理,或与服务端约定类型规范。 - 大 JSON 解析耗时:几百 KB 的 JSON 反射解析几十毫秒起步——确保在子线程(Retrofit 自动),列表页可配合分页减少单次数据量。
fromJson传String.class拿到的是字符串本身;解析失败抛JsonSyntaxException,try-catch 的范围要包住它。- 混淆:release 开启 R8/ProGuard 后,实体类字段被改名/裁剪会解析为空——保留实体的 keep 规则(
-keep class com.example.model.** { *; }),Gson 反射依赖字段名(见 26 构建与发布)。 - JsonElement 三件套:只要部分字段时用
JsonObject/JsonArray手动遍历(gson.fromJson(json, JsonObject.class)),比定义完整实体轻量,适合透传场景。
# 六、org.json 与其他库对比
| 库 | 定位 | 特点 |
|---|---|---|
| Gson | 本节主线 | 反射映射、生态最广(Retrofit 默认搭档之一)、API 简洁 |
org.json(系统内置) | 轻量手动解析 | JSONObject/JSONArray 手动取值;零依赖但样板多,适合解析个别小 JSON |
| Moshi | Square 出品 | Kotlin 友好、支持 codegen(KAPT 生成适配器,比反射快)、与 Retrofit 搭配成熟 |
| kotlinx.serialization | Kotlin 官方 | 编译期生成、零反射、类型最安全;Kotlin 项目新首选 |
| Fastjson | 阿里 | 速度快,但历史安全漏洞多,Android 端不推荐 |
// org.json 手动解析示例(系统内置,无需依赖)
JSONObject root = new JSONObject(json);
long id = root.getLong("id");
JSONArray tags = root.getJSONArray("tags");
for (int i = 0; i < tags.length(); i++) {
Log.i(TAG, tags.getString(i));
}
选型建议:Java 项目 Gson;Kotlin 项目 kotlinx.serialization 或 Moshi;临时小解析用 org.json。
# 结束
本节系统讲解了 JSON 解析:Gson 的反射双向映射工作流(配图)、fromJson/toJson 基本用法、TypeToken 解决泛型擦除、@SerializedName/@Expose/transient 字段控制、Date 格式与 null 序列化配置,六条高频坑清单(基本类型语义、混淆 keep 规则等),以及 org.json/Moshi/kotlinx.serialization 的选型对比。JSON 解析的几乎所有问题都能归结为一句话:Gson 靠反射按字段名匹配,匹配不上就出问题。下一节看另一种格式:12.02 XML 解析。更多内容请参考:Gson 用户指南 (opens new window)。